Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
57 changes: 36 additions & 21 deletions frontend/src/components/ui/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
type PaginationState,
useReactTable,
} from "@tanstack/react-table"

Expand All @@ -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<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
isLoading?: boolean
control?: DataTableControl
pageCount?: number
totalItems?: number
onRowClick?: (row: TData) => void
}

export function DataTable<TData, TValue>({
columns,
data,
isLoading,
control,
pageCount,
totalItems,
onRowClick,
}: DataTableProps<TData, TValue>) {
const [pagination, setPagination] = useState<PaginationState>({
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 (
<div className="flex-1 flex flex-col w-full">
<div className="flex-1 overflow-x-auto">
Expand All @@ -59,22 +71,26 @@ export function DataTable<TData, TValue>({
}}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground animate-pulse">
Ładowanie...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="even:bg-muted/50 hover:bg-muted/80 transition-colors"
onClick={() => onRowClick?.(row.original)}
className={`even:bg-muted/50 hover:bg-muted/80 transition-colors ${onRowClick ? "cursor-pointer" : ""}`}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} style={{
Expand All @@ -99,13 +115,12 @@ export function DataTable<TData, TValue>({

<div className="shrink-0 flex items-center justify-between px-4 py-4 border-t bg-muted/10">
<div className="text-sm text-muted-foreground font-medium">
Łącznie: <span className="text-foreground">{data.length}</span> pozycji
Łącznie: <span className="text-foreground">{displayTotal}</span> pozycji
</div>

<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center justify-center text-sm font-medium">
Strona {table.getPageCount() === 0 ? 0 : table.getState().pagination.pageIndex + 1} z{" "}
{table.getPageCount()}
Strona {totalPages === 0 ? 0 : currentPage} z {totalPages}
</div>

<div className="flex items-center space-x-2">
Expand All @@ -132,4 +147,4 @@ export function DataTable<TData, TValue>({
</div>
</div>
)
}
}
73 changes: 73 additions & 0 deletions frontend/src/features/employee-registry/EmployeeRegistryTable.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="py-8 text-center text-destructive text-sm">
Błąd pobierania pracowników.
</div>
);

return (
<div className="w-full space-y-4">
<div className="flex flex-wrap gap-3">
<Input
placeholder="Szukaj po imieniu, nazwisku lub e-mailu..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="flex-1 min-w-62.5"
/>
<Select
value={roleFilter ?? 'all'}
onValueChange={(value) => handleRoleChange(value === 'all' ? null : value as UserRole)}
>
<SelectTrigger className="w-50">
<SelectValue placeholder="Wszystkie role" />
</SelectTrigger>
<SelectContent>
{ROLE_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>

<DataTable
columns={columns}
data={users?.items ?? []}
isLoading={isLoading}
control={tableControl}
pageCount={users?.totalPages ?? 0}
totalItems={users?.totalCount}
onRowClick={(row: UserResponse) =>
navigate(PATHS.EMPLOYEE_DETAILS(row.id), { state: { user: row } })
}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<tr
onClick={() => navigate(PATHS.EMPLOYEE_DETAILS(user.id), { state: { user } })}
className="bg-white hover:bg-gray-50 cursor-pointer"
>
<td className="px-4 py-3 font-medium text-gray-900">
{user.name && user.surname ? `${user.name} ${user.surname}` : '—'}
</td>
<td className="px-4 py-3 text-gray-600">{user.email}</td>
<td className="px-4 py-3"><RoleBadge role={user.role} /></td>
</tr>
);
};
Original file line number Diff line number Diff line change
@@ -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<UserResponse>[] => [
{
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 }) => (
<span className="text-muted-foreground">{row.original.email}</span>
),
},
{
accessorKey: 'role',
header: 'Rola',
cell: ({ row }) => <RoleBadge role={row.original.role} />,
},
];
42 changes: 42 additions & 0 deletions frontend/src/features/employee-registry/employee-registry.hooks.ts
Original file line number Diff line number Diff line change
@@ -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<UserRole | null>(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,
};
};
1 change: 1 addition & 0 deletions frontend/src/features/employee-registry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EmployeeRegistryTable } from './EmployeeRegistryTable';
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
Loading
Loading