diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx
index 742d15278..cfe22845d 100644
--- a/apps/edr-freight-web/backoffice/src/App.tsx
+++ b/apps/edr-freight-web/backoffice/src/App.tsx
@@ -28,6 +28,7 @@ import UserManagementHostPage from "./pages/dashboard/user-management/UserManage
import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page";
import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page";
import OverviewPage from "./pages/dashboard/OverviewPage";
+import MyProfilePage from "./pages/dashboard/MyProfilePage";
//import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage";
import PermissionsPage from "./pages/dashboard/user-management/PermissionsPage";
import PositionTypesPage from "./pages/dashboard/user-management/PositionTypesPage";
@@ -257,6 +258,7 @@ const App = () => {
} />
}>
} />
+ } />
} />
} />
diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx
index cc6454f60..d0dd0d04d 100644
--- a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx
@@ -10,6 +10,7 @@ import {
User,
} from "lucide-react";
import { Group, Stack, Text, Menu, Tooltip, Box } from "@mantine/core";
+import { useNavigate } from "react-router-dom";
import type { PageMeta } from "./types";
import "./FreightDashboardHeader.css";
@@ -37,6 +38,7 @@ const FreightDashboardHeader = ({
theme,
onToggleTheme,
}: FreightDashboardHeaderProps) => {
+ const navigate = useNavigate();
const initials =
userInitials ??
(userName
@@ -200,7 +202,10 @@ const FreightDashboardHeader = ({
}
- onClick={() => setIsUserMenuOpen(false)}
+ onClick={() => {
+ setIsUserMenuOpen(false);
+ navigate("/dashboard/profile");
+ }}
>
Profile
diff --git a/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts b/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts
index bc57dcdeb..3c421090a 100644
--- a/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts
+++ b/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts
@@ -36,6 +36,13 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
subtitle: "Dashboard summary and key metrics",
},
},
+ {
+ prefix: "/dashboard/profile",
+ meta: {
+ title: "My Profile",
+ subtitle: "Manage your account and signature",
+ },
+ },
{
prefix: "/dashboard/operations/train-scheduling-v2/",
meta: {
diff --git a/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx
new file mode 100644
index 000000000..3ac661e1a
--- /dev/null
+++ b/apps/edr-freight-web/backoffice/src/components/profile/MySignatureCard.tsx
@@ -0,0 +1,144 @@
+import { useState } from "react";
+import { FileSignature, Loader2 } from "lucide-react";
+
+import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { useAuth } from "@/auth/useAuth";
+import {
+ useMySignature,
+ useSaveSignature,
+} from "@/hooks/useSavedSignature";
+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 stored on
+ * their profile. The same signature is offered for approval when signing a
+ * booking contract.
+ */
+export function MySignatureCard() {
+ const { user } = useAuth();
+ const { data: saved, isLoading } = useMySignature();
+ const saveMutation = useSaveSignature();
+
+ const [open, setOpen] = useState(false);
+ const [signerName, setSignerName] = useState("");
+ const [signatureData, setSignatureData] = useState(null);
+
+ const defaultName =
+ user?.name?.en || user?.username || user?.email || "";
+
+ const openDialog = () => {
+ setSignerName(saved?.signerDisplayName ?? defaultName);
+ setSignatureData(null);
+ setOpen(true);
+ };
+
+ const save = () => {
+ if (!signatureData || !signerName.trim()) return;
+ saveMutation.mutate(
+ {
+ signerDisplayName: signerName.trim(),
+ signatureImageBase64: signatureData,
+ },
+ { onSuccess: () => setOpen(false) },
+ );
+ };
+
+ return (
+
+
+
+
+ My signature
+
+
+ This signature can be reused to sign booking contracts.
+
+
+
+ {isLoading ? (
+
+
+
+ ) : saved?.signatureImageUrl ? (
+
+
+

+
+
+ Saved as {saved.signerDisplayName}
+
+
+ ) : (
+
+ You have not saved a signature yet.
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/backoffice/src/hooks/useSavedSignature.ts b/apps/edr-freight-web/backoffice/src/hooks/useSavedSignature.ts
new file mode 100644
index 000000000..b8c9480a7
--- /dev/null
+++ b/apps/edr-freight-web/backoffice/src/hooks/useSavedSignature.ts
@@ -0,0 +1,30 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import toast from "react-hot-toast";
+
+import {
+ signaturesService,
+ type SaveSignaturePayload,
+} from "@/services/signatures.service";
+
+const SAVED_SIGNATURE_KEY = ["me", "signature"] as const;
+
+export function useMySignature() {
+ return useQuery({
+ queryKey: SAVED_SIGNATURE_KEY,
+ queryFn: () => signaturesService.getMySignature(),
+ staleTime: 60_000,
+ });
+}
+
+export function useSaveSignature() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: (payload: SaveSignaturePayload) =>
+ signaturesService.saveMySignature(payload),
+ onSuccess: () => {
+ toast.success("Signature saved");
+ void qc.invalidateQueries({ queryKey: SAVED_SIGNATURE_KEY });
+ },
+ onError: () => toast.error("Failed to save signature"),
+ });
+}
diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx
index 7c22f9d45..4689d28fa 100644
--- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingContractPage.tsx
@@ -40,6 +40,9 @@ export default function BookingContractPage() {
const [signOpen, setSignOpen] = useState(false);
const [signerName, setSignerName] = useState("");
const [signatureData, setSignatureData] = useState(null);
+ // When the user has a saved signature we offer it for approval first; they
+ // can switch to drawing a fresh one.
+ const [drawNew, setDrawNew] = useState(false);
const { data, isLoading, isError } = useQuery({
queryKey: [...QUERY_KEYS.BOOKINGS.byId(id ?? ""), "contract-view"],
@@ -53,6 +56,12 @@ export default function BookingContractPage() {
? "STAFF"
: null;
+ const savedSignature = data?.savedSignature ?? null;
+ const savedSignatureImage = savedSignature?.signatureImageUrl ?? null;
+ // Show the approval view only while a saved signature exists and the user
+ // hasn't opted to draw a new one.
+ const usingSaved = Boolean(savedSignatureImage) && !drawNew;
+
const signMutation = useMutation({
mutationFn: (payload: SignContractPayload) =>
bookingsService.signContract(id!, payload),
@@ -88,16 +97,22 @@ export default function BookingContractPage() {
};
const openSign = () => {
- setSignerName("");
+ // Prefill from the saved signature when available so the user only has to
+ // approve it; otherwise start with an empty pad.
+ setSignerName(savedSignature?.signerDisplayName ?? "");
setSignatureData(null);
+ setDrawNew(false);
setSignOpen(true);
};
const confirmSign = () => {
- if (!signRole || !signatureData || !signerName.trim()) return;
+ if (!signRole || !signerName.trim()) return;
+ // Approve the saved signature, or submit the freshly drawn one.
+ const image = usingSaved ? savedSignatureImage : signatureData;
+ if (!image) return;
signMutation.mutate({
role: signRole,
- signatureImageBase64: signatureData,
+ signatureImageBase64: image,
signerDisplayName: signerName.trim(),
consentText: "I agree to the terms of this contract.",
});
@@ -183,7 +198,9 @@ export default function BookingContractPage() {
{signRole === "CUSTOMER" ? "Customer signature" : "Staff signature"}
- Sign to execute the contract for {data.reference}.
+ {usingSaved
+ ? `Review your saved signature and approve it to execute the contract for ${data.reference}.`
+ : `Sign to execute the contract for ${data.reference}.`}
@@ -196,7 +213,32 @@ export default function BookingContractPage() {
placeholder="As shown on the contract"
/>
-
+ {usingSaved ? (
+
+
+
+

+
+
+
+ ) : (
+
+ )}