feat(freight-api): seal EDR side of contracts with the one global stamp

This commit is contained in:
ghost2023
2026-08-12 06:26:46 +03:00
parent e52d666647
commit a603807e8e
21 changed files with 679 additions and 183 deletions

View File

@@ -12,7 +12,6 @@ import toast from "react-hot-toast";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad";
import { StampUpload } from "@/components/contracts/StampUpload";
import { bookingSurface } from "@/components/bookings/booking-ui.styles";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { invalidateBookingDetail } from "@/utils/queryInvalidation";
@@ -41,9 +40,6 @@ export default function BookingContractPage() {
const [signOpen, setSignOpen] = useState(false);
const [signerName, setSignerName] = useState("");
const [signatureData, setSignatureData] = useState<string | null>(null);
// Company stamp: prefilled from the profile, or uploaded here when none is
// saved yet.
const [stampData, setStampData] = useState<string | null>(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);
@@ -59,7 +55,6 @@ export default function BookingContractPage() {
const savedSignature = data?.savedSignature ?? null;
const savedSignatureImage = savedSignature?.signatureImageUrl ?? null;
const savedStampImage = savedSignature?.stampImageUrl ?? 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;
@@ -103,8 +98,6 @@ export default function BookingContractPage() {
// approve it; otherwise start with an empty pad.
setSignerName(savedSignature?.signerDisplayName ?? "");
setSignatureData(null);
// Prefill with the reusable stamp saved on the profile; still replaceable.
setStampData(savedStampImage);
setDrawNew(false);
setSignOpen(true);
};
@@ -113,12 +106,12 @@ export default function BookingContractPage() {
if (!canSign || !signerName.trim()) return;
// Approve the saved signature, or submit the freshly drawn one.
const image = usingSaved ? savedSignatureImage : signatureData;
// The API rejects a STAFF signature without a stamp.
if (!image || !stampData) return;
if (!image) return;
// No stamp is sent: EDR's seal is the ONE global company stamp, applied
// server-side at render time (see StampSettingsService).
signMutation.mutate({
role: "STAFF",
signatureImageBase64: image,
stampImageBase64: stampData,
signerDisplayName: signerName.trim(),
consentText: "I agree to the terms of this contract.",
});
@@ -243,15 +236,6 @@ export default function BookingContractPage() {
) : (
<ContractSignaturePad onChange={setSignatureData} />
)}
<StampUpload
value={stampData}
onChange={setStampData}
description={
savedStampImage
? "Your saved company stamp — replace it for this contract if needed."
: "Required. Attach your official company stamp or seal."
}
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setSignOpen(false)}>
@@ -261,7 +245,6 @@ export default function BookingContractPage() {
disabled={
signMutation.isPending ||
(!usingSaved && !signatureData) ||
!stampData ||
!signerName.trim()
}
onClick={confirmSign}

View File

@@ -18,7 +18,6 @@ import toast from "react-hot-toast";
import { ContractSignSuccessModal } from "@/components/contracts/ContractSignSuccessModal";
import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad";
import { StampUpload } from "@/components/contracts/StampUpload";
import { contractsService } from "@/services/contracts.service";
import { extractApiError } from "@/utils/result";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
@@ -40,7 +39,6 @@ export default function ContractViewPage() {
const [successOpen, setSuccessOpen] = useState(false);
const [signerName, setSignerName] = useState("");
const [signatureData, setSignatureData] = useState<string | null>(null);
const [stampData, setStampData] = useState<string | null>(null);
const [drawNew, setDrawNew] = useState(false);
const { data, isLoading, isError, refetch } = useQuery({
@@ -56,10 +54,11 @@ export default function ContractViewPage() {
mutationFn: () =>
contractsService.signContract(id!, {
role: "STAFF",
// No stamp is sent: EDR's seal is the ONE global company stamp, applied
// server-side from StampSettingsService when the signature is stored.
signatureImageBase64: usingSaved
? (savedSignatureImage as string)
: (signatureData ?? ""),
stampImageBase64: stampData ?? "",
signerDisplayName: signerName.trim(),
consentText: "I confirm this contract on behalf of EDR.",
}),
@@ -98,14 +97,12 @@ export default function ContractViewPage() {
const openSign = () => {
setSignerName(data?.savedSignature?.signerDisplayName ?? "");
setSignatureData(null);
// Prefill with the reusable stamp saved on the profile; still replaceable.
setStampData(data?.savedSignature?.stampImageUrl ?? null);
setDrawNew(false);
setSignOpen(true);
};
const confirmSign = () => {
if (!signerName.trim() || !stampData) return;
if (!signerName.trim()) return;
const image = usingSaved ? savedSignatureImage : signatureData;
if (!image) return;
signMutation.mutate();
@@ -243,13 +240,6 @@ export default function ContractViewPage() {
<ContractSignaturePad onChange={setSignatureData} />
)}
<StampUpload
value={stampData}
onChange={setStampData}
label="EDR company stamp"
description="Attach the official EDR stamp or seal — it is applied to the contract next to the signature."
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setSignOpen(false)}>
Cancel
@@ -260,8 +250,7 @@ export default function ContractViewPage() {
disabled={
signMutation.isPending ||
!signerName.trim() ||
(!usingSaved && !signatureData) ||
!stampData
(!usingSaved && !signatureData)
}
onClick={confirmSign}
>