Merge branch 'dev'

This commit is contained in:
Marshal
2026-08-12 06:44:30 +00:00
94 changed files with 5399 additions and 3583 deletions

View File

@@ -170,12 +170,13 @@ export function ContractCustomerCard({
/>
</SectionCard>
<SectionCard icon={User} title="General manager" accent="grape">
{/* Whoever the eTrade licence names as the business's manager. */}
<SectionCard icon={User} title="Owner" accent="grape">
<InfoRows
rows={[
{ icon: User, label: "Name", value: company.generalManagerName },
{ icon: Mail, label: "Email", value: company.generalManagerEmail },
{ icon: Phone, label: "Phone", value: company.generalManagerPhone },
{ icon: User, label: "Name", value: company.ownerName },
{ icon: Mail, label: "Email", value: company.ownerEmail },
{ icon: Phone, label: "Phone", value: company.ownerPhone },
]}
/>
</SectionCard>

View File

@@ -43,9 +43,17 @@ export const FIELD_LABELS: Record<string, string> = {
contactPersonPosition: "Contact position",
contactPersonEmail: "Contact email",
contactPersonPhone: "Contact phone",
generalManagerName: "General manager",
generalManagerEmail: "GM email",
generalManagerPhone: "GM phone",
ownerName: "Owner name",
ownerEmail: "Owner email",
ownerPhone: "Owner phone",
poaDeclared: "Has a Power of Attorney",
poaPassportNumber: "PoA passport number",
// Nothing writes these any more — the general manager was removed — but
// change requests filed before that still carry them, and without a label
// the reviewer sees a raw attribute key.
generalManagerName: "General manager (retired)",
generalManagerEmail: "GM email (retired)",
generalManagerPhone: "GM phone (retired)",
poaName: "PoA name",
poaPhone: "PoA phone",
poaEmail: "PoA email",
@@ -79,9 +87,9 @@ export function currentValue(company: Company, key: string): string {
nationality: c.nationality,
contactPersonName: c.contactPersonName ?? attrs.contactPersonName,
contactPersonPhone: c.contactPersonPhone ?? attrs.contactPersonPhone,
generalManagerName: c.generalManagerName ?? attrs.generalManagerName,
generalManagerEmail: c.generalManagerEmail ?? attrs.generalManagerEmail,
generalManagerPhone: c.generalManagerPhone ?? attrs.generalManagerPhone,
ownerName: c.ownerName ?? attrs.ownerName,
ownerEmail: c.ownerEmail ?? attrs.ownerEmail,
ownerPhone: c.ownerPhone ?? attrs.ownerPhone,
};
const v = key in map ? map[key] : (c[key] ?? attrs[key]);
return v === null || v === undefined || v === "" ? "—" : String(v);

View File

@@ -485,6 +485,18 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
icon: <Settings />,
permission: FREIGHT_PERMS.settings.dropdown.view,
},
{
label: "Stamp settings",
href: "/dashboard/stamp-settings",
icon: <FileSignature />,
permission: FREIGHT_PERMS.settings.stamp.view,
},
{
label: "Invoice stamp",
href: "/dashboard/invoice-stamp-settings",
icon: <Receipt />,
permission: FREIGHT_PERMS.settings.invoiceStamp.view,
},
{
label: "Contract templates",
href: "/dashboard/contract-templates",

View File

@@ -1,11 +1,10 @@
import { useState } from "react";
import { FileSignature, Loader2, Stamp } from "lucide-react";
import { FileSignature, Loader2 } 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,
@@ -27,9 +26,13 @@ import {
} 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.
* Lets the signed-in user view and update the reusable signature stored on
* their profile, offered for approval when signing a contract.
*
* Signature only — there is no per-employee stamp. EDR seals with ONE global
* company stamp, managed under Settings and applied server-side, so a staff
* member never uploads or picks a stamp. (Customers do upload their own, in
* the portal — that is a different card.)
*/
export function MySignatureCard() {
const { user } = useAuth();
@@ -39,10 +42,8 @@ export function MySignatureCard() {
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 || "";
@@ -60,7 +61,6 @@ export function MySignatureCard() {
{
signerDisplayName: signerName.trim(),
signatureImageBase64: signatureData,
// Stamp untouched — it is managed by its own dialog.
},
{
onSuccess: () => {
@@ -72,38 +72,16 @@ export function MySignatureCard() {
);
};
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 &amp; Stamp
Signature
</CardTitle>
<CardDescription>
This signature can be reused to sign booking contracts.
This signature can be reused to sign booking contracts. The EDR
company stamp is applied automatically you do not upload one.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
@@ -112,54 +90,29 @@ export function MySignatureCard() {
<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.
<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>
)}
<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>
</>
</>
) : (
<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>
)}
</CardContent>
@@ -203,39 +156,6 @@ 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>
);
}