Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/renderer/src/components/ui/metrics-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface MetricsSectionProps {

export function MetricsSection({ kpis, className }: MetricsSectionProps) {
return (
<div className={cn("grid gap-3 md:grid-cols-2 lg:grid-cols-4", className)}>
<div className={cn("grid gap-2 md:grid-cols-2 lg:grid-cols-4", className)}>
{kpis.map((kpi, index) => (
<Card className="p-4" key={index}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-2">
Expand Down
276 changes: 251 additions & 25 deletions src/renderer/src/pages/agencies-page.tsx
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";
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type Safety Issue: Using any type for state variables reduces type safety. Consider defining proper TypeScript interfaces for agency data.

Suggested change
const [editingAgency, setEditingAgency] = useState<any>(null);
interface Agency {
id: string;
name: string;
code?: string;
isActive: boolean;
createdAt?: string;
updatedAt?: string;
}
const [selectedAgency, setSelectedAgency] = useState<Agency | null>(null);

const [deletingAgency, setDeletingAgency] = useState<any>(null);
const [selectedAgency, setSelectedAgency] = useState<any>(null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type Safety: Using any type reduces type safety. Consider defining a proper TypeScript interface for the agency state.

Suggested change
const [selectedAgency, setSelectedAgency] = useState<any>(null);
const [selectedAgency, setSelectedAgency] = useState<Agency | null>(null);

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(
() => ({
Expand Down Expand Up @@ -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")}
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance Concern: The AgencyDetailPanel component is always mounted and fetching data via useEmployees() and useContracts() hooks, even when the panel isn't visible. This means all employees and contracts are fetched on page load.

Consider:

  1. Using enabled: !!agency in the query if supported
  2. Fetching filtered data at the API level
  3. Using React.memo to prevent unnecessary re-renders
Suggested change
agency: any;
const { data: employees = [] } = useEmployees({
enabled: !!agency
});

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",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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:

  1. Using a more conventional position (top-right)
  2. Using an X icon instead of text
  3. Adding a tooltip
Suggested change
<Button
className="absolute top-2 right-2 z-10"
size="icon"
variant="ghost"
onClick={onClose}
>
<X className="h-4 w-4" />
<span className="sr-only">Close panel</span>
</Button>

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()
Expand Down
Loading