-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add resizable side panel for agency details (#81) #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||
| import { useQueryClient } from "@tanstack/react-query"; | ||||||||||||||||||||||
| import { Building2, Edit, Plus, Search, Trash2 } from "lucide-react"; | ||||||||||||||||||||||
| import { Building2, Edit, Plus, Search, Trash2, X } from "lucide-react"; | ||||||||||||||||||||||
| import { useEffect, useMemo, useState } from "react"; | ||||||||||||||||||||||
| import { useTranslation } from "react-i18next"; | ||||||||||||||||||||||
| import { Button } from "@/components/ui/button"; | ||||||||||||||||||||||
|
|
@@ -8,6 +8,7 @@ import { ErrorDisplay } from "@/components/ui/error-display"; | |||||||||||||||||||||
| import { Input } from "@/components/ui/input"; | ||||||||||||||||||||||
| import { MetricsSection } from "@/components/ui/metrics-section"; | ||||||||||||||||||||||
| import { PageHeaderCard } from "@/components/ui/page-header-card"; | ||||||||||||||||||||||
| import { Card } from "@/components/ui/card"; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| Select, | ||||||||||||||||||||||
| SelectContent, | ||||||||||||||||||||||
|
|
@@ -24,11 +25,18 @@ import { | |||||||||||||||||||||
| TableRow, | ||||||||||||||||||||||
| } from "@/components/ui/table"; | ||||||||||||||||||||||
| import { PageHeaderSkeleton } from "@/components/ui/table-skeleton"; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| ResizableHandle, | ||||||||||||||||||||||
| ResizablePanel, | ||||||||||||||||||||||
| ResizablePanelGroup, | ||||||||||||||||||||||
| } from "@/components/ui/resizable"; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| useCreateAgency, | ||||||||||||||||||||||
| useDeleteAgency, | ||||||||||||||||||||||
| useAgencies, | ||||||||||||||||||||||
| useUpdateAgency, | ||||||||||||||||||||||
| useEmployees, | ||||||||||||||||||||||
| useContracts, | ||||||||||||||||||||||
| } from "@/hooks"; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| Dialog, | ||||||||||||||||||||||
|
|
@@ -39,22 +47,32 @@ import { | |||||||||||||||||||||
| DialogTitle, | ||||||||||||||||||||||
| } from "@/components/ui/dialog"; | ||||||||||||||||||||||
| import { Label } from "@/components/ui/label"; | ||||||||||||||||||||||
| import { useSidebar } from "@/components/ui/sidebar"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function AgenciesPage() { | ||||||||||||||||||||||
| const { t } = useTranslation(); | ||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||
| const { setOpen } = useSidebar(); | ||||||||||||||||||||||
| const [search, setSearch] = useState(""); | ||||||||||||||||||||||
| const [statusFilter, setStatusFilter] = useState<string>("all"); | ||||||||||||||||||||||
| const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); | ||||||||||||||||||||||
| const [editingAgency, setEditingAgency] = useState<any>(null); | ||||||||||||||||||||||
| const [deletingAgency, setDeletingAgency] = useState<any>(null); | ||||||||||||||||||||||
| const [selectedAgency, setSelectedAgency] = useState<any>(null); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type Safety: Using
Suggested change
If you have an Agency interface defined elsewhere in the codebase, please import and use it. |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Use TanStack Query hooks | ||||||||||||||||||||||
| const { data: agencies = [], isLoading, error } = useAgencies(); | ||||||||||||||||||||||
| const createAgency = useCreateAgency(); | ||||||||||||||||||||||
| const updateAgency = useUpdateAgency(); | ||||||||||||||||||||||
| const deleteAgency = useDeleteAgency(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Auto-close sidebar when agency is selected | ||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| if (selectedAgency) { | ||||||||||||||||||||||
| setOpen(false); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, [selectedAgency, setOpen]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // KPIs - calculated dynamically | ||||||||||||||||||||||
| const kpis = useMemo( | ||||||||||||||||||||||
| () => ({ | ||||||||||||||||||||||
|
|
@@ -141,8 +159,10 @@ export function AgenciesPage() { | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <> | ||||||||||||||||||||||
| <div className="flex flex-1 flex-col gap-4 p-4 pt-6"> | ||||||||||||||||||||||
| <div className="min-h-full space-y-3"> | ||||||||||||||||||||||
| <ResizablePanelGroup className={selectedAgency ? "bg-sidebar gap-0.5 p-1.5" : "gap-0.5 p-1.5"} direction="horizontal"> | ||||||||||||||||||||||
| <ResizablePanel defaultSize={selectedAgency ? 50 : 100} minSize={30} className={selectedAgency ? "border border-border rounded-md bg-background" : "bg-background"}> | ||||||||||||||||||||||
| <div className="flex flex-1 flex-col gap-4 p-4 pt-6"> | ||||||||||||||||||||||
| <div className="min-h-full space-y-3"> | ||||||||||||||||||||||
| {/* Header */} | ||||||||||||||||||||||
| <PageHeaderCard | ||||||||||||||||||||||
| description={t("agencies.description")} | ||||||||||||||||||||||
|
|
@@ -257,7 +277,11 @@ export function AgenciesPage() { | |||||||||||||||||||||
| </TableHeader> | ||||||||||||||||||||||
| <TableBody> | ||||||||||||||||||||||
| {filteredAgencies.map((agency) => ( | ||||||||||||||||||||||
| <TableRow className="hover:bg-muted/50" key={agency.id}> | ||||||||||||||||||||||
| <TableRow | ||||||||||||||||||||||
| className="cursor-pointer hover:bg-muted/50" | ||||||||||||||||||||||
| key={agency.id} | ||||||||||||||||||||||
| onClick={() => setSelectedAgency(agency)} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| <TableCell className="px-4"> | ||||||||||||||||||||||
| {agency.code ? ( | ||||||||||||||||||||||
| <span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-0.5 font-medium text-xs"> | ||||||||||||||||||||||
|
|
@@ -322,35 +346,237 @@ export function AgenciesPage() { | |||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </ResizablePanel> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Create Dialog */} | ||||||||||||||||||||||
| {isCreateDialogOpen && ( | ||||||||||||||||||||||
| <CreateAgencyDialog | ||||||||||||||||||||||
| onClose={() => setIsCreateDialogOpen(false)} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| {/* Resizable Handle */} | ||||||||||||||||||||||
| {selectedAgency && <ResizableHandle className="w-1 bg-transparent hover:bg-border rounded-md transition-all duration-200" />} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Edit Dialog */} | ||||||||||||||||||||||
| {editingAgency && ( | ||||||||||||||||||||||
| <EditAgencyDialog | ||||||||||||||||||||||
| agency={editingAgency} | ||||||||||||||||||||||
| onClose={() => setEditingAgency(null)} | ||||||||||||||||||||||
| onSave={handleUpdateAgency} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| {/* Detail Panel */} | ||||||||||||||||||||||
| {selectedAgency && ( | ||||||||||||||||||||||
| <ResizablePanel defaultSize={50} minSize={30} className="border border-border rounded-md bg-background"> | ||||||||||||||||||||||
| <AgencyDetailPanel | ||||||||||||||||||||||
| agency={selectedAgency} | ||||||||||||||||||||||
| onClose={() => setSelectedAgency(null)} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| </ResizablePanel> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </ResizablePanelGroup> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Delete Dialog */} | ||||||||||||||||||||||
| {deletingAgency && ( | ||||||||||||||||||||||
| <DeleteAgencyDialog | ||||||||||||||||||||||
| agency={deletingAgency} | ||||||||||||||||||||||
| onClose={() => setDeletingAgency(null)} | ||||||||||||||||||||||
| onConfirm={handleDeleteAgency} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| {/* Dialogs */} | ||||||||||||||||||||||
| <> | ||||||||||||||||||||||
| {/* Create Dialog */} | ||||||||||||||||||||||
| {isCreateDialogOpen && ( | ||||||||||||||||||||||
| <CreateAgencyDialog | ||||||||||||||||||||||
| onClose={() => setIsCreateDialogOpen(false)} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Edit Dialog */} | ||||||||||||||||||||||
| {editingAgency && ( | ||||||||||||||||||||||
| <EditAgencyDialog | ||||||||||||||||||||||
| agency={editingAgency} | ||||||||||||||||||||||
| onClose={() => setEditingAgency(null)} | ||||||||||||||||||||||
| onSave={handleUpdateAgency} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Delete Dialog */} | ||||||||||||||||||||||
| {deletingAgency && ( | ||||||||||||||||||||||
| <DeleteAgencyDialog | ||||||||||||||||||||||
| agency={deletingAgency} | ||||||||||||||||||||||
| onClose={() => setDeletingAgency(null)} | ||||||||||||||||||||||
| onConfirm={handleDeleteAgency} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </> | ||||||||||||||||||||||
| </> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function AgencyDetailPanel({ | ||||||||||||||||||||||
| agency, | ||||||||||||||||||||||
| onClose, | ||||||||||||||||||||||
| }: { | ||||||||||||||||||||||
| agency: any; | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance Concern: The Consider:
Suggested change
|
||||||||||||||||||||||
| onClose: () => void; | ||||||||||||||||||||||
| }) { | ||||||||||||||||||||||
| const { t } = useTranslation(); | ||||||||||||||||||||||
| const { data: employees = [] } = useEmployees(); | ||||||||||||||||||||||
| const { data: contracts = [] } = useContracts(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Get employees with their contract info for this agency | ||||||||||||||||||||||
| const agencyEmployees = useMemo(() => { | ||||||||||||||||||||||
| const now = new Date(); | ||||||||||||||||||||||
| const result: Array<{ | ||||||||||||||||||||||
| employee: typeof employees[0]; | ||||||||||||||||||||||
| contractType?: string; | ||||||||||||||||||||||
| }> = []; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| contracts.forEach((contract) => { | ||||||||||||||||||||||
| if (contract.agencyId === agency.id && contract.isActive) { | ||||||||||||||||||||||
| // Check if contract has ended | ||||||||||||||||||||||
| if (contract.endDate && new Date(contract.endDate) < now) { | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| const employee = employees.find((e) => e.id === contract.employeeId); | ||||||||||||||||||||||
| if (employee) { | ||||||||||||||||||||||
| result.push({ | ||||||||||||||||||||||
| employee, | ||||||||||||||||||||||
| contractType: contract.contractType, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return result; | ||||||||||||||||||||||
| }, [employees, contracts, agency.id]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const contractColors: { [key: string]: string } = { | ||||||||||||||||||||||
| CDI: "bg-blue-500/15 border border-blue-500/25 text-blue-600", | ||||||||||||||||||||||
| CDD: "bg-orange-500/15 border border-orange-500/25 text-orange-600", | ||||||||||||||||||||||
| "Intérim": "bg-teal-500/15 border border-teal-500/25 text-teal-600", | ||||||||||||||||||||||
| Alternance: "bg-purple-500/15 border border-purple-500/25 text-purple-600", | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. UX Suggestion: The close button is positioned at the bottom-left which may be hard to discover. Consider:
Suggested change
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div className="relative flex h-full flex-col overflow-y-auto rounded-md"> | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| className="absolute left-2 bottom-2 z-10" | ||||||||||||||||||||||
| onClick={onClose} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| Close panel | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Header */} | ||||||||||||||||||||||
| <div className="border-b p-4 pt-6"> | ||||||||||||||||||||||
| <PageHeaderCard | ||||||||||||||||||||||
| description={`${agency.code || ""} - ${ | ||||||||||||||||||||||
| agency.isActive | ||||||||||||||||||||||
| ? t("agencies.active") | ||||||||||||||||||||||
| : t("agencies.inactive") | ||||||||||||||||||||||
| }`} | ||||||||||||||||||||||
| icon={ | ||||||||||||||||||||||
| <Building2 className="h-4 w-4" /> | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| title={agency.name} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Info Grid */} | ||||||||||||||||||||||
| <div className="mt-4 grid grid-cols-2 gap-3"> | ||||||||||||||||||||||
| <Card className="p-3"> | ||||||||||||||||||||||
| <p className="text-muted-foreground text-xs">{t("agencies.code")}</p> | ||||||||||||||||||||||
| <p className="mt-0.5 font-medium text-sm"> | ||||||||||||||||||||||
| {agency.code || "-"} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||
| <Card className="p-3"> | ||||||||||||||||||||||
| <p className="text-muted-foreground text-xs"> | ||||||||||||||||||||||
| {t("employees.title")} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| <p className="mt-0.5 text-lg font-semibold"> | ||||||||||||||||||||||
| {agencyEmployees.length} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||
| <Card className="p-3"> | ||||||||||||||||||||||
| <p className="text-muted-foreground text-xs"> | ||||||||||||||||||||||
| {t("agencies.createdAt")} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| <p className="mt-0.5 text-sm"> | ||||||||||||||||||||||
| {agency.createdAt | ||||||||||||||||||||||
| ? new Date(agency.createdAt).toLocaleDateString() | ||||||||||||||||||||||
| : "-"} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||
| <Card className="p-3"> | ||||||||||||||||||||||
| <p className="text-muted-foreground text-xs"> | ||||||||||||||||||||||
| {t("agencies.updatedAt")} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| <p className="mt-0.5 text-sm"> | ||||||||||||||||||||||
| {agency.updatedAt | ||||||||||||||||||||||
| ? new Date(agency.updatedAt).toLocaleDateString() | ||||||||||||||||||||||
| : "-"} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Employees Table */} | ||||||||||||||||||||||
| <div className="flex-1 overflow-auto p-4"> | ||||||||||||||||||||||
| <h3 className="mb-3 text-sm font-medium"> | ||||||||||||||||||||||
| {t("employees.employeesList", "Employees")} | ||||||||||||||||||||||
| </h3> | ||||||||||||||||||||||
| {agencyEmployees.length === 0 ? ( | ||||||||||||||||||||||
| <div className="flex flex-col items-center justify-center rounded-lg border bg-background py-8 text-center"> | ||||||||||||||||||||||
| <Building2 className="mb-2 h-8 w-8 text-muted-foreground" /> | ||||||||||||||||||||||
| <p className="text-muted-foreground text-sm"> | ||||||||||||||||||||||
| {t("agencies.noEmployees", "No employees in this agency")} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||
| <div className="overflow-hidden rounded-lg border bg-card"> | ||||||||||||||||||||||
| <Table> | ||||||||||||||||||||||
| <TableHeader> | ||||||||||||||||||||||
| <TableRow> | ||||||||||||||||||||||
| <TableHead className="px-3"> | ||||||||||||||||||||||
| {t("employeeDetail.fullName")} | ||||||||||||||||||||||
| </TableHead> | ||||||||||||||||||||||
| <TableHead className="px-3"> | ||||||||||||||||||||||
| {t("employees.currentContract")} | ||||||||||||||||||||||
| </TableHead> | ||||||||||||||||||||||
| <TableHead className="px-3"> | ||||||||||||||||||||||
| {t("employeeDetail.status")} | ||||||||||||||||||||||
| </TableHead> | ||||||||||||||||||||||
| </TableRow> | ||||||||||||||||||||||
| </TableHeader> | ||||||||||||||||||||||
| <TableBody> | ||||||||||||||||||||||
| {agencyEmployees.map(({ employee, contractType }) => ( | ||||||||||||||||||||||
| <TableRow | ||||||||||||||||||||||
| key={employee.id} | ||||||||||||||||||||||
| className="hover:bg-muted/50" | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| <TableCell className="px-3 py-2.5"> | ||||||||||||||||||||||
| <div> | ||||||||||||||||||||||
| <p className="font-medium text-sm"> | ||||||||||||||||||||||
| {employee.firstName} {employee.lastName} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| <p className="text-muted-foreground text-xs"> | ||||||||||||||||||||||
| {t("common.employeeId")} | ||||||||||||||||||||||
| {employee.id.toString().padStart(4, "0")} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||
| <TableCell className="px-3 py-2.5"> | ||||||||||||||||||||||
| <span | ||||||||||||||||||||||
| className={`inline-flex items-center rounded-md px-2 py-0.5 font-medium text-xs ${ | ||||||||||||||||||||||
| contractColors[contractType || ""] || | ||||||||||||||||||||||
| "bg-gray-500/15 border border-gray-500/25 text-gray-600" | ||||||||||||||||||||||
| }`} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| {contractType || "-"} | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||
| <TableCell className="px-3 py-2.5"> | ||||||||||||||||||||||
| {employee.status === "active" ? ( | ||||||||||||||||||||||
| <span className="inline-flex items-center rounded-md border border-green-500/25 bg-green-500/15 px-2 py-0.5 font-medium text-green-600 text-xs"> | ||||||||||||||||||||||
| {t("employees.active")} | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||
| <span className="inline-flex items-center rounded-md border border-yellow-500/25 bg-yellow-500/15 px-2 py-0.5 font-medium text-yellow-600 text-xs"> | ||||||||||||||||||||||
| {t("employees.onLeave")} | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||
| </TableRow> | ||||||||||||||||||||||
| ))} | ||||||||||||||||||||||
| </TableBody> | ||||||||||||||||||||||
| </Table> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function generateCode(name: string): string { | ||||||||||||||||||||||
| return name | ||||||||||||||||||||||
| .toUpperCase() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type Safety Issue: Using
anytype for state variables reduces type safety. Consider defining proper TypeScript interfaces for agency data.