Skip to content

Commit 2da9f8b

Browse files
authored
Merge pull request #314 from GTBitsOfGood/tiffany/307-partner-and-staff-signoff
Tiffany/307 partner and staff signoff
2 parents 00da022 + 0fcde99 commit 2da9f8b

18 files changed

Lines changed: 419 additions & 139 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- AlterTable
2+
ALTER TABLE "SignOff" ADD COLUMN "partnerSignatureUrl" TEXT,
3+
ADD COLUMN "partnerSignerName" TEXT;

‎prisma/schema.prisma‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,10 @@ model SignOff {
310310
staffMemberName String
311311
partnerId Int
312312
partnerName String
313+
partnerSignerName String?
313314
date DateTime
314315
signatureUrl String?
316+
partnerSignatureUrl String?
315317
316318
createdAt DateTime @default(now())
317319

‎src/app/api/reset/route.ts‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,22 @@ async function resetDatabase() {
1717
const signOffs = await db.signOff.findMany({
1818
select: {
1919
signatureUrl: true,
20+
partnerSignatureUrl: true,
2021
},
2122
});
2223

2324
// Delete signatures from Azure Storage
2425
for (const signOff of signOffs) {
25-
if (signOff.signatureUrl) {
26+
const signatureUrls = [
27+
signOff.signatureUrl,
28+
signOff.partnerSignatureUrl,
29+
].filter(Boolean);
30+
31+
for (const signatureUrl of signatureUrls) {
2632
try {
27-
await FileService.deleteSignature(signOff.signatureUrl);
33+
await FileService.deleteSignature(signatureUrl!);
2834
} catch (error) {
29-
console.warn(
30-
`Failed to delete signature: ${signOff.signatureUrl}`,
31-
error
32-
);
35+
console.warn(`Failed to delete signature: ${signatureUrl}`, error);
3336
}
3437
}
3538
}
@@ -75,4 +78,3 @@ export async function POST() {
7578
return errorResponse(error);
7679
}
7780
}
78-

‎src/app/api/signOffs/[signOffId]/route.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ const patchSchema = z.object({
2424
staffName: z.string().optional(),
2525
partnerId: z.number().int().positive().optional(),
2626
partnerName: z.string().optional(),
27+
partnerSignerName: z.string().optional(),
2728
date: z
2829
.string()
2930
.transform((str) => new Date(str))
3031
.optional(),
3132
signatureUrl: z.string().optional(),
33+
partnerSignatureUrl: z.string().optional(),
3234
allocations: z.array(z.number().int().positive()).optional(),
35+
staffUserId: z.number().int().positive().optional(),
3336
});
3437

3538
export async function GET(
@@ -85,6 +88,7 @@ export async function PATCH(
8588
}
8689

8790
const form = await request.formData();
91+
const userId = parseInt(session.user.id!);
8892
const formObj = {
8993
staffName:
9094
form.get("staffName") !== null
@@ -98,15 +102,24 @@ export async function PATCH(
98102
form.get("partnerName") !== null
99103
? String(form.get("partnerName"))
100104
: undefined,
105+
partnerSignerName:
106+
form.get("partnerSignerName") !== null
107+
? String(form.get("partnerSignerName"))
108+
: undefined,
101109
date: form.get("date") !== null ? String(form.get("date")) : undefined,
102110
signatureUrl:
103111
form.get("signatureUrl") !== null
104112
? String(form.get("signatureUrl"))
105113
: undefined,
114+
partnerSignatureUrl:
115+
form.get("partnerSignatureUrl") !== null
116+
? String(form.get("partnerSignatureUrl"))
117+
: undefined,
106118
allocations:
107119
form.getAll("allocation").length > 0
108120
? form.getAll("allocation").map((val) => Number(val))
109121
: undefined,
122+
staffUserId: userId,
110123
};
111124
const parsed = patchSchema.safeParse(formObj);
112125

‎src/app/api/signOffs/route.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ const createSignOffSchema = z.object({
1414
partnerId: z.number().int().positive(),
1515
staffName: z.string(),
1616
partnerName: z.string(),
17+
partnerSignerName: z.string(),
1718
date: z.string().transform((str) => new Date(str)),
1819
signatureUrl: z.string(),
20+
partnerSignatureUrl: z.string(),
1921
allocations: z.array(z.number()),
2022
staffUserId: z.number().int().positive().optional(),
2123
});
@@ -85,6 +87,15 @@ export async function POST(req: NextRequest) {
8587
? String(form.get("partnerName"))
8688
: undefined,
8789
date: form.get("date") != null ? String(form.get("date")) : undefined,
90+
partnerSignerName:
91+
form.get("partnerSignerName") != null
92+
? String(form.get("partnerSignerName"))
93+
: undefined,
94+
partnerSignatureUrl:
95+
form.get("partnerSignatureUrl") != null
96+
? String(form.get("partnerSignatureUrl"))
97+
: undefined,
98+
8899
signatureUrl:
89100
form.get("signatureUrl") != null
90101
? String(form.get("signatureUrl"))

‎src/components/SignOffModal.tsx‎

Lines changed: 132 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export default function SignOffModal({
2828
}: SignOffModalProps) {
2929
const { user } = useUser();
3030
const { apiClient } = useApiClient();
31-
const signatureCanvasRef = useRef<SignatureCanvas>(null);
3231
const [isSubmitting, setIsSubmitting] = useState(false);
3332

3433
const { data: userData } = useFetch<{
@@ -37,37 +36,56 @@ export default function SignOffModal({
3736

3837
const staffName = userData?.user?.name || userData?.user?.email || "";
3938
const currentDate = new Date();
39+
const staffSignatureCanvasRef = useRef<SignatureCanvas>(null);
40+
const partnerSignatureCanvasRef = useRef<SignatureCanvas>(null);
41+
const [partnerSignerName, setPartnerSignerName] = useState("");
4042

4143
useEffect(() => {
42-
if (isOpen && signatureCanvasRef.current) {
43-
signatureCanvasRef.current.clear();
44-
}
44+
if (!isOpen) return;
45+
46+
staffSignatureCanvasRef.current?.clear();
47+
partnerSignatureCanvasRef.current?.clear();
48+
setPartnerSignerName("");
4549
}, [isOpen]);
4650

4751
const handleSubmit = async () => {
48-
if (!signatureCanvasRef.current) {
49-
toast.error("Please provide a signature");
52+
if (!partnerSignerName.trim()) {
53+
toast.error("Please enter the partner name");
5054
return;
5155
}
5256

53-
if (signatureCanvasRef.current.isEmpty()) {
54-
toast.error("Please provide a signature");
57+
if (!staffSignatureCanvasRef.current || !partnerSignatureCanvasRef.current) {
58+
toast.error("Please provide both signatures");
59+
return;
60+
}
61+
62+
if (
63+
staffSignatureCanvasRef.current.isEmpty() ||
64+
partnerSignatureCanvasRef.current.isEmpty()
65+
) {
66+
toast.error("Please provide both signatures");
5567
return;
5668
}
5769

5870
setIsSubmitting(true);
5971

6072
try {
61-
const base64Data = signatureCanvasRef.current
73+
const signatureUrl = staffSignatureCanvasRef.current
74+
.getCanvas()
75+
.toDataURL("image/png");
76+
const partnerSignatureUrl = partnerSignatureCanvasRef.current
6277
.getCanvas()
6378
.toDataURL("image/png");
6479

6580
const formData = new FormData();
6681
formData.append("partnerId", partnerId.toString());
67-
formData.append("staffName", staffName);
6882
formData.append("partnerName", partnerName);
83+
formData.append("partnerSignerName", partnerSignerName);
84+
formData.append("staffName", staffName);
6985
formData.append("date", currentDate.toISOString());
70-
formData.append("signatureUrl", base64Data);
86+
formData.append("signatureUrl", signatureUrl);
87+
formData.append("partnerSignatureUrl", partnerSignatureUrl);
88+
7189
selectedAllocationIds.forEach((id) => {
7290
formData.append("allocation", id.toString());
7391
});
@@ -87,88 +105,137 @@ export default function SignOffModal({
87105
}
88106
};
89107

90-
const handleClear = () => {
91-
if (signatureCanvasRef.current) {
92-
signatureCanvasRef.current.clear();
93-
}
94-
};
108+
const clearStaffSignature = () => staffSignatureCanvasRef.current?.clear();
109+
const clearPartnerSignature = () => partnerSignatureCanvasRef.current?.clear();
95110

96111
if (!isOpen) return null;
97112

98113
return (
99-
<div className="fixed inset-0 flex items-center justify-center bg-black/50 z-50">
100-
<div className="relative bg-white rounded-2xl p-6 w-full max-w-lg shadow-lg text-[#22070B]">
114+
<div className="fixed inset-0 z-50 overflow-y-auto bg-black/50 px-4 py-6">
115+
<div className="relative mx-auto flex max-h-[calc(100dvh-3rem)] w-full max-w-lg flex-col overflow-hidden rounded-2xl bg-white text-gray-primary shadow-lg">
101116
<button
102117
onClick={onClose}
103118
aria-label="Close"
104119
className="absolute top-4 right-4 text-2xl text-gray-500 hover:text-gray-700"
105120
>
106121
<X className="size-6" />
107122
</button>
108-
<h2 className="text-[20px] font-semibold leading-[28px] mb-4">
109-
Sign Off Items
110-
</h2>
111-
112-
<div className="space-y-4">
113-
<div>
114-
<label className="block text-sm font-medium text-gray-700 mb-1">
115-
Staff Name
116-
</label>
117-
<input
118-
type="text"
119-
value={staffName}
120-
readOnly
121-
className="w-full rounded-[4px] border border-[rgba(34,7,11,0.05)] bg-[rgba(34,7,11,0.05)] text-[16px] p-3 cursor-not-allowed"
122-
/>
123-
</div>
123+
<div className="shrink-0 border-b border-gray-200 px-6 py-5 pr-14">
124+
<h2 className="text-xl font-semibold leading-7">
125+
Sign Off Items
126+
</h2>
127+
</div>
128+
129+
<div className="flex-1 space-y-6 overflow-y-auto px-6 py-5">
130+
<div className="space-y-4">
131+
<div>
132+
<label className="mb-1 block text-sm font-medium text-gray-700">
133+
Staff Name
134+
</label>
135+
<input
136+
type="text"
137+
value={staffName}
138+
readOnly
139+
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed"
140+
/>
141+
</div>
142+
143+
<div>
144+
<label className="mb-1 block text-sm font-medium text-gray-700">
145+
Date and Time
146+
</label>
147+
<input
148+
type="text"
149+
value={format(currentDate, "MMM d, yyyy 'at' h:mm a")}
150+
readOnly
151+
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed"
152+
/>
153+
</div>
124154

125-
<div>
126-
<label className="block text-sm font-medium text-gray-700 mb-1">
127-
Date and Time
128-
</label>
129-
<input
130-
type="text"
131-
value={format(currentDate, "MMM d, yyyy 'at' h:mm a")}
132-
readOnly
133-
className="w-full rounded-[4px] border border-[rgba(34,7,11,0.05)] bg-[rgba(34,7,11,0.05)] text-[16px] p-3 cursor-not-allowed"
134-
/>
155+
<div>
156+
<label className="mb-1 block text-sm font-medium text-gray-700">
157+
Signature
158+
</label>
159+
<div className="rounded-sm border-2 border-gray-primary/20 bg-white">
160+
<SignatureCanvas
161+
ref={staffSignatureCanvasRef}
162+
canvasProps={{
163+
className: "h-40 w-full sm:h-48",
164+
}}
165+
backgroundColor="white"
166+
/>
167+
</div>
168+
<button
169+
type="button"
170+
onClick={clearStaffSignature}
171+
className="mt-2 text-sm text-blue-primary hover:text-blue-primary/80"
172+
>
173+
Clear Signature
174+
</button>
175+
</div>
135176
</div>
136177

137-
<div>
138-
<label className="block text-sm font-medium text-gray-700 mb-1">
139-
Signature
140-
</label>
141-
<div className="border-2 border-gray-300 rounded-[4px] bg-white">
142-
<SignatureCanvas
143-
ref={signatureCanvasRef}
144-
canvasProps={{
145-
className: "w-full h-48",
146-
}}
147-
backgroundColor="white"
178+
<div className="space-y-4 border-t border-gray-200 pt-6">
179+
<div>
180+
<label className="mb-1 block text-sm font-medium text-gray-700">
181+
Partner Name
182+
</label>
183+
<input
184+
type="text"
185+
value={partnerSignerName}
186+
onChange={(e) => setPartnerSignerName(e.target.value)}
187+
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary"
188+
/>
189+
</div>
190+
191+
<div>
192+
<label className="mb-1 block text-sm font-medium text-gray-700">
193+
Date and Time
194+
</label>
195+
<input
196+
type="text"
197+
value={format(currentDate, "MMM d, yyyy 'at' h:mm a")}
198+
readOnly
199+
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed"
148200
/>
149201
</div>
150-
<button
151-
type="button"
152-
onClick={handleClear}
153-
className="mt-2 text-sm text-blue-primary hover:text-blue-primary/80"
154-
>
155-
Clear Signature
156-
</button>
202+
203+
<div>
204+
<label className="mb-1 block text-sm font-medium text-gray-700">
205+
Signature
206+
</label>
207+
<div className="rounded-sm border-2 border-gray-primary/20 bg-white">
208+
<SignatureCanvas
209+
ref={partnerSignatureCanvasRef}
210+
canvasProps={{
211+
className: "h-40 w-full sm:h-48",
212+
}}
213+
backgroundColor="white"
214+
/>
215+
</div>
216+
<button
217+
type="button"
218+
onClick={clearPartnerSignature}
219+
className="mt-2 text-sm text-blue-primary hover:text-blue-primary/80"
220+
>
221+
Clear Signature
222+
</button>
223+
</div>
157224
</div>
158225
</div>
159226

160-
<div className="mt-6 flex justify-end gap-4">
227+
<div className="mt-auto flex shrink-0 justify-end gap-4 border-t border-gray-200 bg-white px-6 py-4">
161228
<button
162229
onClick={onClose}
163230
disabled={isSubmitting}
164-
className="bg-white hover:bg-gray-100 text-[#EF3340] border-2 border-[#EF3340] font-semibold py-2 px-4 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
231+
className="rounded-md border-2 border-red-primary bg-white px-4 py-2 font-semibold text-red-primary hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
165232
>
166233
Cancel
167234
</button>
168235
<button
169236
onClick={handleSubmit}
170237
disabled={isSubmitting}
171-
className="bg-[#EF3340] hover:bg-[#a32027] text-white font-semibold py-2 px-4 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
238+
className="rounded-md bg-red-primary px-4 py-2 font-semibold text-white hover:bg-red-primary/80 disabled:cursor-not-allowed disabled:opacity-50"
172239
>
173240
{isSubmitting ? "Submitting..." : "Submit"}
174241
</button>

0 commit comments

Comments
 (0)