configer the stamp

This commit is contained in:
Marshal
2026-08-02 20:29:09 +00:00
parent 3956c78c54
commit 950bcc0912
7 changed files with 281 additions and 123 deletions

View File

@@ -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<string | null>(null);
const [stampData, setStampData] = useState<string | null>(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 (
<Card>
<CardHeader>
@@ -85,47 +106,64 @@ export function MySignatureCard() {
This signature can be reused to sign booking contracts.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="space-y-6">
{isLoading ? (
<div className="flex h-36 items-center justify-center">
<Loader2 className="size-6 animate-spin text-primary" />
</div>
) : saved?.signatureImageUrl ? (
<div className="space-y-2">
<div className="overflow-hidden rounded-lg border-2 border-dashed border-border bg-white">
<img
src={saved.signatureImageUrl}
alt="My saved signature"
className="mx-auto h-36 w-full object-contain"
/>
</div>
<p className="text-xs text-muted-foreground">
Saved as {saved.signerDisplayName}
</p>
</div>
) : (
<p className="text-sm text-muted-foreground">
You have not saved a signature yet.
</p>
)}
{saved?.stampImageUrl && (
<div className="space-y-2">
<div className="overflow-hidden rounded-lg border-2 border-dashed border-border bg-white">
<img
src={saved.stampImageUrl}
alt="My saved company stamp"
className="mx-auto h-24 w-full object-contain"
/>
<>
<div className="space-y-2">
{saved?.signatureImageUrl ? (
<>
<div className="overflow-hidden rounded-lg border-2 border-dashed border-border bg-white">
<img
src={saved.signatureImageUrl}
alt="My saved signature"
className="mx-auto h-36 w-full object-contain"
/>
</div>
<p className="text-xs text-muted-foreground">
Saved as {saved.signerDisplayName}
</p>
</>
) : (
<p className="text-sm text-muted-foreground">
You have not saved a signature yet.
</p>
)}
<Button variant="outline" size="sm" onClick={openSignatureDialog}>
{saved?.signatureImageUrl ? "Update signature" : "Add signature"}
</Button>
</div>
<p className="text-xs text-muted-foreground">Company stamp</p>
</div>
<div className="space-y-2">
{saved?.stampImageUrl ? (
<>
<div className="overflow-hidden rounded-lg border-2 border-dashed border-border bg-white">
<img
src={saved.stampImageUrl}
alt="My saved company stamp"
className="mx-auto h-24 w-full object-contain"
/>
</div>
<p className="text-xs text-muted-foreground">Company stamp</p>
</>
) : (
<p className="text-sm text-muted-foreground">
You have not uploaded a company stamp yet.
</p>
)}
<Button variant="outline" size="sm" onClick={openStampDialog}>
<Stamp className="size-4" />
{saved?.stampImageUrl ? "Update stamp" : "Upload stamp"}
</Button>
</div>
</>
)}
<Button variant="outline" size="sm" onClick={openDialog}>
{saved?.signatureImageUrl ? "Update signature" : "Add signature"}
</Button>
</CardContent>
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={signatureOpen} onOpenChange={setSignatureOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Save your signature</DialogTitle>
@@ -145,21 +183,16 @@ export function MySignatureCard() {
/>
</div>
<ContractSignaturePad onChange={setSignatureData} />
<StampUpload
value={stampData}
onChange={setStampData}
description="Stored on your profile and prefilled when you sign contracts."
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
<Button variant="outline" onClick={() => setSignatureOpen(false)}>
Cancel
</Button>
<Button
disabled={
saveMutation.isPending || !signatureData || !signerName.trim()
}
onClick={save}
onClick={saveSignature}
>
{saveMutation.isPending ? (
<Loader2 className="size-4 animate-spin" />
@@ -170,6 +203,39 @@ export function MySignatureCard() {
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={stampOpen} onOpenChange={setStampOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Company stamp</DialogTitle>
<DialogDescription>
Upload your official company stamp or seal as an image. It is
stored on your profile and applied next to your signature on
contracts.
</DialogDescription>
</DialogHeader>
<StampUpload
value={stampData}
onChange={setStampData}
description="Stored on your profile and prefilled when you sign contracts."
/>
<DialogFooter>
<Button variant="outline" onClick={() => setStampOpen(false)}>
Cancel
</Button>
<Button
disabled={saveMutation.isPending || !stampData}
onClick={saveStamp}
>
{saveMutation.isPending ? (
<Loader2 className="size-4 animate-spin" />
) : (
"Save stamp"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
}