Files
edr-platform/apps/edr-freight-web/backoffice/src/components/contracts/StampUpload.tsx
Marshal fde5e6de4b add company stamp upload functionality for contract signing
- Introduced StampUpload component for uploading company stamp images.
- Integrated stamp upload in contract signing modal, supporting PNG and JPG formats.
- Implemented validation for file type and size (max 5 MB).
- Added visual feedback for drag-and-drop functionality.
- Updated contract-related pages to handle duplicate contract alerts and pricing notices.
- Enhanced contract expiry management with a nightly sweep service.
- Added unit tests for new features and updated existing tests for contract handling.
2026-07-25 17:14:58 +00:00

172 lines
5.0 KiB
TypeScript

import { useRef, useState } from "react";
import { Box, Button, Group, Image, Paper, Stack, Text } from "@mantine/core";
import { RefreshCw, Stamp, X } from "lucide-react";
const MAX_STAMP_MB = 5;
export interface StampUploadProps {
/** Stamp image as a data URL, or null when none is attached yet. */
value: string | null;
onChange: (dataUrl: string | null) => void;
label?: string;
description?: string;
}
/**
* Company stamp/seal attachment for the contract signing modal. Reads the
* picked image straight into a data URL because the signing endpoint takes
* base64 in JSON (same transport as the drawn signature), not multipart.
*/
export function StampUpload({
value,
onChange,
label = "Company stamp",
description = "Attach your official company stamp or seal.",
}: StampUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [dragging, setDragging] = useState(false);
const [error, setError] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const readFile = (file: File | undefined | null) => {
if (!file) return;
if (!file.type.startsWith("image/")) {
setError("The stamp must be an image file (PNG or JPG).");
return;
}
if (file.size > MAX_STAMP_MB * 1024 * 1024) {
setError(`The stamp image must be under ${MAX_STAMP_MB} MB.`);
return;
}
const reader = new FileReader();
reader.onload = () => {
setError(null);
setFileName(file.name);
onChange(typeof reader.result === "string" ? reader.result : null);
};
reader.onerror = () => setError("Could not read that file. Try another.");
reader.readAsDataURL(file);
};
const openPicker = () => inputRef.current?.click();
const clear = () => {
setFileName(null);
setError(null);
onChange(null);
if (inputRef.current) inputRef.current.value = "";
};
return (
<Stack gap={6}>
<Text size="sm" fw={500}>
{label}
</Text>
<input
ref={inputRef}
type="file"
accept="image/png,image/jpeg,image/webp"
hidden
onChange={(e) => readFile(e.currentTarget.files?.[0])}
/>
{value ? (
<Paper withBorder radius="md" p="sm">
<Group gap="md" wrap="nowrap" align="center">
<Box
style={{
background:
"repeating-conic-gradient(var(--mantine-color-gray-1) 0% 25%, transparent 0% 50%) 50% / 14px 14px",
borderRadius: 8,
flexShrink: 0,
padding: 6,
}}
>
<Image
src={value}
alt="Company stamp"
fit="contain"
h={92}
w={92}
/>
</Box>
<Stack gap={4} style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={500} truncate>
{fileName ?? "Stamp attached"}
</Text>
<Text size="xs" c="dimmed">
This stamp is applied next to your signature on the contract.
</Text>
<Group gap="xs" mt={2}>
<Button
size="compact-xs"
variant="light"
color="edr-green"
leftSection={<RefreshCw size={13} />}
onClick={openPicker}
>
Replace
</Button>
<Button
size="compact-xs"
variant="subtle"
color="red"
leftSection={<X size={13} />}
onClick={clear}
>
Remove
</Button>
</Group>
</Stack>
</Group>
</Paper>
) : (
<Paper
withBorder
radius="md"
p="lg"
onClick={openPicker}
onDragOver={(e) => {
e.preventDefault();
setDragging(true);
}}
onDragLeave={() => setDragging(false)}
onDrop={(e) => {
e.preventDefault();
setDragging(false);
readFile(e.dataTransfer.files?.[0]);
}}
style={{
borderColor: dragging
? "var(--mantine-color-edr-green-6)"
: undefined,
borderStyle: "dashed",
backgroundColor: dragging
? "var(--mantine-color-edr-green-0)"
: undefined,
cursor: "pointer",
}}
>
<Stack gap={6} align="center">
<Stamp size={26} color="var(--mantine-color-edr-green-6)" />
<Text size="sm" fw={500}>
Upload company stamp
</Text>
<Text size="xs" c="dimmed" ta="center">
{description} Drop an image here or click to browse PNG or JPG,
up to {MAX_STAMP_MB} MB.
</Text>
</Stack>
</Paper>
)}
{error && (
<Text size="xs" c="red.7">
{error}
</Text>
)}
</Stack>
);
}