Skip to content

Commit 2c88977

Browse files
authored
feat: stub-out the initial ui for accepting driver signatures (#403)
* feat: initial ui for displaying the drivers who have submitted their signature * refactor: cleanup the styling a bit * chore: show different icons * chore: remove comment * refactor: move the summary tab state into the URL * fix: type errors * fix: remove tab value
1 parent 188bff7 commit 2c88977

File tree

11 files changed

+223
-73
lines changed

11 files changed

+223
-73
lines changed

src/components/ui/icons.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
SearchIcon,
5353
SettingsIcon,
5454
SheetIcon,
55+
SignatureIcon,
5556
SortAscIcon,
5657
SortDescIcon,
5758
SunIcon,
@@ -115,6 +116,7 @@ export const icons = {
115116
Clear: BanIcon,
116117
Copy: CopyIcon,
117118
DollarSign: DollarSignIcon,
119+
Signature: SignatureIcon,
118120
FileSignature: FileSignatureIcon,
119121
Files: FilesIcon,
120122
Folder: FolderIcon,

src/lib/config/features.ts

+10
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,25 @@ const incompleteApplicationSettingsTabsFeatureFlag: SwitchFeatureFlag = {
4444
input_type: "switch",
4545
default_value: false,
4646
} as const;
47+
const incompleteAgreementCustomerSignatureFeatureFlag: SwitchFeatureFlag = {
48+
id: "incomplete_agreement.customer.signature",
49+
name: "Accept digital signatures from the drivers",
50+
description:
51+
"Toggle this feature to enable the digital signature acceptance from the drivers.",
52+
input_type: "switch",
53+
default_value: false,
54+
} as const;
4755
// Features END
4856

4957
const featureFlags: FeatureFlags = [
5058
incompleteSettingsNavigationFeatureFlag,
5159
incompleteApplicationSettingsTabsFeatureFlag,
60+
incompleteAgreementCustomerSignatureFeatureFlag,
5261
] as const;
5362

5463
export {
5564
featureFlags, // the array of all the feature flags
5665
incompleteSettingsNavigationFeatureFlag,
5766
incompleteApplicationSettingsTabsFeatureFlag,
67+
incompleteAgreementCustomerSignatureFeatureFlag,
5868
};

src/lib/schemas/agreement/agreementData.ts

+50-50
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,55 @@ export const AgreementMiscChargeItem = z.object({
6464
monthlyQuantity: z.preprocess((val) => (val === null ? 0 : val), z.number()),
6565
});
6666

67+
const AgreementDataDriver = z.object({
68+
driverId: z.number(),
69+
agreementId: z.number().nullable(),
70+
customerId: z.number().nullable(),
71+
driverName: z.string().nullable(),
72+
firstName: z.string().nullable(),
73+
lastName: z.string().nullable(),
74+
dateOfBirth: z.string().nullable(),
75+
driverType: z.number(),
76+
createdBy: z.coerce.string().nullable(),
77+
licenseNumber: z.string().nullable(),
78+
licenseCategory: z.string().nullable(),
79+
licenseExpiryDate: z.string().nullable(),
80+
email: z.string().nullable(),
81+
licenseIssueDate: z.string().nullable(),
82+
licenseIssueState: z.string().nullable(),
83+
hPhone: z.string().nullable(),
84+
bPhone: z.string().nullable(),
85+
cPhone: z.string().nullable(),
86+
address: z.string().nullable(),
87+
city: z.string().nullable(),
88+
stateId: z.number().nullable(),
89+
zipCode: z.string().nullable(),
90+
countryId: z.number().nullable(),
91+
signatureName: z.string().nullable(),
92+
signatureImageUrl: z.string().nullable(),
93+
reservationId: z.number().nullable(),
94+
insuranceCompany: z.string().nullable(),
95+
checkForDelete: z.number().nullable(),
96+
dateofBirth: z.string().nullable(),
97+
dateofBirthStr: z.string().nullable(),
98+
createdDate: z.string().nullable(),
99+
updatedDate: z.string().nullable(),
100+
updateBy: z.coerce.string().nullable(),
101+
driverLicenseNumber: z.string().nullable(),
102+
driverLicenseCategory: z.string().nullable(),
103+
driverLicenseExpiryDate: z.string().nullable(),
104+
isDelete: z.boolean(),
105+
isFromCustomer: z.boolean(),
106+
phone: z.string().nullable(),
107+
signatureImageUrlString: z.string().nullable(),
108+
startDate: z.string().nullable(),
109+
endDate: z.string().nullable(),
110+
referenceType: z.string().nullable(),
111+
referenceId: z.coerce.string().nullable(),
112+
countryName: z.string().nullable(),
113+
signatureDate: z.string().nullable(),
114+
});
115+
67116
export const AgreementDataSchema = z.object({
68117
agreementId: z.number(),
69118
agreementNumber: z.string().nullable(),
@@ -290,56 +339,7 @@ export const AgreementDataSchema = z.object({
290339
})
291340
),
292341

293-
driverList: z.array(
294-
z.object({
295-
driverId: z.number(),
296-
agreementId: z.number().nullable(),
297-
customerId: z.number().nullable(),
298-
driverName: z.string().nullable(),
299-
firstName: z.string().nullable(),
300-
lastName: z.string().nullable(),
301-
dateOfBirth: z.string().nullable(),
302-
driverType: z.number(),
303-
createdBy: z.coerce.string().nullable(),
304-
licenseNumber: z.string().nullable(),
305-
licenseCategory: z.string().nullable(),
306-
licenseExpiryDate: z.string().nullable(),
307-
email: z.string().nullable(),
308-
licenseIssueDate: z.string().nullable(),
309-
licenseIssueState: z.string().nullable(),
310-
hPhone: z.string().nullable(),
311-
bPhone: z.string().nullable(),
312-
cPhone: z.string().nullable(),
313-
address: z.string().nullable(),
314-
city: z.string().nullable(),
315-
stateId: z.number().nullable(),
316-
zipCode: z.string().nullable(),
317-
countryId: z.number().nullable(),
318-
signatureName: z.string().nullable(),
319-
signatureImageUrl: z.string().nullable(),
320-
reservationId: z.number().nullable(),
321-
insuranceCompany: z.string().nullable(),
322-
checkForDelete: z.number().nullable(),
323-
dateofBirth: z.string().nullable(),
324-
dateofBirthStr: z.string().nullable(),
325-
createdDate: z.string().nullable(),
326-
updatedDate: z.string().nullable(),
327-
updateBy: z.coerce.string().nullable(),
328-
driverLicenseNumber: z.string().nullable(),
329-
driverLicenseCategory: z.string().nullable(),
330-
driverLicenseExpiryDate: z.string().nullable(),
331-
isDelete: z.boolean(),
332-
isFromCustomer: z.boolean(),
333-
phone: z.string().nullable(),
334-
signatureImageUrlString: z.string().nullable(),
335-
startDate: z.string().nullable(),
336-
endDate: z.string().nullable(),
337-
referenceType: z.string().nullable(),
338-
referenceId: z.coerce.string().nullable(),
339-
countryName: z.string().nullable(),
340-
signatureDate: z.string().nullable(),
341-
})
342-
),
342+
driverList: z.array(AgreementDataDriver),
343343

344344
customerEmail: z.string().nullable(),
345345

src/routes/-components/auth/header/command-menu.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,6 @@ export const CommandMenu = () => {
365365
navigate({
366366
to: "/agreements/$agreementId/summary",
367367
params: { agreementId: item.referenceId },
368-
search: () => ({ tab: "summary" }),
369368
})
370369
);
371370
}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Button } from "@/components/ui/button";
2+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3+
import { icons } from "@/components/ui/icons";
4+
5+
import type { AgreementDataParsed } from "@/lib/schemas/agreement";
6+
7+
import { cn } from "@/lib/utils";
8+
9+
export default function SummarySignatureCard(props: {
10+
agreement: AgreementDataParsed;
11+
}) {
12+
const agreementId = props.agreement.agreementId.toString();
13+
const drivers = props.agreement.driverList;
14+
15+
return (
16+
<Card>
17+
<CardHeader className="flex flex-row items-center justify-between border-b px-5 py-4">
18+
<CardTitle className="text-base font-medium sm:text-lg">
19+
Driver {drivers.length > 1 ? "signatures" : "signature"}
20+
</CardTitle>
21+
<span>
22+
<icons.Signature className="h-5 w-5" />
23+
</span>
24+
</CardHeader>
25+
<CardContent className="p-0">
26+
<ul className="divide-y">
27+
{drivers.map((d) => (
28+
<Driver
29+
key={`${agreementId}_${d.driverId || "no-driver-id"}`}
30+
driver={d}
31+
agreementId={agreementId}
32+
isPrimary={d.customerId === props.agreement.customerId}
33+
/>
34+
))}
35+
</ul>
36+
</CardContent>
37+
</Card>
38+
);
39+
}
40+
41+
function Driver(props: {
42+
driver: AgreementDataParsed["driverList"][number];
43+
agreementId: string;
44+
isPrimary: boolean;
45+
}) {
46+
const isSigned = !!props.driver.signatureDate;
47+
48+
return (
49+
<li className="flex flex-row items-center justify-between gap-4 p-5 xl:gap-2">
50+
<div className="grid min-w-0 justify-start text-sm">
51+
<p className="inline-flex items-center gap-1 truncate font-semibold leading-6 text-foreground">
52+
{props.isPrimary ? (
53+
<icons.User className="size-3" />
54+
) : (
55+
<icons.Users className="size-3" />
56+
)}
57+
<span>{props.driver.driverName}</span>
58+
</p>
59+
<p className="mt-1 leading-5 text-muted-foreground">
60+
<icons.Signature
61+
className={cn(
62+
"mr-1 inline size-3",
63+
isSigned ? "text-green-500" : "text-destructive"
64+
)}
65+
/>
66+
{isSigned ? (
67+
<>
68+
<span className="hidden 2xl:inline">Signed on:</span>
69+
<span>&nbsp;{props.driver.signatureDate ?? "..."}</span>
70+
</>
71+
) : (
72+
<span>Not signed</span>
73+
)}
74+
</p>
75+
</div>
76+
<div className="flex items-center gap-2">
77+
<Button variant="outline" size="sm" className="gap-1 bg-transparent">
78+
<icons.Signature className="size-3" />
79+
<span className="hidden text-xs xl:inline">
80+
{isSigned ? "Redo" : "Sign"}
81+
</span>
82+
</Button>
83+
{isSigned ? (
84+
<Button variant="ghost" size="icon" className="h-9">
85+
<icons.More className="size-3" />
86+
</Button>
87+
) : null}
88+
</div>
89+
</li>
90+
);
91+
}

src/routes/_auth/(agreements)/agreements.$agreementId._details.summary.tsx

+66-18
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,59 @@
11
import * as React from "react";
22
import { useSuspenseQuery } from "@tanstack/react-query";
33
import { createFileRoute } from "@tanstack/react-router";
4+
import { z } from "zod";
45

56
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
67

8+
import { useFeature } from "@/lib/hooks/useFeature";
9+
import { useLocalStorage } from "@/lib/hooks/useLocalStorage";
10+
711
import CustomerInformation from "@/routes/_auth/-modules/information-block/customer-information";
812
import RentalInformation from "@/routes/_auth/-modules/information-block/rental-information";
913
import VehicleInformation from "@/routes/_auth/-modules/information-block/vehicle-information";
1014
import { RentalSummary } from "@/routes/_auth/-modules/summary/rental-summary";
1115
import { Container } from "@/routes/-components/container";
1216

17+
import { incompleteAgreementCustomerSignatureFeatureFlag } from "@/lib/config/features";
18+
1319
export const Route = createFileRoute(
1420
"/_auth/(agreements)/agreements/$agreementId/_details/summary"
1521
)({
1622
component: Component,
23+
validateSearch: z.object({
24+
summary_tab: z.string().optional(),
25+
}),
1726
});
27+
const SummarySignatureCard = React.lazy(
28+
() => import("@/routes/_auth/(agreements)/-components/summary-signature-card")
29+
);
1830

1931
function Component() {
32+
const navigate = Route.useNavigate();
33+
34+
const currentTab = Route.useSearch({
35+
select: (s) => {
36+
if (s.summary_tab && ["vehicle", "rental"].includes(s.summary_tab)) {
37+
return s.summary_tab;
38+
}
39+
return "vehicle";
40+
},
41+
});
2042
const { viewAgreementOptions, viewAgreementSummaryOptions } =
2143
Route.useRouteContext();
2244

2345
const canViewCustomerInformation = true;
2446
const canViewRentalInformation = true;
2547

48+
const [_, canViewDigitalSignaturePad] = useFeature(
49+
"DIGITAL_SIGNATURE_PAD",
50+
null
51+
);
52+
const [showIncompleteAgreementSignature] = useLocalStorage(
53+
incompleteAgreementCustomerSignatureFeatureFlag.id,
54+
incompleteAgreementCustomerSignatureFeatureFlag.default_value
55+
);
56+
2657
const agreementQuery = useSuspenseQuery(viewAgreementOptions);
2758
const agreement =
2859
agreementQuery.data?.status === 200 ? agreementQuery.data.body : null;
@@ -32,8 +63,6 @@ function Component() {
3263
const summaryData =
3364
summaryQuery.data?.status === 200 ? summaryQuery.data?.body : undefined;
3465

35-
const [currentTab, setCurrentTab] = React.useState("vehicle");
36-
3766
const tabsConfig = React.useMemo(() => {
3867
const tabs: { id: string; label: string; component: React.ReactNode }[] =
3968
[];
@@ -109,6 +138,13 @@ function Component() {
109138
isCheckedIn,
110139
]);
111140

141+
const setCurrentTab = (name: string) => {
142+
navigate({
143+
search: (s) => ({ ...s, summary_tab: name }),
144+
resetScroll: false,
145+
});
146+
};
147+
112148
return (
113149
<Container as="div">
114150
<div className="mb-6 grid max-w-full grid-cols-1 gap-4 px-2 sm:px-4 lg:grid-cols-12">
@@ -139,28 +175,40 @@ function Component() {
139175
/>
140176
)}
141177

142-
<Tabs value={currentTab} onValueChange={setCurrentTab}>
143-
<TabsList className="w-full sm:max-w-max">
178+
{tabsConfig.length >= 1 ? (
179+
<Tabs value={currentTab} onValueChange={setCurrentTab}>
180+
<TabsList className="w-full sm:max-w-max">
181+
{tabsConfig.map((tab, idx) => (
182+
<TabsTrigger
183+
key={`tab-summary-trigger-${idx}`}
184+
value={tab.id}
185+
>
186+
{tab.label}
187+
</TabsTrigger>
188+
))}
189+
</TabsList>
144190
{tabsConfig.map((tab, idx) => (
145-
<TabsTrigger key={`tab-summary-trigger-${idx}`} value={tab.id}>
146-
{tab.label}
147-
</TabsTrigger>
191+
<TabsContent
192+
key={`tab-summary-content-${idx}`}
193+
value={tab.id}
194+
className="min-h-[180px]"
195+
>
196+
{tab.component}
197+
</TabsContent>
148198
))}
149-
</TabsList>
150-
{tabsConfig.map((tab, idx) => (
151-
<TabsContent
152-
key={`tab-summary-content-${idx}`}
153-
value={tab.id}
154-
className="min-h-[180px]"
155-
>
156-
{tab.component}
157-
</TabsContent>
158-
))}
159-
</Tabs>
199+
</Tabs>
200+
) : null}
160201
</div>
161202

162203
<div className="flex flex-col gap-4 lg:col-span-4">
163204
<RentalSummary module="agreements" summaryData={summaryData} />
205+
<React.Suspense fallback={null}>
206+
{showIncompleteAgreementSignature &&
207+
canViewDigitalSignaturePad &&
208+
agreement ? (
209+
<SummarySignatureCard agreement={agreement} />
210+
) : null}
211+
</React.Suspense>
164212
</div>
165213
</div>
166214
</Container>

0 commit comments

Comments
 (0)