mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
769 lines
25 KiB
TypeScript
769 lines
25 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
Alert,
|
|
Badge,
|
|
Box,
|
|
Button,
|
|
FileButton,
|
|
Group,
|
|
Loader,
|
|
Paper,
|
|
Progress,
|
|
Stack,
|
|
Text,
|
|
Textarea,
|
|
ThemeIcon,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import {
|
|
AlertCircle,
|
|
CheckCircle2,
|
|
Clock,
|
|
Download,
|
|
Eye,
|
|
FileCheck2,
|
|
FileText,
|
|
MessageSquareWarning,
|
|
Upload,
|
|
UserCheck,
|
|
} from "lucide-react";
|
|
import type { Freight } from "@edr/types";
|
|
import { isViewable } from "@edr/ui-common";
|
|
|
|
import { SectionCard } from "@/components/bookings/detail/SectionCard";
|
|
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
|
import { contractsService } from "@/services/contracts.service";
|
|
import {
|
|
downloadBookingFile,
|
|
fetchViewableFile,
|
|
} from "@/services/files.service";
|
|
import { useContractClearanceMutations } from "@/hooks/contracts/useContracts";
|
|
import { useFileViewer } from "@/hooks/useFileViewer";
|
|
|
|
export interface ContractClearanceReviewSectionProps {
|
|
contractId: string;
|
|
/** Called after any review/finalize mutation so the parent can refetch. */
|
|
onChanged?: () => void;
|
|
/** Hide the inline progress summary (e.g. when the parent renders its own). */
|
|
hideSummary?: boolean;
|
|
/**
|
|
* Path A (non-customs): the reviewer is Operations, not GL, and there is no GL
|
|
* output upload step. Routes review/finalize to the Operations endpoints.
|
|
*/
|
|
selfClear?: boolean;
|
|
/**
|
|
* Clearance is finalized — render the document outcomes (approved / queried,
|
|
* by whom, when) but hide all approve / query / finalize actions.
|
|
*/
|
|
readOnly?: boolean;
|
|
/**
|
|
* Document approvals are locked (e.g. after all docs approved in phased flow)
|
|
* but queries remain available until {@link queriesLocked} or {@link readOnly}.
|
|
*/
|
|
approvalsLocked?: boolean;
|
|
/**
|
|
* Pre-clearance finalized — block opening new queries on customer documents.
|
|
*/
|
|
queriesLocked?: boolean;
|
|
/**
|
|
* ONE_TIME customs contracts use the phased milestone workflow. Hides the
|
|
* legacy "Finalize document approval" shortcut; booking readiness follows delivery
|
|
* order (import) or export release.
|
|
*/
|
|
phasedCustoms?: boolean;
|
|
}
|
|
|
|
const STATUS_META: Record<
|
|
Freight.ContractDocReviewStatus,
|
|
{ label: string; color: string }
|
|
> = {
|
|
APPROVED: { label: "Approved", color: "edr-green" },
|
|
QUERIED: { label: "Queried", color: "red" },
|
|
PENDING: { label: "Pending", color: "gray" },
|
|
};
|
|
|
|
function formatReviewedAt(value?: string | null): string | null {
|
|
if (!value) return null;
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return null;
|
|
return d.toLocaleString(undefined, {
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pre-booking clearance review for a CONTRACT. Approve / query each customer
|
|
* document, upload GL output documents, and finalize once every required
|
|
* document is approved. When `readOnly` it becomes an audit view: approved /
|
|
* queried outcomes with reviewer + timestamp, no actions.
|
|
*/
|
|
export function ContractClearanceReviewSection({
|
|
contractId,
|
|
onChanged,
|
|
hideSummary,
|
|
selfClear = false,
|
|
readOnly = false,
|
|
phasedCustoms = false,
|
|
approvalsLocked = false,
|
|
queriesLocked = false,
|
|
}: ContractClearanceReviewSectionProps) {
|
|
const [queryNotes, setQueryNotes] = useState<Record<string, string>>({});
|
|
const [openQuery, setOpenQuery] = useState<Record<string, boolean>>({});
|
|
const [uploadingKey, setUploadingKey] = useState<string | null>(null);
|
|
const { view, viewer } = useFileViewer();
|
|
|
|
const reviewerTeam = selfClear ? "Operations" : "Global Logistics";
|
|
|
|
const { data: clearance, isLoading } = useQuery({
|
|
queryKey: QUERY_KEYS.CONTRACTS.clearance(contractId),
|
|
queryFn: () => contractsService.getClearance(contractId),
|
|
});
|
|
|
|
const { reviewDocument, approveAll, uploadOutputDocuments, finalizeClearance } =
|
|
useContractClearanceMutations(contractId, selfClear);
|
|
|
|
const customerDocs = useMemo(
|
|
() =>
|
|
(clearance?.documents ?? []).filter((d) => d.uploadedBy === "customer"),
|
|
[clearance],
|
|
);
|
|
// GL output documents — anything not uploaded by the customer. The backend
|
|
// tags these `uploadedBy: 'gl'`; matching on "not customer" keeps it robust if
|
|
// that ever splits into gl_et / gl_dj.
|
|
const glDocs = useMemo(
|
|
() =>
|
|
(clearance?.documents ?? []).filter((d) => d.uploadedBy !== "customer"),
|
|
[clearance],
|
|
);
|
|
|
|
const stats = useMemo(() => {
|
|
const total = customerDocs.length;
|
|
const approved = customerDocs.filter(
|
|
(d) => d.reviewStatus === "APPROVED",
|
|
).length;
|
|
const queried = customerDocs.filter(
|
|
(d) => d.reviewStatus === "QUERIED",
|
|
).length;
|
|
const pending = total - approved - queried;
|
|
const pct = total === 0 ? 0 : Math.round((approved / total) * 100);
|
|
return { total, approved, queried, pending, pct };
|
|
}, [customerDocs]);
|
|
|
|
// Documents with a file uploaded but not yet approved — "Approve all" targets.
|
|
const approvableKeys = customerDocs
|
|
.filter((d) => d.file && d.reviewStatus !== "APPROVED")
|
|
.map((d) => d.fileKey);
|
|
|
|
const hasDocsAwaitingApproval = customerDocs.some(
|
|
(d) => d.file && d.reviewStatus !== "APPROVED",
|
|
);
|
|
const effectiveApprovalsLocked = approvalsLocked && !hasDocsAwaitingApproval;
|
|
|
|
if (isLoading || !clearance) {
|
|
return (
|
|
<Group justify="center" py="xl" gap={10}>
|
|
<Loader size="sm" color="edr-green" />
|
|
<Text c="dimmed">Loading clearance…</Text>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
const handleReview = (
|
|
fileKey: string,
|
|
status: "APPROVED" | "QUERIED",
|
|
note?: string,
|
|
) =>
|
|
reviewDocument.mutate(
|
|
{ fileKey, status, note },
|
|
{
|
|
onSuccess: () => {
|
|
if (status === "QUERIED")
|
|
setOpenQuery((o) => ({ ...o, [fileKey]: false }));
|
|
onChanged?.();
|
|
},
|
|
},
|
|
);
|
|
|
|
return (
|
|
<Stack gap="lg">
|
|
<SectionCard
|
|
icon={FileText}
|
|
title="Customer documents"
|
|
subtitle={
|
|
readOnly
|
|
? `Reviewed by the ${reviewerTeam} team.`
|
|
: effectiveApprovalsLocked
|
|
? "Documents are approved — you can still open a query if something needs fixing."
|
|
: "Approve each document, or open a query to tell the customer what to fix."
|
|
}
|
|
extra={
|
|
<Group gap={10} wrap="nowrap" align="center">
|
|
<Text size="xs" c="dimmed" fw={600}>
|
|
{stats.approved}/{stats.total} approved
|
|
</Text>
|
|
{!readOnly && !effectiveApprovalsLocked && approvableKeys.length > 0 && (
|
|
<Button
|
|
size="compact-sm"
|
|
color="edr-green"
|
|
radius="md"
|
|
leftSection={<FileCheck2 size={14} />}
|
|
loading={approveAll.isPending}
|
|
disabled={reviewDocument.isPending}
|
|
onClick={() => approveAll.mutate(approvableKeys)}
|
|
>
|
|
Approve all ({approvableKeys.length})
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
}
|
|
>
|
|
<Stack gap={12}>
|
|
{!hideSummary && stats.total > 0 && (
|
|
<Box>
|
|
<Progress
|
|
value={stats.pct}
|
|
color="edr-green"
|
|
radius="xl"
|
|
size="sm"
|
|
mb={6}
|
|
/>
|
|
<Group gap="lg">
|
|
<StatPill color="edr-green" label="Approved" value={stats.approved} />
|
|
<StatPill color="red" label="Queried" value={stats.queried} />
|
|
<StatPill color="gray" label="Pending" value={stats.pending} />
|
|
</Group>
|
|
</Box>
|
|
)}
|
|
{customerDocs.length === 0 ? (
|
|
<Text size="sm" c="dimmed">
|
|
No customer documents are required for this contract.
|
|
</Text>
|
|
) : (
|
|
customerDocs.map((doc) => (
|
|
<DocReviewCard
|
|
key={`${doc.settingCode}:${doc.fileKey}`}
|
|
doc={doc}
|
|
reviewerTeam={reviewerTeam}
|
|
readOnly={readOnly}
|
|
approvalsLocked={effectiveApprovalsLocked}
|
|
queriesLocked={queriesLocked}
|
|
note={queryNotes[doc.fileKey] ?? ""}
|
|
queryOpen={openQuery[doc.fileKey] ?? false}
|
|
onToggleQuery={(open) =>
|
|
setOpenQuery((o) => ({ ...o, [doc.fileKey]: open }))
|
|
}
|
|
onNote={(v) =>
|
|
setQueryNotes((n) => ({ ...n, [doc.fileKey]: v }))
|
|
}
|
|
onApprove={() => handleReview(doc.fileKey, "APPROVED")}
|
|
onQuery={() =>
|
|
handleReview(doc.fileKey, "QUERIED", queryNotes[doc.fileKey])
|
|
}
|
|
onView={view}
|
|
busy={reviewDocument.isPending}
|
|
/>
|
|
))
|
|
)}
|
|
</Stack>
|
|
</SectionCard>
|
|
|
|
{glDocs.length > 0 && !phasedCustoms && (
|
|
<SectionCard
|
|
icon={Upload}
|
|
title="GL output documents"
|
|
subtitle="Upload each document individually — changes save immediately."
|
|
accent="edr-green"
|
|
>
|
|
<Stack gap={10}>
|
|
{glDocs.map((doc) => {
|
|
const isUploading =
|
|
uploadingKey === doc.fileKey && uploadOutputDocuments.isPending;
|
|
return (
|
|
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
|
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<FileText size={16} color="var(--mantine-color-edr-green-6)" />
|
|
<Text fz="13px" c="edr-text" truncate>
|
|
{doc.label}
|
|
{doc.required ? " *" : ""}
|
|
</Text>
|
|
</Group>
|
|
<Group gap={8} wrap="nowrap">
|
|
{doc.file ? (
|
|
<>
|
|
{isViewable({
|
|
name: doc.file.name,
|
|
url: "",
|
|
}) && (
|
|
<Tooltip label="View">
|
|
<Box
|
|
component="button"
|
|
type="button"
|
|
onClick={() =>
|
|
void fetchViewableFile(
|
|
doc.file!.id,
|
|
doc.file!.name,
|
|
).then(view)
|
|
}
|
|
c="edr-green"
|
|
style={{
|
|
display: "flex",
|
|
background: "transparent",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<Eye size={15} />
|
|
</Box>
|
|
</Tooltip>
|
|
)}
|
|
<Tooltip label="Download">
|
|
<Box
|
|
component="button"
|
|
type="button"
|
|
onClick={() =>
|
|
void downloadBookingFile(
|
|
doc.file!.id,
|
|
doc.file!.name,
|
|
)
|
|
}
|
|
c="edr-green"
|
|
style={{
|
|
display: "flex",
|
|
background: "transparent",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<Download size={15} />
|
|
</Box>
|
|
</Tooltip>
|
|
</>
|
|
) : (
|
|
<Text fz="12px" c="edr-muted">
|
|
Not uploaded
|
|
</Text>
|
|
)}
|
|
{!readOnly && (
|
|
<FileButton
|
|
onChange={(f) => {
|
|
if (!f) return;
|
|
setUploadingKey(doc.fileKey);
|
|
uploadOutputDocuments.mutate(
|
|
{ [doc.fileKey]: f },
|
|
{ onSuccess: () => { setUploadingKey(null); onChanged?.(); },
|
|
onError: () => setUploadingKey(null) },
|
|
);
|
|
}}
|
|
accept="application/pdf,image/*"
|
|
disabled={isUploading}
|
|
>
|
|
{(props) => (
|
|
<Button
|
|
{...props}
|
|
size="compact-xs"
|
|
variant="light"
|
|
color="edr-green"
|
|
leftSection={
|
|
isUploading ? (
|
|
<Loader size={12} color="edr-green" />
|
|
) : (
|
|
<Upload size={13} />
|
|
)
|
|
}
|
|
loading={isUploading}
|
|
>
|
|
{doc.file ? "Replace" : "Upload"}
|
|
</Button>
|
|
)}
|
|
</FileButton>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
);
|
|
})}
|
|
</Stack>
|
|
</SectionCard>
|
|
)}
|
|
|
|
{!readOnly && finalizeClearance.isError && (
|
|
<Alert color="red" radius="md" icon={<AlertCircle size={16} />}>
|
|
{finalizeClearance.error instanceof Error
|
|
? finalizeClearance.error.message
|
|
: "Could not finalize document approval."}
|
|
</Alert>
|
|
)}
|
|
|
|
{readOnly ? (
|
|
<Paper
|
|
withBorder
|
|
radius="md"
|
|
p="md"
|
|
style={{
|
|
borderColor: "var(--mantine-color-edr-green-3)",
|
|
background:
|
|
"linear-gradient(135deg, var(--mantine-color-edr-green-0) 0%, #FFFFFF 70%)",
|
|
}}
|
|
>
|
|
<Group gap={10} wrap="nowrap">
|
|
<ThemeIcon variant="light" color="edr-green" radius="md" size={28}>
|
|
<CheckCircle2 size={15} />
|
|
</ThemeIcon>
|
|
<Text fz="12.5px" c="dimmed">
|
|
{phasedCustoms
|
|
? "Document review is complete. Continue customs milestones in the action panel."
|
|
: `Clearance was finalized by the ${reviewerTeam} team. This is a read-only record of the approved documents.`}
|
|
</Text>
|
|
</Group>
|
|
</Paper>
|
|
) : phasedCustoms && effectiveApprovalsLocked ? (
|
|
<Paper withBorder radius="md" p="md">
|
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<ThemeIcon variant="light" color="edr-green" radius="md" size={28}>
|
|
<CheckCircle2 size={15} />
|
|
</ThemeIcon>
|
|
<Text fz="12.5px" c="dimmed">
|
|
Document review is complete. Use the action panel for declaration, duty, and
|
|
transit steps
|
|
{queriesLocked
|
|
? ". Pre-clearance is finalized — customer documents can no longer be queried."
|
|
: " — or open a query above if a customer document needs correction."}
|
|
</Text>
|
|
</Group>
|
|
</Paper>
|
|
) : phasedCustoms ? (
|
|
<Paper withBorder radius="md" p="md">
|
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<ThemeIcon
|
|
variant="light"
|
|
color={clearance.allApproved ? "edr-green" : "gray"}
|
|
radius="md"
|
|
size={28}
|
|
>
|
|
<FileCheck2 size={15} />
|
|
</ThemeIcon>
|
|
<Text fz="12.5px" c="dimmed">
|
|
{clearance.allApproved
|
|
? "All required documents are approved. Continue declaration, duty, and transit in the action panel."
|
|
: "Approve every required document to unlock the customs milestone steps."}
|
|
</Text>
|
|
</Group>
|
|
</Paper>
|
|
) : (
|
|
<Paper withBorder radius="md" p="md">
|
|
<Group justify="space-between" wrap="nowrap">
|
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<ThemeIcon
|
|
variant="light"
|
|
color={clearance.allApproved ? "edr-green" : "gray"}
|
|
radius="md"
|
|
size={28}
|
|
>
|
|
<FileCheck2 size={15} />
|
|
</ThemeIcon>
|
|
<Text fz="12.5px" c="dimmed">
|
|
{clearance.allApproved
|
|
? "All required documents are approved — you can finalize."
|
|
: "Approve every required document to unlock finalization."}
|
|
</Text>
|
|
</Group>
|
|
<Button
|
|
color="edr-green"
|
|
radius="md"
|
|
leftSection={<CheckCircle2 size={16} />}
|
|
disabled={!clearance.allApproved}
|
|
loading={finalizeClearance.isPending}
|
|
onClick={() =>
|
|
finalizeClearance.mutate(undefined, {
|
|
onSuccess: () => onChanged?.(),
|
|
})
|
|
}
|
|
>
|
|
Finalize document approval
|
|
</Button>
|
|
</Group>
|
|
</Paper>
|
|
)}
|
|
{viewer}
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
function StatPill({
|
|
color,
|
|
label,
|
|
value,
|
|
}: {
|
|
color: string;
|
|
label: string;
|
|
value: number;
|
|
}) {
|
|
return (
|
|
<Group gap={6} wrap="nowrap">
|
|
<Box
|
|
style={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 999,
|
|
background: `var(--mantine-color-${color}-6)`,
|
|
}}
|
|
/>
|
|
<Text fz="12.5px" c="edr-text" fw={600}>
|
|
{value}
|
|
</Text>
|
|
<Text fz="12.5px" c="dimmed">
|
|
{label}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
function DocReviewCard({
|
|
doc,
|
|
reviewerTeam,
|
|
readOnly,
|
|
approvalsLocked,
|
|
queriesLocked,
|
|
note,
|
|
queryOpen,
|
|
onToggleQuery,
|
|
onNote,
|
|
onApprove,
|
|
onQuery,
|
|
onView,
|
|
busy,
|
|
}: {
|
|
doc: Freight.ContractClearanceDocument;
|
|
reviewerTeam: string;
|
|
readOnly: boolean;
|
|
approvalsLocked: boolean;
|
|
queriesLocked: boolean;
|
|
note: string;
|
|
queryOpen: boolean;
|
|
onToggleQuery: (open: boolean) => void;
|
|
onNote: (v: string) => void;
|
|
onApprove: () => void;
|
|
onQuery: () => void;
|
|
onView: (file: { name: string; url: string }) => void;
|
|
busy: boolean;
|
|
}) {
|
|
const status = doc.reviewStatus ?? "PENDING";
|
|
const meta = STATUS_META[status];
|
|
const hasFile = !!doc.file;
|
|
const isApproved = status === "APPROVED";
|
|
const isQueried = status === "QUERIED";
|
|
const reviewedAt = formatReviewedAt(doc.reviewedAt);
|
|
|
|
// Approved cards get a light green gradient + green border so the outcome is
|
|
// instantly scannable; queried cards get a soft red; pending stay neutral.
|
|
const cardStyle = isApproved
|
|
? {
|
|
borderColor: "var(--mantine-color-edr-green-3)",
|
|
background:
|
|
"linear-gradient(135deg, var(--mantine-color-edr-green-0) 0%, #FFFFFF 72%)",
|
|
}
|
|
: isQueried
|
|
? {
|
|
borderColor: "var(--mantine-color-red-2)",
|
|
background:
|
|
"linear-gradient(135deg, var(--mantine-color-red-0) 0%, #FFFFFF 78%)",
|
|
}
|
|
: { borderColor: "var(--mantine-color-edr-border-6)" };
|
|
|
|
return (
|
|
<Paper withBorder radius="md" p="md" style={cardStyle}>
|
|
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
|
<Group gap={12} wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<ThemeIcon
|
|
variant="light"
|
|
color={isApproved ? "edr-green" : isQueried ? "red" : "gray"}
|
|
radius="md"
|
|
size={40}
|
|
>
|
|
{isApproved ? <CheckCircle2 size={19} /> : <FileText size={19} />}
|
|
</ThemeIcon>
|
|
<Box style={{ minWidth: 0 }}>
|
|
<Text fz="14px" fw={700} c="edr-text" truncate>
|
|
{doc.label}
|
|
{doc.required ? " *" : ""}
|
|
</Text>
|
|
<Text fz="12px" c="edr-muted" truncate>
|
|
{hasFile ? doc.file!.name : "Not uploaded by customer"}
|
|
</Text>
|
|
{isApproved && (reviewedAt || reviewerTeam) && (
|
|
<Group gap={5} wrap="nowrap" mt={3}>
|
|
<UserCheck size={12} color="var(--mantine-color-edr-green-7)" />
|
|
<Text fz="11.5px" c="edr-green.8" fw={600} truncate>
|
|
Approved by {reviewerTeam}
|
|
{reviewedAt ? ` · ${reviewedAt}` : ""}
|
|
</Text>
|
|
</Group>
|
|
)}
|
|
</Box>
|
|
</Group>
|
|
|
|
<Group gap={8} wrap="nowrap">
|
|
<Badge
|
|
variant="light"
|
|
color={meta.color}
|
|
radius="sm"
|
|
leftSection={
|
|
isApproved ? (
|
|
<CheckCircle2 size={11} />
|
|
) : isQueried ? (
|
|
<MessageSquareWarning size={11} />
|
|
) : (
|
|
<Clock size={11} />
|
|
)
|
|
}
|
|
>
|
|
{meta.label}
|
|
</Badge>
|
|
{hasFile &&
|
|
isViewable({
|
|
name: doc.file!.name,
|
|
url: "",
|
|
}) && (
|
|
<Tooltip label="Preview document">
|
|
<Button
|
|
size="compact-xs"
|
|
variant="default"
|
|
radius="md"
|
|
leftSection={<Eye size={13} />}
|
|
onClick={() =>
|
|
void fetchViewableFile(doc.file!.id, doc.file!.name).then(
|
|
onView,
|
|
)
|
|
}
|
|
>
|
|
View
|
|
</Button>
|
|
</Tooltip>
|
|
)}
|
|
{hasFile && (
|
|
<Tooltip label="Download">
|
|
<Button
|
|
component="button"
|
|
type="button"
|
|
onClick={() =>
|
|
void downloadBookingFile(doc.file!.id, doc.file!.name)
|
|
}
|
|
size="compact-xs"
|
|
variant="default"
|
|
radius="md"
|
|
leftSection={<Download size={13} />}
|
|
>
|
|
Download
|
|
</Button>
|
|
</Tooltip>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
|
|
{isQueried && doc.note && (
|
|
<Alert
|
|
mt="sm"
|
|
color="red"
|
|
variant="light"
|
|
radius="md"
|
|
icon={<MessageSquareWarning size={15} />}
|
|
p="xs"
|
|
>
|
|
<Text fz="12.5px" c="red.9">
|
|
{doc.note}
|
|
</Text>
|
|
</Alert>
|
|
)}
|
|
|
|
{!readOnly && hasFile && (
|
|
<Box mt="sm">
|
|
{!queryOpen ? (
|
|
<Group justify="flex-end" gap={8}>
|
|
{!queriesLocked && (
|
|
<Button
|
|
size="sm"
|
|
variant="light"
|
|
color="red"
|
|
radius="md"
|
|
leftSection={<MessageSquareWarning size={15} />}
|
|
disabled={busy}
|
|
onClick={() => onToggleQuery(true)}
|
|
>
|
|
Open query
|
|
</Button>
|
|
)}
|
|
{!isApproved && !approvalsLocked && (
|
|
<Button
|
|
size="sm"
|
|
color="edr-green"
|
|
radius="md"
|
|
leftSection={<CheckCircle2 size={15} />}
|
|
disabled={busy}
|
|
onClick={onApprove}
|
|
>
|
|
Approve
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
) : (
|
|
<Box
|
|
p="sm"
|
|
style={{
|
|
borderRadius: 12,
|
|
background: "var(--mantine-color-red-0)",
|
|
border: "1px solid var(--mantine-color-red-2)",
|
|
}}
|
|
>
|
|
<Group gap={6} mb={6}>
|
|
<MessageSquareWarning
|
|
size={14}
|
|
color="var(--mantine-color-red-7)"
|
|
/>
|
|
<Text fz="12.5px" fw={700} c="red.8">
|
|
Describe the problem for the customer
|
|
</Text>
|
|
</Group>
|
|
<Textarea
|
|
placeholder="e.g. The commercial invoice is missing the HS code."
|
|
value={note}
|
|
onChange={(e) => onNote(e.currentTarget.value)}
|
|
autosize
|
|
minRows={2}
|
|
radius="md"
|
|
size="sm"
|
|
autoFocus
|
|
/>
|
|
<Group justify="flex-end" gap={8} mt={8}>
|
|
<Button
|
|
size="sm"
|
|
variant="subtle"
|
|
color="gray"
|
|
radius="md"
|
|
disabled={busy}
|
|
onClick={() => onToggleQuery(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
color="red"
|
|
radius="md"
|
|
leftSection={<MessageSquareWarning size={15} />}
|
|
loading={busy}
|
|
disabled={!note.trim()}
|
|
onClick={onQuery}
|
|
>
|
|
Send query to customer
|
|
</Button>
|
|
</Group>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
);
|
|
}
|