Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "SignOff" ADD COLUMN "partnerSignatureUrl" TEXT,
ADD COLUMN "partnerSignerName" TEXT;
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,10 @@ model SignOff {
staffMemberName String
partnerId Int
partnerName String
partnerSignerName String?
date DateTime
signatureUrl String?
partnerSignatureUrl String?

createdAt DateTime @default(now())

Expand Down
16 changes: 9 additions & 7 deletions src/app/api/reset/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@ async function resetDatabase() {
const signOffs = await db.signOff.findMany({
select: {
signatureUrl: true,
partnerSignatureUrl: true,
},
});

// Delete signatures from Azure Storage
for (const signOff of signOffs) {
if (signOff.signatureUrl) {
const signatureUrls = [
signOff.signatureUrl,
signOff.partnerSignatureUrl,
].filter(Boolean);

for (const signatureUrl of signatureUrls) {
try {
await FileService.deleteSignature(signOff.signatureUrl);
await FileService.deleteSignature(signatureUrl!);
} catch (error) {
console.warn(
`Failed to delete signature: ${signOff.signatureUrl}`,
error
);
console.warn(`Failed to delete signature: ${signatureUrl}`, error);
}
}
}
Expand Down Expand Up @@ -75,4 +78,3 @@ export async function POST() {
return errorResponse(error);
}
}

13 changes: 13 additions & 0 deletions src/app/api/signOffs/[signOffId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ const patchSchema = z.object({
staffName: z.string().optional(),
partnerId: z.number().int().positive().optional(),
partnerName: z.string().optional(),
partnerSignerName: z.string().optional(),
date: z
.string()
.transform((str) => new Date(str))
.optional(),
signatureUrl: z.string().optional(),
partnerSignatureUrl: z.string().optional(),
allocations: z.array(z.number().int().positive()).optional(),
staffUserId: z.number().int().positive().optional(),
});

export async function GET(
Expand Down Expand Up @@ -85,6 +88,7 @@ export async function PATCH(
}

const form = await request.formData();
const userId = parseInt(session.user.id!);
const formObj = {
staffName:
form.get("staffName") !== null
Expand All @@ -98,15 +102,24 @@ export async function PATCH(
form.get("partnerName") !== null
? String(form.get("partnerName"))
: undefined,
partnerSignerName:
form.get("partnerSignerName") !== null
? String(form.get("partnerSignerName"))
: undefined,
date: form.get("date") !== null ? String(form.get("date")) : undefined,
signatureUrl:
form.get("signatureUrl") !== null
? String(form.get("signatureUrl"))
: undefined,
partnerSignatureUrl:
form.get("partnerSignatureUrl") !== null
? String(form.get("partnerSignatureUrl"))
: undefined,
allocations:
form.getAll("allocation").length > 0
? form.getAll("allocation").map((val) => Number(val))
: undefined,
staffUserId: userId,
};
const parsed = patchSchema.safeParse(formObj);

Expand Down
11 changes: 11 additions & 0 deletions src/app/api/signOffs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ const createSignOffSchema = z.object({
partnerId: z.number().int().positive(),
staffName: z.string(),
partnerName: z.string(),
partnerSignerName: z.string(),
date: z.string().transform((str) => new Date(str)),
signatureUrl: z.string(),
partnerSignatureUrl: z.string(),
allocations: z.array(z.number()),
staffUserId: z.number().int().positive().optional(),
});
Expand Down Expand Up @@ -85,6 +87,15 @@ export async function POST(req: NextRequest) {
? String(form.get("partnerName"))
: undefined,
date: form.get("date") != null ? String(form.get("date")) : undefined,
partnerSignerName:
form.get("partnerSignerName") != null
? String(form.get("partnerSignerName"))
: undefined,
partnerSignatureUrl:
form.get("partnerSignatureUrl") != null
? String(form.get("partnerSignatureUrl"))
: undefined,

signatureUrl:
form.get("signatureUrl") != null
? String(form.get("signatureUrl"))
Expand Down
197 changes: 132 additions & 65 deletions src/components/SignOffModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export default function SignOffModal({
}: SignOffModalProps) {
const { user } = useUser();
const { apiClient } = useApiClient();
const signatureCanvasRef = useRef<SignatureCanvas>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

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

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

useEffect(() => {
if (isOpen && signatureCanvasRef.current) {
signatureCanvasRef.current.clear();
}
if (!isOpen) return;

staffSignatureCanvasRef.current?.clear();
partnerSignatureCanvasRef.current?.clear();
setPartnerSignerName("");
}, [isOpen]);

const handleSubmit = async () => {
if (!signatureCanvasRef.current) {
toast.error("Please provide a signature");
if (!partnerSignerName.trim()) {
toast.error("Please enter the partner name");
return;
}

if (signatureCanvasRef.current.isEmpty()) {
toast.error("Please provide a signature");
if (!staffSignatureCanvasRef.current || !partnerSignatureCanvasRef.current) {
toast.error("Please provide both signatures");
return;
}

if (
staffSignatureCanvasRef.current.isEmpty() ||
partnerSignatureCanvasRef.current.isEmpty()
) {
toast.error("Please provide both signatures");
return;
}

setIsSubmitting(true);

try {
const base64Data = signatureCanvasRef.current
const signatureUrl = staffSignatureCanvasRef.current
.getCanvas()
.toDataURL("image/png");
const partnerSignatureUrl = partnerSignatureCanvasRef.current
.getCanvas()
.toDataURL("image/png");

const formData = new FormData();
formData.append("partnerId", partnerId.toString());
formData.append("staffName", staffName);
formData.append("partnerName", partnerName);
formData.append("partnerSignerName", partnerSignerName);
formData.append("staffName", staffName);
formData.append("date", currentDate.toISOString());
formData.append("signatureUrl", base64Data);
formData.append("signatureUrl", signatureUrl);
formData.append("partnerSignatureUrl", partnerSignatureUrl);

selectedAllocationIds.forEach((id) => {
formData.append("allocation", id.toString());
});
Expand All @@ -87,88 +105,137 @@ export default function SignOffModal({
}
};

const handleClear = () => {
if (signatureCanvasRef.current) {
signatureCanvasRef.current.clear();
}
};
const clearStaffSignature = () => staffSignatureCanvasRef.current?.clear();
const clearPartnerSignature = () => partnerSignatureCanvasRef.current?.clear();

if (!isOpen) return null;

return (
<div className="fixed inset-0 flex items-center justify-center bg-black/50 z-50">
<div className="relative bg-white rounded-2xl p-6 w-full max-w-lg shadow-lg text-[#22070B]">
<div className="fixed inset-0 z-50 overflow-y-auto bg-black/50 px-4 py-6">
<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">
<button
onClick={onClose}
aria-label="Close"
className="absolute top-4 right-4 text-2xl text-gray-500 hover:text-gray-700"
>
<X className="size-6" />
</button>
<h2 className="text-[20px] font-semibold leading-[28px] mb-4">
Sign Off Items
</h2>

<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Staff Name
</label>
<input
type="text"
value={staffName}
readOnly
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"
/>
</div>
<div className="shrink-0 border-b border-gray-200 px-6 py-5 pr-14">
<h2 className="text-xl font-semibold leading-7">
Sign Off Items
</h2>
</div>

<div className="flex-1 space-y-6 overflow-y-auto px-6 py-5">
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Staff Name
</label>
<input
type="text"
value={staffName}
readOnly
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed"
/>
</div>

<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Date and Time
</label>
<input
type="text"
value={format(currentDate, "MMM d, yyyy 'at' h:mm a")}
readOnly
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Date and Time
</label>
<input
type="text"
value={format(currentDate, "MMM d, yyyy 'at' h:mm a")}
readOnly
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"
/>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Signature
</label>
<div className="rounded-sm border-2 border-gray-primary/20 bg-white">
<SignatureCanvas
ref={staffSignatureCanvasRef}
canvasProps={{
className: "h-40 w-full sm:h-48",
}}
backgroundColor="white"
/>
</div>
<button
type="button"
onClick={clearStaffSignature}
className="mt-2 text-sm text-blue-primary hover:text-blue-primary/80"
>
Clear Signature
</button>
</div>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Signature
</label>
<div className="border-2 border-gray-300 rounded-[4px] bg-white">
<SignatureCanvas
ref={signatureCanvasRef}
canvasProps={{
className: "w-full h-48",
}}
backgroundColor="white"
<div className="space-y-4 border-t border-gray-200 pt-6">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Partner Name
</label>
<input
type="text"
value={partnerSignerName}
onChange={(e) => setPartnerSignerName(e.target.value)}
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary"
/>
</div>

<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Date and Time
</label>
<input
type="text"
value={format(currentDate, "MMM d, yyyy 'at' h:mm a")}
readOnly
className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed"
/>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
</div>
<button
type="button"
onClick={handleClear}
className="mt-2 text-sm text-blue-primary hover:text-blue-primary/80"
>
Clear Signature
</button>

<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Signature
</label>
<div className="rounded-sm border-2 border-gray-primary/20 bg-white">
<SignatureCanvas
ref={partnerSignatureCanvasRef}
canvasProps={{
className: "h-40 w-full sm:h-48",
}}
backgroundColor="white"
/>
</div>
<button
type="button"
onClick={clearPartnerSignature}
className="mt-2 text-sm text-blue-primary hover:text-blue-primary/80"
>
Clear Signature
</button>
</div>
</div>
</div>

<div className="mt-6 flex justify-end gap-4">
<div className="mt-auto flex shrink-0 justify-end gap-4 border-t border-gray-200 bg-white px-6 py-4">
<button
onClick={onClose}
disabled={isSubmitting}
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"
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"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={isSubmitting}
className="bg-[#EF3340] hover:bg-[#a32027] text-white font-semibold py-2 px-4 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
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"
>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
Expand Down
Loading
Loading