Skip to content

Commit f5bc6d2

Browse files
committed
style: edit training assignment drawer
1 parent a90dc2b commit f5bc6d2

File tree

3 files changed

+213
-89
lines changed

3 files changed

+213
-89
lines changed

apps/erp/app/modules/people/ui/Training/TrainingAssignmentForm.tsx

Lines changed: 198 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Select, ValidatedForm } from "@carbon/form";
22
import {
33
Badge,
44
Button,
5+
cn,
6+
Count,
57
HStack,
8+
Input,
69
ModalDrawer,
710
ModalDrawerBody,
811
ModalDrawerContent,
@@ -14,20 +17,21 @@ import {
1417
TabsContent,
1518
TabsList,
1619
TabsTrigger,
20+
ToggleGroup,
21+
ToggleGroupItem,
1722
VStack,
1823
} from "@carbon/react";
1924
import { useFetcher } from "@remix-run/react";
20-
import { memo, useState } from "react";
21-
import type { ColumnDef } from "@tanstack/react-table";
25+
import { memo, useMemo, useState } from "react";
2226
import {
2327
LuTriangleAlert,
2428
LuCalendar,
2529
LuCircleCheck,
2630
LuClock,
27-
LuUser,
31+
LuSearch,
2832
} from "react-icons/lu";
2933
import type { z } from "zod/v3";
30-
import { EmployeeAvatar, Table } from "~/components";
34+
import { EmployeeAvatar, Empty } from "~/components";
3135
import { Hidden, Submit, Users } from "~/components/Form";
3236
import { usePermissions } from "~/hooks";
3337
import { trainingAssignmentValidator } from "~/modules/people";
@@ -76,110 +80,217 @@ function StatusBadge({ status }: { status: string }) {
7680
}
7781
}
7882

