From 950bcc0912cb2c3646f648002b80dda96448e553 Mon Sep 17 00:00:00 2001 From: Marshal Date: Sun, 2 Aug 2026 20:29:09 +0000 Subject: [PATCH] configer the stamp --- .../signatures/dto/save-signature.dto.ts | 8 +- .../modules/signatures/signatures.service.ts | 23 ++- .../components/profile/MySignatureCard.tsx | 174 ++++++++++++------ .../src/services/signatures.service.ts | 3 +- .../components/profile/MySignatureCard.tsx | 168 +++++++++++------ .../portal/src/pages/accounts/SignupPage.tsx | 25 +++ .../portal/src/services/signatures.service.ts | 3 +- 7 files changed, 281 insertions(+), 123 deletions(-) diff --git a/apps/edr-freight-api/src/modules/signatures/dto/save-signature.dto.ts b/apps/edr-freight-api/src/modules/signatures/dto/save-signature.dto.ts index a1f245e23..6d7898b53 100644 --- a/apps/edr-freight-api/src/modules/signatures/dto/save-signature.dto.ts +++ b/apps/edr-freight-api/src/modules/signatures/dto/save-signature.dto.ts @@ -7,12 +7,14 @@ export class SaveSignatureDto { @MinLength(1) signerDisplayName!: string; - @ApiProperty({ - description: 'PNG signature image as base64 (with or without data URL prefix)', + @ApiPropertyOptional({ + description: + 'PNG signature image as base64 (with or without data URL prefix). Omit to keep the existing saved signature (stamp-only update).', }) + @IsOptional() @IsString() @MinLength(20) - signatureImageBase64!: string; + signatureImageBase64?: string; @ApiPropertyOptional({ description: diff --git a/apps/edr-freight-api/src/modules/signatures/signatures.service.ts b/apps/edr-freight-api/src/modules/signatures/signatures.service.ts index 836888dab..d346457a0 100644 --- a/apps/edr-freight-api/src/modules/signatures/signatures.service.ts +++ b/apps/edr-freight-api/src/modules/signatures/signatures.service.ts @@ -12,7 +12,8 @@ import { SavedSignatureDto } from './dto/save-signature.dto'; export interface UpsertSignatureInput { userId: string; signerDisplayName: string; - signatureImageBase64: string; + /** Optional; omitted = keep the existing saved signature (stamp-only update). */ + signatureImageBase64?: string; /** Optional company stamp/seal; omitted = keep the existing saved stamp. */ stampImageBase64?: string; } @@ -46,12 +47,14 @@ export class SignaturesService { const previousFileId = existing?.signatureFileId ?? null; const previousStampFileId = existing?.stampFileId ?? null; - const fileRecord = await this.filesService.upload({ - resourceId: input.userId, - resource: 'saved_signatures', - code: 'signature', - file: this.toUploadFile('signature', input.userId, input.signatureImageBase64), - }); + const fileRecord = input.signatureImageBase64 + ? await this.filesService.upload({ + resourceId: input.userId, + resource: 'saved_signatures', + code: 'signature', + file: this.toUploadFile('signature', input.userId, input.signatureImageBase64), + }) + : null; const stampRecord = input.stampImageBase64 ? await this.filesService.upload({ @@ -65,13 +68,13 @@ export class SignaturesService { const saved = await this.signaturesRepository.upsert({ userId: input.userId, signerDisplayName: input.signerDisplayName, - signatureFileId: fileRecord.id, - // Omitted stamp keeps whatever was saved before. + // Omitted image keeps whatever was saved before. + ...(fileRecord ? { signatureFileId: fileRecord.id } : {}), ...(stampRecord ? { stampFileId: stampRecord.id } : {}), }); const staleIds = [ - previousFileId !== fileRecord.id ? previousFileId : null, + fileRecord && previousFileId !== fileRecord.id ? previousFileId : null, stampRecord && previousStampFileId !== stampRecord.id ? previousStampFileId : null, diff --git a/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx index ca9effd27..0a70b3aec 100644 --- a/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { FileSignature, Loader2 } from "lucide-react"; +import { FileSignature, Loader2, Stamp } from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import toast from "react-hot-toast"; @@ -27,9 +27,9 @@ import { } from "@edr/ui-common"; /** - * Lets the signed-in user view and update the reusable signature stored on - * their profile. The same signature is offered for approval when signing a - * booking contract. + * Lets the signed-in user view and update the reusable signature and company + * stamp stored on their profile — managed independently of each other. Both + * are offered when signing a booking contract. */ export function MySignatureCard() { const { user } = useAuth(); @@ -38,42 +38,63 @@ export function MySignatureCard() { ); const saveMutation = useMutation(api.signatures.save.mutationOptions()); - const [open, setOpen] = useState(false); + const [signatureOpen, setSignatureOpen] = useState(false); + const [stampOpen, setStampOpen] = useState(false); const [signerName, setSignerName] = useState(""); const [signatureData, setSignatureData] = useState(null); const [stampData, setStampData] = useState(null); const defaultName = user?.name?.en || user?.username || user?.email || ""; + const savedName = saved?.signerDisplayName ?? defaultName; - const openDialog = () => { - setSignerName(saved?.signerDisplayName ?? defaultName); + const openSignatureDialog = () => { + setSignerName(savedName); setSignatureData(null); - setStampData(saved?.stampImageUrl ?? null); - setOpen(true); + setSignatureOpen(true); }; - const save = () => { + const saveSignature = () => { if (!signatureData || !signerName.trim()) return; saveMutation.mutate( { signerDisplayName: signerName.trim(), signatureImageBase64: signatureData, - // Only send the stamp when it changed — omitted keeps the saved one. - ...(stampData && stampData !== saved?.stampImageUrl - ? { stampImageBase64: stampData } - : {}), + // Stamp untouched — it is managed by its own dialog. }, { onSuccess: () => { toast.success("Signature saved"); - setOpen(false); + setSignatureOpen(false); }, onError: () => toast.error("Failed to save signature"), }, ); }; + const openStampDialog = () => { + setStampData(saved?.stampImageUrl ?? null); + setStampOpen(true); + }; + + const saveStamp = () => { + if (!stampData) return; + saveMutation.mutate( + { + signerDisplayName: savedName || defaultName, + // Signature untouched — stamp-only update. + stampImageBase64: stampData, + }, + { + onSuccess: () => { + toast.success("Stamp saved"); + setStampOpen(false); + }, + onError: () => toast.error("Failed to save stamp"), + }, + ); + }; + return ( @@ -85,47 +106,64 @@ export function MySignatureCard() { This signature can be reused to sign booking contracts. - + {isLoading ? (
- ) : saved?.signatureImageUrl ? ( -
-
- My saved signature -
-

- Saved as {saved.signerDisplayName} -

-
) : ( -

- You have not saved a signature yet. -

- )} - {saved?.stampImageUrl && ( -
-
- My saved company stamp + <> +
+ {saved?.signatureImageUrl ? ( + <> +
+ My saved signature +
+

+ Saved as {saved.signerDisplayName} +

+ + ) : ( +

+ You have not saved a signature yet. +

+ )} +
-

Company stamp

-
+ +
+ {saved?.stampImageUrl ? ( + <> +
+ My saved company stamp +
+

Company stamp

+ + ) : ( +

+ You have not uploaded a company stamp yet. +

+ )} + +
+ )} - - + Save your signature @@ -145,21 +183,16 @@ export function MySignatureCard() { />
- - + + + +
); } diff --git a/apps/edr-freight-web/backoffice/src/services/signatures.service.ts b/apps/edr-freight-web/backoffice/src/services/signatures.service.ts index 577e3b4de..6a71c1836 100644 --- a/apps/edr-freight-web/backoffice/src/services/signatures.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/signatures.service.ts @@ -11,7 +11,8 @@ export interface SavedSignature { export interface SaveSignaturePayload { signerDisplayName: string; - signatureImageBase64: string; + /** Omit to keep the existing saved signature (stamp-only update). */ + signatureImageBase64?: string; /** Omit to keep the existing saved stamp. */ stampImageBase64?: string; } diff --git a/apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx b/apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx index 678829cb3..dd911d284 100644 --- a/apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx +++ b/apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { FileSignature, Loader2 } from "lucide-react"; +import { FileSignature, Loader2, Stamp } from "lucide-react"; import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad"; import { StampUpload } from "@/components/contracts/StampUpload"; @@ -26,41 +26,56 @@ import { } from "@edr/ui-common"; /** - * Lets the signed-in customer view and update the reusable signature stored on - * their profile. The same signature is offered for approval when signing a - * booking contract. + * Lets the signed-in customer view and update the reusable signature and + * company stamp stored on their profile — managed independently of each + * other. Both are offered when signing a booking contract. */ export function MySignatureCard() { const { user } = useAuth(); const { data: saved, isPending } = useMySignature(); const saveMutation = useSaveSignature(); - const [open, setOpen] = useState(false); + const [signatureOpen, setSignatureOpen] = useState(false); + const [stampOpen, setStampOpen] = useState(false); const [signerName, setSignerName] = useState(""); const [signatureData, setSignatureData] = useState(null); const [stampData, setStampData] = useState(null); const defaultName = user?.name?.en || user?.username || user?.email || ""; + const savedName = saved?.signerDisplayName ?? defaultName; - const openDialog = () => { - setSignerName(saved?.signerDisplayName ?? defaultName); + const openSignatureDialog = () => { + setSignerName(savedName); setSignatureData(null); - setStampData(saved?.stampImageUrl ?? null); - setOpen(true); + setSignatureOpen(true); }; - const save = () => { + const saveSignature = () => { if (!signatureData || !signerName.trim()) return; saveMutation.mutate( { signerDisplayName: signerName.trim(), signatureImageBase64: signatureData, - // Only send the stamp when it changed — omitted keeps the saved one. - ...(stampData && stampData !== saved?.stampImageUrl - ? { stampImageBase64: stampData } - : {}), + // Stamp untouched — it is managed by its own dialog. }, - { onSuccess: () => setOpen(false) }, + { onSuccess: () => setSignatureOpen(false) }, + ); + }; + + const openStampDialog = () => { + setStampData(saved?.stampImageUrl ?? null); + setStampOpen(true); + }; + + const saveStamp = () => { + if (!stampData) return; + saveMutation.mutate( + { + signerDisplayName: savedName || defaultName, + // Signature untouched — stamp-only update. + stampImageBase64: stampData, + }, + { onSuccess: () => setStampOpen(false) }, ); }; @@ -75,47 +90,64 @@ export function MySignatureCard() { Reused to approve and sign booking contracts. - + {isPending ? (
- ) : saved?.signatureImageUrl ? ( -
-
- My saved signature -
-

- Saved as {saved.signerDisplayName} -

-
) : ( -

- You have not saved a signature yet. -

- )} - {saved?.stampImageUrl && ( -
-
- My saved company stamp + <> +
+ {saved?.signatureImageUrl ? ( + <> +
+ My saved signature +
+

+ Saved as {saved.signerDisplayName} +

+ + ) : ( +

+ You have not saved a signature yet. +

+ )} +
-

Company stamp

-
+ +
+ {saved?.stampImageUrl ? ( + <> +
+ My saved company stamp +
+

Company stamp

+ + ) : ( +

+ You have not uploaded a company stamp yet. +

+ )} + +
+ )} - - + Save your signature @@ -135,21 +167,16 @@ export function MySignatureCard() { />
- - + + + + ); } diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx index d56272e0c..03a9e515a 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx @@ -21,7 +21,9 @@ import AuthShell from "@/components/auth/AuthShell"; import OtpChannelStep, { OTP_LENGTH } from "@/components/auth/OtpChannelStep"; import PasswordChecklist from "@/components/auth/PasswordChecklist"; import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField"; +import { StampUpload } from "@/components/contracts/StampUpload"; import { api } from "@/services/api"; +import { signaturesService } from "@/services/signatures.service"; import { confirmPasswordField, passwordField, @@ -59,6 +61,10 @@ export default function SignupPage() { const navigate = useNavigate(); const { signup } = useAuth(); const [error, setError] = useState(null); + // Company stamp image, captured at signup and stored on the new profile so + // it is ready when the customer signs their first contract. Optional — + // individuals without a stamp can add one later from Settings. + const [stampData, setStampData] = useState(null); // Two-stage signup: fill the form, then a mandatory OTP challenge before the // account is actually created. The code goes to BOTH the email and phone just @@ -179,6 +185,18 @@ export default function SignupPage() { }; const result = await signup(payload); if (result.success) { + // Signup logs the user in, so the stamp can land on their profile + // right away. Non-fatal — it can also be added later from Settings. + if (stampData) { + try { + await signaturesService.saveMySignature({ + signerDisplayName: payload.name.en, + stampImageBase64: stampData, + }); + } catch { + // Ignore — account exists; the stamp can be re-uploaded later. + } + } navigate("/portal"); } else { setOtpError(result.error.message); @@ -269,6 +287,13 @@ export default function SignupPage() { {...register("confirmPassword")} /> + + {error ? (