mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 14:38:12 +00:00
242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
import { useState } from "react";
|
|
import { FileSignature, Loader2, Stamp } from "lucide-react";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import toast from "react-hot-toast";
|
|
|
|
import { api } from "@/services/api";
|
|
import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad";
|
|
import { StampUpload } from "@/components/contracts/StampUpload";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { useAuth } from "@/auth/useAuth";
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
Input,
|
|
Label,
|
|
} from "@edr/ui-common";
|
|
|
|
/**
|
|
* 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();
|
|
const { data: saved, isLoading } = useQuery(
|
|
api.signatures.mySignature.queryOptions({ staleTime: 60_000 }),
|
|
);
|
|
const saveMutation = useMutation(api.signatures.save.mutationOptions());
|
|
|
|
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 openSignatureDialog = () => {
|
|
setSignerName(savedName);
|
|
setSignatureData(null);
|
|
setSignatureOpen(true);
|
|
};
|
|
|
|
const saveSignature = () => {
|
|
if (!signatureData || !signerName.trim()) return;
|
|
saveMutation.mutate(
|
|
{
|
|
signerDisplayName: signerName.trim(),
|
|
signatureImageBase64: signatureData,
|
|
// Stamp untouched — it is managed by its own dialog.
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
toast.success("Signature saved");
|
|
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>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<FileSignature className="size-4" />
|
|
Signature & Stamp
|
|
</CardTitle>
|
|
<CardDescription>
|
|
This signature can be reused to sign booking contracts.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{isLoading ? (
|
|
<div className="flex h-36 items-center justify-center">
|
|
<Loader2 className="size-6 animate-spin text-primary" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<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>
|
|
|
|
<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>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
|
|
<Dialog open={signatureOpen} onOpenChange={setSignatureOpen}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Save your signature</DialogTitle>
|
|
<DialogDescription>
|
|
Draw your signature below. It will be stored on your profile for
|
|
future contracts.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="profileSignerName">Full name</Label>
|
|
<Input
|
|
id="profileSignerName"
|
|
value={signerName}
|
|
onChange={(e) => setSignerName(e.target.value)}
|
|
placeholder="As shown on contracts"
|
|
/>
|
|
</div>
|
|
<ContractSignaturePad onChange={setSignatureData} />
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setSignatureOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
disabled={
|
|
saveMutation.isPending || !signatureData || !signerName.trim()
|
|
}
|
|
onClick={saveSignature}
|
|
>
|
|
{saveMutation.isPending ? (
|
|
<Loader2 className="size-4 animate-spin" />
|
|
) : (
|
|
"Save signature"
|
|
)}
|
|
</Button>
|
|
</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>
|
|
);
|
|
}
|