79-
const StatusTable = memo(
80-
({
81-
data,
82-
currentPeriod,
83-
}: {
84-
data: TrainingAssignmentStatusItem[];
85-
currentPeriod: string | null;
86-
}) => {
87-
const permissions = usePermissions();
88-
const fetcher = useFetcher();
83+
type StatusFilter =
84+
| "All"
85+
| "Completed"
86+
| "Pending"
87+
| "Overdue"
88+
| "Not Required";
89+
90+
function AssignmentListItem({
91+
assignment,
92+
currentPeriod,
93+
disabled,
94+
isLast,
95+
}: {
96+
assignment: TrainingAssignmentStatusItem;
97+
currentPeriod: string | null;
98+
disabled: boolean;
99+
isLast: boolean;
100+
}) {
101+
const fetcher = useFetcher();
102+
const isSubmitting = fetcher.state !== "idle";
103+
const canMarkComplete =
104+
assignment.status !== "Completed" && assignment.status !== "Not Required";
89105

90-
const columns: ColumnDef<TrainingAssignmentStatusItem>[] = [
91-
{
92-
accessorKey: "employeeName",
93-
header: "Employee",
94-
cell: ({ row }) => (
95-
<HStack spacing={2}>
96-
<EmployeeAvatar employeeId={row.original.employeeId} />
97-
<span>{row.original.employeeName}</span>
98-
</HStack>
99-
),
100-
meta: {
101-
icon: <LuUser />,
102-
},
103-
},
104-
{
105-
accessorKey: "employeeStartDate",
106-
header: "Start Date",
107-
cell: ({ row }) =>
108-
row.original.employeeStartDate
109-
? new Date(row.original.employeeStartDate).toLocaleDateString()
110-
: "-",
111-
meta: {
112-
icon: <LuCalendar />,
113-
},
114-
},
115-
{
116-
accessorKey: "status",
117-
header: "Status",
118-
cell: ({ row }) => <StatusBadge status={row.original.status} />,
119-
},
120-
{
121-
accessorKey: "completedAt",
122-
header: "Completed At",
123-
cell: ({ row }) =>
124-
row.original.completedAt
125-
? new Date(row.original.completedAt).toLocaleDateString()
126-
: "-",
127-
meta: {
128-
icon: <LuClock />,
129-
},
130-
},
131-
{
132-
id: "actions",
133-
header: "",
134-
cell: ({ row }) => {
135-
if (
136-
row.original.status === "Completed" ||
137-
row.original.status === "Not Required"
138-
) {
139-
return null;
140-
}
141-
const isSubmitting = fetcher.state !== "idle";
142-
return (
106+
return (
107+
<div className={cn("p-4", !isLast && "border-b w-full")}>
108+
<div className="flex flex-1 justify-between items-center w-full">
109+
<HStack spacing={4} className="flex-1">
110+
<VStack spacing={0} className="flex-1">
111+
<EmployeeAvatar employeeId={assignment.employeeId} />
112+
{assignment.employeeStartDate && (
113+
<HStack spacing={1} className="text-xs text-muted-foreground">
114+
<LuCalendar className="size-3" />
115+
<span>
116+
Started{" "}
117+
{new Date(assignment.employeeStartDate).toLocaleDateString()}
118+
</span>
119+
</HStack>
120+
)}
121+
</VStack>
122+
</HStack>
123+
<HStack spacing={4}>
124+
<StatusBadge status={assignment.status} />
125+
{assignment.completedAt && (
126+
<span className="text-xs text-muted-foreground">
127+
<LuClock className="inline mr-1 size-3" />
128+
{new Date(assignment.completedAt).toLocaleDateString()}
129+
</span>
130+
)}
131+
{canMarkComplete && (
143132
<fetcher.Form method="post" action={path.to.markTrainingComplete}>
144133
<input
145134
type="hidden"
146135
name="trainingAssignmentId"
147-
value={row.original.trainingAssignmentId}
136+
value={assignment.trainingAssignmentId}
148137
/>
149138
<input
150139
type="hidden"
151140
name="employeeId"
152-
value={row.original.employeeId}
141+
value={assignment.employeeId}
153142
/>
154143
<input type="hidden" name="period" value={currentPeriod ?? ""} />
155144
<Button
156145
type="submit"
157146
variant="secondary"
158147
size="sm"
159-
leftIcon={<LuCircleCheck />}
160-
disabled={!permissions.can("update", "people") || isSubmitting}
148+
disabled={disabled || isSubmitting}
161149
isLoading={isSubmitting}
150+
leftIcon={<LuCircleCheck />}
162151
>
163152
Mark Complete
164153
</Button>
165154
</fetcher.Form>
166-
);
167-
},
168-
},
169-
];
155+
)}
156+
</HStack>
157+
</div>
158+
</div>
159+
);
160+
}
161+
162+
const StatusList = memo(
163+
({
164+
data,
165+
currentPeriod,
166+
}: {
167+
data: TrainingAssignmentStatusItem[];
168+
currentPeriod: string | null;
169+
}) => {
170+
const permissions = usePermissions();
171+
const [search, setSearch] = useState("");
172+
const [statusFilter, setStatusFilter] = useState<StatusFilter>("All");
173+
174+
const filteredAssignments = useMemo(() => {
175+
return data.filter((assignment) => {
176+
const matchesSearch =
177+
(search === "" ||
178+
assignment.employeeName
179+
?.toLowerCase()
180+
.includes(search.toLowerCase())) ??
181+
false;
182+
const matchesStatus =
183+
statusFilter === "All" || assignment.status === statusFilter;
184+
return matchesSearch && matchesStatus;
185+
});
186+
}, [data, search, statusFilter]);
187+
188+
const statusCounts = useMemo(() => {
189+
return data.reduce((acc, assignment) => {
190+
acc[assignment.status] = (acc[assignment.status] || 0) + 1;
191+
return acc;
192+
}, {} as Record<string, number>);
193+
}, [data]);
170194

171195
return (
172-
<Table<TrainingAssignmentStatusItem>
173-
data={data}
174-
columns={columns}
175-
count={data.length}
176-
withPagination={false}
177-
/>
196+
<VStack spacing={0} className="h-full w-full">
197+
<div className="flex flex-col gap-4 w-full">
198+
<div className="relative">
199+
<LuSearch className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
200+
<Input
201+
placeholder="Search employees..."
202+
value={search}
203+
onChange={(e) => setSearch(e.target.value)}
204+
className="pl-9"
205+
/>
206+
</div>
207+
<ToggleGroup
208+
type="single"
209+
value={statusFilter}
210+
onValueChange={(value) => {
211+
if (value) setStatusFilter(value as StatusFilter);
212+
}}
213+
className="justify-start flex-wrap"
214+
>
215+
<ToggleGroupItem
216+
className="flex gap-1.5 items-center"
217+
size="sm"
218+
value="All"
219+
>
220+
All <Count count={data.length} />
221+
</ToggleGroupItem>
222+
<ToggleGroupItem
223+
className="flex gap-1.5 items-center"
224+
size="sm"
225+
value="Completed"
226+
>
227+
<LuCircleCheck className="mr-1 size-3" />
228+
Completed <Count count={statusCounts["Completed"] || 0} />
229+
</ToggleGroupItem>
230+
<ToggleGroupItem
231+
className="flex gap-1.5 items-center"
232+
size="sm"
233+
value="Pending"
234+
>
235+
<LuClock className="mr-1 size-3" />
236+
Pending <Count count={statusCounts["Pending"] || 0} />
237+
</ToggleGroupItem>
238+
<ToggleGroupItem
239+
className="flex gap-1.5 items-center"
240+
size="sm"
241+
value="Overdue"
242+
>
243+
<LuTriangleAlert className="mr-1 size-3" />
244+
Overdue <Count count={statusCounts["Overdue"] || 0} />
245+
</ToggleGroupItem>
246+
<ToggleGroupItem
247+
className="flex gap-1.5 items-center"
248+
size="sm"
249+
value="Not Required"
250+
>
251+
Not Required <Count count={statusCounts["Not Required"] || 0} />
252+
</ToggleGroupItem>
253+
</ToggleGroup>
254+
</div>
255+
<div className="flex-1 overflow-y-auto w-full pt-4">
256+
{filteredAssignments.length > 0 ? (
257+
<div className="border rounded-lg w-full">
258+
{filteredAssignments.map((assignment, index) => (
259+
<AssignmentListItem
260+
key={`${assignment.employeeId}-${assignment.trainingAssignmentId}`}
261+
assignment={assignment}
262+
currentPeriod={currentPeriod}
263+
disabled={!permissions.can("update", "people")}
264+
isLast={index === filteredAssignments.length - 1}
265+
/>
266+
))}
267+
</div>
268+
) : (
269+
<div className="flex items-center justify-center h-full text-muted-foreground p-8">
270+
<VStack
271+
spacing={2}
272+
className="w-full items-center justify-center"
273+
>
274+
<Empty>No employees found</Empty>
275+
{search && (
276+
<Button
277+
variant="ghost"
278+
size="sm"
279+
onClick={() => setSearch("")}
280+
>
281+
Clear search
282+
</Button>
283+
)}
284+
</VStack>
285+
</div>
286+
)}
287+
</div>
288+
</VStack>
178289
);
179290
}
180291
);
181292

182-
StatusTable.displayName = "StatusTable";
293+
StatusList.displayName = "StatusList";
183294

184295
const TrainingAssignmentForm = ({
185296
initialValues,
@@ -199,6 +310,9 @@ const TrainingAssignmentForm = ({
199310

200311
const [activeTab, setActiveTab] = useState<string>("details");
201312

313+
// Drawer grows when status tab is visible
314+
const drawerSize = activeTab === "status" ? "lg" : undefined;
315+
202316
return (
203317
<ModalDrawerProvider type="drawer">
204318
<ModalDrawer
@@ -207,7 +321,7 @@ const TrainingAssignmentForm = ({
207321
if (!open) onClose?.();
208322
}}
209323
>
210-
<ModalDrawerContent>
324+
<ModalDrawerContent size={drawerSize}>
211325
<Tabs
212326
value={activeTab}
213327
onValueChange={setActiveTab}
@@ -259,7 +373,7 @@ const TrainingAssignmentForm = ({
259373
className="w-full flex flex-col gap-4"
260374
>
261375
{assignmentStatus.length > 0 ? (
262-
<StatusTable
376+
<StatusList
263377
data={assignmentStatus}
264378
currentPeriod={currentPeriod}
265379
/>

0 commit comments

Comments
 (0)