mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
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.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { Text, Tooltip, UnstyledButton } from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { canSeeDocReviewAlert } from "@/lib/permissions";
|
||||
import { api } from "@/services/api";
|
||||
|
||||
/**
|
||||
* Booking-request statuses that count as "nobody decided yet". These are the
|
||||
* exact statuses the window engine expires when document review ends
|
||||
* (findUnacceptedForRouteDay), so the deep-linked list shows precisely the
|
||||
* requests the countdown is warning about.
|
||||
*/
|
||||
const UNDECIDED_STATUSES = [
|
||||
"OPERATION_REQUESTED",
|
||||
"OPERATION_REQUEST_PENDING",
|
||||
"OPERATION_CHANGES_REQUESTED",
|
||||
"OPERATION_PRICE_PENDING_CONFIRM",
|
||||
].join(",");
|
||||
|
||||
const pendingRequestsHref = (tradeDirection: string) =>
|
||||
`/dashboard/booking-requests?statuses=${UNDECIDED_STATUSES}&tradeDirection=${tradeDirection}`;
|
||||
|
||||
/** mm:ss (or h:mm:ss past an hour), fixed width so the pill never jitters. */
|
||||
function formatRemaining(ms: number): string {
|
||||
const total = Math.max(0, Math.floor(ms / 1000));
|
||||
const hours = Math.floor(total / 3600);
|
||||
const minutes = Math.floor((total % 3600) / 60);
|
||||
const seconds = total % 60;
|
||||
const mm = String(minutes).padStart(2, "0");
|
||||
const ss = String(seconds).padStart(2, "0");
|
||||
return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header alarm for the document-review deadline. Appears only once the review
|
||||
* phase is half spent AND requests are still undecided — everything still
|
||||
* pending when the clock runs out is expired automatically, so this is the last
|
||||
* call to accept or reject. Clicking opens the booking requests already
|
||||
* filtered to those undecided import requests.
|
||||
*/
|
||||
export default function DocReviewAlertButton() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data: alert } = useQuery({
|
||||
...api.trainScheduling.docReviewAlert.queryOptions(),
|
||||
// Dedicated permission: only the position types granted it are alarmed.
|
||||
enabled: canSeeDocReviewAlert(user),
|
||||
// The window engine ticks every 10s; a minute is close enough for a header
|
||||
// chip — the countdown itself runs locally.
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
|
||||
const deadlineMs = alert ? new Date(alert.docReviewEndsAt).getTime() : 0;
|
||||
const [remaining, setRemaining] = useState(() => deadlineMs - Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (!deadlineMs) return;
|
||||
const tick = () => setRemaining(deadlineMs - Date.now());
|
||||
tick();
|
||||
const id = window.setInterval(tick, 1000);
|
||||
return () => window.clearInterval(id);
|
||||
}, [deadlineMs]);
|
||||
|
||||
if (!alert) return null;
|
||||
// Half the review phase has to be gone before staff are alarmed — a 30-minute
|
||||
// review warns with 15 minutes left.
|
||||
const halfMs = (Math.max(alert.docReviewMinutes, 1) * 60_000) / 2;
|
||||
if (remaining <= 0 || remaining > halfMs) return null;
|
||||
|
||||
const requestLabel = alert.pendingCount === 1 ? "request" : "requests";
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
withArrow
|
||||
openDelay={200}
|
||||
multiline
|
||||
w={260}
|
||||
label={`Document review ends in ${formatRemaining(remaining)}. ${alert.pendingCount} ${alert.tradeDirection.toLowerCase()} booking ${requestLabel} ${alert.pendingCount === 1 ? "is" : "are"} still neither accepted nor rejected and will expire automatically. Click to review them.`}
|
||||
>
|
||||
<UnstyledButton
|
||||
onClick={() => navigate(pendingRequestsHref(alert.tradeDirection))}
|
||||
aria-label={`${alert.pendingCount} import booking ${requestLabel} awaiting a decision — document review ends in ${formatRemaining(remaining)}`}
|
||||
className="group flex h-9 shrink-0 items-center gap-2 rounded-full border border-red-600/60 bg-red-600 pl-2.5 pr-2 text-white shadow-[0_2px_10px_rgba(220,38,38,0.35)] transition-transform hover:scale-[1.02] hover:bg-red-700"
|
||||
>
|
||||
{/* Live dot: a ping ring behind a solid core, so the pill reads as
|
||||
active without animating the whole chip. */}
|
||||
<span className="relative flex size-2 shrink-0">
|
||||
<span className="absolute inline-flex size-full animate-ping rounded-full bg-white opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-white" />
|
||||
</span>
|
||||
|
||||
<AlertTriangle size={15} strokeWidth={2.2} className="shrink-0" />
|
||||
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
visibleFrom="sm"
|
||||
className="whitespace-nowrap text-white!"
|
||||
>
|
||||
{alert.pendingCount} undecided
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
className="text-white!"
|
||||
style={{ fontVariantNumeric: "tabular-nums", letterSpacing: "0.02em" }}
|
||||
>
|
||||
{formatRemaining(remaining)}
|
||||
</Text>
|
||||
|
||||
<ChevronRight
|
||||
size={14}
|
||||
strokeWidth={2.2}
|
||||
className="shrink-0 opacity-80 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user