diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b83f434e..58056b24 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,12 +19,12 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "date-fns": "^4.1.0", + "date-fns": "^4.2.1", "jwt-decode": "^4.0.0", "lucide-react": "^1.8.0", "radix-ui": "^1.4.3", "react": "^19.2.4", - "react-day-picker": "^10.0.0", + "react-day-picker": "^10.0.1", "react-dom": "^19.2.4", "react-hook-form": "^7.75.0", "react-router-dom": "^7.14.1", @@ -4837,9 +4837,9 @@ } }, "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.2.1.tgz", + "integrity": "sha512-37RhSdxaG1suen6VDCza6rNrQfooyQh57HFVPwQGEq2QWliVLzPQZ8Oa017weOu+HZCnzI7N3Pf/wyoBKfEqrA==", "license": "MIT", "funding": { "type": "github", @@ -7831,9 +7831,9 @@ } }, "node_modules/react-day-picker": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-10.0.0.tgz", - "integrity": "sha512-lrEXo5wFPsq5LTcayelM3BPueD00v7zbdipAY+EIdPcseVykYwkOWx4Ujn/EtbBvpnp8ZPUHol17HXH6kVbZoA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-10.0.1.tgz", + "integrity": "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==", "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", @@ -7847,7 +7847,13 @@ "url": "https://github.com/sponsors/gpbl" }, "peerDependencies": { + "@types/react": ">=16.8.0", "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/react-dom": { diff --git a/frontend/package.json b/frontend/package.json index 442fc3b6..a2f7f9ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,12 +21,12 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "date-fns": "^4.1.0", + "date-fns": "^4.2.1", "jwt-decode": "^4.0.0", "lucide-react": "^1.8.0", "radix-ui": "^1.4.3", "react": "^19.2.4", - "react-day-picker": "^10.0.0", + "react-day-picker": "^10.0.1", "react-dom": "^19.2.4", "react-hook-form": "^7.75.0", "react-router-dom": "^7.14.1", diff --git a/frontend/src/components/ui/data-table.tsx b/frontend/src/components/ui/data-table.tsx index 04feeeab..0e565d0e 100644 --- a/frontend/src/components/ui/data-table.tsx +++ b/frontend/src/components/ui/data-table.tsx @@ -5,7 +5,6 @@ import { flexRender, getCoreRowModel, getPaginationRowModel, - type PaginationState, useReactTable, } from "@tanstack/react-table" @@ -17,34 +16,47 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { useState } from "react" import { Button } from "./button" +import { useDataTableControls, type DataTableControl } from "@/lib/use-data-table-controls" interface DataTableProps { columns: ColumnDef[] data: TData[] + isLoading?: boolean + control?: DataTableControl + pageCount?: number + totalItems?: number + onRowClick?: (row: TData) => void } export function DataTable({ columns, data, + isLoading, + control, + pageCount, + totalItems, + onRowClick, }: DataTableProps) { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, - }); + const backoffControl = useDataTableControls(10) + const activeControl = control ?? backoffControl const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onPaginationChange: setPagination, - state: { - pagination, - }, + ...(control + ? { pageCount: pageCount ?? 0, manualPagination: true } + : { getPaginationRowModel: getPaginationRowModel() } + ), + state: { pagination: activeControl.pagination }, + onPaginationChange: activeControl.onPaginationChange, }) + const displayTotal = totalItems ?? data.length + const currentPage = table.getState().pagination.pageIndex + 1 + const totalPages = table.getPageCount() + return (
@@ -59,22 +71,26 @@ export function DataTable({ }}> {header.isPlaceholder ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} + : flexRender(header.column.columnDef.header, header.getContext())} ))} ))} - {table.getRowModel().rows?.length ? ( + {isLoading ? ( + + + Ładowanie... + + + ) : table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( onRowClick?.(row.original)} + className={`even:bg-muted/50 hover:bg-muted/80 transition-colors ${onRowClick ? "cursor-pointer" : ""}`} > {row.getVisibleCells().map((cell) => ( ({
- Łącznie: {data.length} pozycji + Łącznie: {displayTotal} pozycji
- Strona {table.getPageCount() === 0 ? 0 : table.getState().pagination.pageIndex + 1} z{" "} - {table.getPageCount()} + Strona {totalPages === 0 ? 0 : currentPage} z {totalPages}
@@ -132,4 +147,4 @@ export function DataTable({
) -} \ No newline at end of file +} diff --git a/frontend/src/features/employee-registry/EmployeeRegistryTable.tsx b/frontend/src/features/employee-registry/EmployeeRegistryTable.tsx new file mode 100644 index 00000000..d9cb1da6 --- /dev/null +++ b/frontend/src/features/employee-registry/EmployeeRegistryTable.tsx @@ -0,0 +1,73 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { PATHS } from '@/routes/paths'; +import { UserRole } from '@/features/auth/auth.types'; +import type { UserResponse } from '@/features/user-management/user-management.types'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { DataTable } from '@/components/ui/data-table'; +import { useDataTableControls } from '@/lib/use-data-table-controls'; +import { getEmployeeColumns } from './employee-registry.columns'; +import { useEmployeeRegistry } from './employee-registry.hooks'; + +const ROLE_OPTIONS: { label: string; value: UserRole | 'all' }[] = [ + { label: 'Wszystkie role', value: 'all' }, + { label: 'Pracownik', value: UserRole.COMMON }, + { label: 'Władze Wydziału', value: UserRole.AUTHORITY }, + { label: 'Kierownik Liniowy', value: UserRole.LINEAR_MANAGER }, + { label: 'Kierownik Projektu', value: UserRole.PROJECT_MANAGER }, +]; + +export const EmployeeRegistryTable = () => { + const navigate = useNavigate(); + const tableControl = useDataTableControls(20); + + const { users, isLoading, isError, searchInput, setSearchInput, roleFilter, handleRoleChange } = + useEmployeeRegistry(tableControl.pagination, tableControl.resetPage); + + const columns = useMemo(() => getEmployeeColumns(), []); + + if (isError) return ( +
+ Błąd pobierania pracowników. +
+ ); + + return ( +
+
+ setSearchInput(e.target.value)} + className="flex-1 min-w-62.5" + /> + +
+ + + navigate(PATHS.EMPLOYEE_DETAILS(row.id), { state: { user: row } }) + } + /> +
+ ); +}; diff --git a/frontend/src/features/employee-registry/EmployeeRegistryTableRow.tsx b/frontend/src/features/employee-registry/EmployeeRegistryTableRow.tsx new file mode 100644 index 00000000..e8124d04 --- /dev/null +++ b/frontend/src/features/employee-registry/EmployeeRegistryTableRow.tsx @@ -0,0 +1,25 @@ +import { useNavigate } from 'react-router-dom'; +import { PATHS } from '@/routes/paths'; +import type { UserResponse } from '@/features/user-management/user-management.types'; +import { RoleBadge } from '@/features/user-management/components/RoleBadge'; + +interface EmployeeRegistryTableRowProps { + user: UserResponse; +} + +export const EmployeeRegistryTableRow = ({ user }: EmployeeRegistryTableRowProps) => { + const navigate = useNavigate(); + + return ( + navigate(PATHS.EMPLOYEE_DETAILS(user.id), { state: { user } })} + className="bg-white hover:bg-gray-50 cursor-pointer" + > + + {user.name && user.surname ? `${user.name} ${user.surname}` : '—'} + + {user.email} + + + ); +}; diff --git a/frontend/src/features/employee-registry/employee-registry.columns.tsx b/frontend/src/features/employee-registry/employee-registry.columns.tsx new file mode 100644 index 00000000..48c93096 --- /dev/null +++ b/frontend/src/features/employee-registry/employee-registry.columns.tsx @@ -0,0 +1,26 @@ +import type { ColumnDef } from '@tanstack/react-table'; +import type { UserResponse } from '@/features/user-management/user-management.types'; +import { RoleBadge } from '@/features/user-management/components/RoleBadge'; + +export const getEmployeeColumns = (): ColumnDef[] => [ + { + accessorKey: 'name', + header: 'Imię i nazwisko', + cell: ({ row }) => { + const { name, surname } = row.original; + return name && surname ? `${name} ${surname}` : '—'; + }, + }, + { + accessorKey: 'email', + header: 'E-mail', + cell: ({ row }) => ( + {row.original.email} + ), + }, + { + accessorKey: 'role', + header: 'Rola', + cell: ({ row }) => , + }, +]; diff --git a/frontend/src/features/employee-registry/employee-registry.hooks.ts b/frontend/src/features/employee-registry/employee-registry.hooks.ts new file mode 100644 index 00000000..a07e4fd4 --- /dev/null +++ b/frontend/src/features/employee-registry/employee-registry.hooks.ts @@ -0,0 +1,42 @@ +import { useState } from 'react'; +import { useDebounce } from 'use-debounce'; +import type { PaginationState } from '@tanstack/react-table'; +import { useUsersQuery } from '@/features/user-management/user-management.hooks'; +import { UserStatus } from '@/features/user-management/user-management.types'; +import type { UserRole } from '@/features/auth/auth.types'; + +export const useEmployeeRegistry = (pagination: PaginationState, resetPage: () => void) => { + const [roleFilter, setRoleFilter] = useState(null); + const [searchInput, setSearchInputState] = useState(''); + const [debouncedSearch] = useDebounce(searchInput, 400); + + const setSearchInput = (value: string) => { + setSearchInputState(value); + resetPage(); + }; + + const handleRoleChange = (userRole: UserRole | null) => { + setRoleFilter(userRole); + resetPage(); + }; + + const { users, isLoading, isError } = useUsersQuery( + pagination.pageIndex, + pagination.pageSize, + { + status: UserStatus.ACTIVE, + userRole: roleFilter ?? undefined, + search: debouncedSearch.length > 0 ? debouncedSearch : undefined, + } + ); + + return { + users, + isLoading, + isError, + searchInput, + setSearchInput, + roleFilter, + handleRoleChange, + }; +}; diff --git a/frontend/src/features/employee-registry/index.ts b/frontend/src/features/employee-registry/index.ts new file mode 100644 index 00000000..b0d76337 --- /dev/null +++ b/frontend/src/features/employee-registry/index.ts @@ -0,0 +1 @@ +export { EmployeeRegistryTable } from './EmployeeRegistryTable'; diff --git a/frontend/src/features/user-management/user-management.types.ts b/frontend/src/features/user-management/user-management.types.ts index 5d6466ca..c558602a 100644 --- a/frontend/src/features/user-management/user-management.types.ts +++ b/frontend/src/features/user-management/user-management.types.ts @@ -1,7 +1,12 @@ import type { UserRole } from '@/features/auth/auth.types'; import type { ChartInterval } from '@/features/employee-assignments/employee-assignments.types'; -export type UserStatus = 'PENDING' | 'ACTIVE' | 'EXPIRED'; +export const UserStatus = { + PENDING: 'PENDING', + ACTIVE: 'ACTIVE', + EXPIRED: 'EXPIRED', +} as const; +export type UserStatus = typeof UserStatus[keyof typeof UserStatus]; export const AdminAssignableRole = { COMMON: 'COMMON', diff --git a/frontend/src/lib/use-data-table-controls.ts b/frontend/src/lib/use-data-table-controls.ts new file mode 100644 index 00000000..6dce828f --- /dev/null +++ b/frontend/src/lib/use-data-table-controls.ts @@ -0,0 +1,23 @@ +import { useState } from "react" +import type { PaginationState } from "@tanstack/react-table" + +export interface DataTableControl { + pagination: PaginationState + onPaginationChange: React.Dispatch> + resetPage: () => void +} + +export function useDataTableControls(initialPageSize = 10): DataTableControl { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: initialPageSize, + }) + + const resetPage = () => setPagination((p) => ({ ...p, pageIndex: 0 })) + + return { + pagination, + onPaginationChange: setPagination, + resetPage, + } +} diff --git a/frontend/src/pages/EmployeeDetailsPage.tsx b/frontend/src/pages/EmployeeDetailsPage.tsx new file mode 100644 index 00000000..630a7f19 --- /dev/null +++ b/frontend/src/pages/EmployeeDetailsPage.tsx @@ -0,0 +1,33 @@ +import { useLocation, useParams } from 'react-router-dom'; +import { ROUTE_PARAMS } from '@/routes/paths'; +import type { UserResponse } from '@/features/user-management/user-management.types'; +import { RoleBadge } from '@/features/user-management/components/RoleBadge'; + +export const EmployeeDetailsPage = () => { + const { [ROUTE_PARAMS.USER_ID]: userId } = useParams(); + const location = useLocation(); + const user = location.state?.user as UserResponse | undefined; + + const fullName = user?.name && user?.surname ? `${user.name} ${user.surname}` : userId; + + return ( +
+
+
+

{fullName}

+ {user && ( +
+ {user.email} + · + +
+ )} +
+ +
+ Profil kompetencyjny pracownika — w trakcie realizacji +
+
+
+ ); +}; diff --git a/frontend/src/pages/EmployeeRegistryPage.tsx b/frontend/src/pages/EmployeeRegistryPage.tsx new file mode 100644 index 00000000..38a00c40 --- /dev/null +++ b/frontend/src/pages/EmployeeRegistryPage.tsx @@ -0,0 +1,14 @@ +import { EmployeeRegistryTable } from '@/features/employee-registry'; +import { TablePageShell } from '@/components/layout/TablePageShell'; + +export const EmployeeRegistryPage = () => { + return ( +
+ +
+ +
+
+
+ ); +}; diff --git a/frontend/src/routes/AppRoutes.tsx b/frontend/src/routes/AppRoutes.tsx index 3709767a..1cc4a718 100644 --- a/frontend/src/routes/AppRoutes.tsx +++ b/frontend/src/routes/AppRoutes.tsx @@ -19,6 +19,8 @@ import { QualificationRequestsPage } from '@/pages/QualificationRequestsPage'; import { CreateProjectGroupPage } from '@/pages/CreateProjectGroupPage'; import { ProjectRegistryPage } from '@/pages/ProjectRegistryPage'; import { useAuth } from '@/providers/AuthContext'; +import { EmployeeRegistryPage } from '@/pages/EmployeeRegistryPage'; +import { EmployeeDetailsPage } from '@/pages/EmployeeDetailsPage'; export const AppRoutes = () => { const { user } = useAuth(); @@ -39,7 +41,9 @@ export const AppRoutes = () => { : } /> } /> } /> - } /> {/* <-- Przywrócone z developa */} + } /> + } /> + } /> } /> {/* LINEAR MANAGER ROUTES */} diff --git a/frontend/src/routes/navigation.ts b/frontend/src/routes/navigation.ts index 0a599b5d..a5d45932 100644 --- a/frontend/src/routes/navigation.ts +++ b/frontend/src/routes/navigation.ts @@ -3,7 +3,7 @@ import { ClipboardList, FolderPlus, ShieldAlert, - GraduationCap, PlusSquare, FolderOpen, Briefcase + GraduationCap, PlusSquare, FolderOpen, Briefcase, Users } from 'lucide-react'; import { UserRole } from '@/features/auth/auth.types'; import { PATHS } from '@/routes/paths'; @@ -29,9 +29,8 @@ export const NAV_ITEMS: NavItem[] = [ icon: FolderOpen, roles: [UserRole.AUTHORITY, UserRole.LINEAR_MANAGER, UserRole.PROJECT_MANAGER, UserRole.COMMON], children: [ - // TODO: Gdy juz beda rejestry mozna tutaj odkomentowac { label: 'Rejestr projektów', path: PATHS.PROJECTS_REGISTRY, icon: Briefcase }, - // { label: 'Rejestr pracowników', path: PATHS.EMPLOYEES_REGISTRY, icon: Users }, + { label: 'Rejestr pracowników', path: PATHS.EMPLOYEES_REGISTRY, icon: Users }, ] }, { diff --git a/frontend/src/routes/paths.ts b/frontend/src/routes/paths.ts index b83d57c2..e0b652a2 100644 --- a/frontend/src/routes/paths.ts +++ b/frontend/src/routes/paths.ts @@ -24,4 +24,6 @@ export const PATHS = { CREATE_PROJECT_GROUP: `/create-project-group`, NOTIFICATION: `notifications`, PROJECTS_REGISTRY: "/projects-registry", + EMPLOYEES_REGISTRY: "/employees-registry", + EMPLOYEE_DETAILS: (userId: string) => `/employees/${userId}`, } as const;