import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { ActionIcon, Alert, Box, Button, Divider, Group, Loader, Modal, // Select, // ponytail: unused now the validity dropdown below is commented out Stack, Text, Textarea, TextInput, Tooltip, } from "@mantine/core"; import { ArrowDown, ArrowUp, FileText, Info, Lock, Plus, Trash2, } from "lucide-react"; import { DateInput } from "@mantine/dates"; import type { Freight } from "@edr/types"; import { contractsService } from "@/services/contracts.service"; /** New client-side article id (server keeps whatever id we send). */ function newArticleId(): string { const c = globalThis.crypto; if (c && typeof c.randomUUID === "function") return c.randomUUID(); return `art-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; } /** Midnight today — the earliest day a contract's validity may start. */ function startOfToday(): Date { const d = new Date(); d.setHours(0, 0, 0, 0); return d; } interface EditableArticle { id: string; title: string; body: string; } export interface ContractDocumentEditorModalProps { opened: boolean; onClose: () => void; contractId: string; /** * "accept" — shown from the Accept-for-approval action: pick a validity window * and (optionally) edit the articles, then start the approval chain. * "edit" — re-edit the frozen articles of an already-accepted contract before * generating/regenerating its PDF. */ mode: "accept" | "edit"; /** Validity options (accept mode only). */ validityOptions?: Array<{ value: string; label: string }>; validityLoading?: boolean; accepting?: boolean; saving?: boolean; onAccept?: ( validityDays: number, snapshot: Freight.IContractDocumentSnapshot, window: { validFrom: string; validUntil: string }, ) => void; onSaveEdit?: (snapshot: Freight.IContractDocumentSnapshot) => void; } /** * Per-contract contract-document editor. Loads the resolved template (or this * contract's frozen snapshot) and lets staff add/remove/reorder/edit articles * for THIS contract only — it never writes back to the shared six templates. */ export function ContractDocumentEditorModal({ opened, onClose, contractId, mode, // ponytail: validityOptions/validityLoading fed the now-commented dropdown // above — caller still passes them, left unread here for a quick revert. // validityOptions = [], // validityLoading = false, accepting = false, saving = false, onAccept, onSaveEdit, }: ContractDocumentEditorModalProps) { const { data: draft, isLoading } = useQuery({ queryKey: ["contracts", contractId, "document-draft"], queryFn: () => contractsService.getContractDocumentDraft(contractId), enabled: opened && Boolean(contractId), // Always refetch the current draft when the dialog opens. staleTime: 0, }); const [documentTitle, setDocumentTitle] = useState(""); const [whereasClauses, setWhereasClauses] = useState([]); const [articles, setArticles] = useState([]); // const [validityDays, setValidityDays] = useState(null); // ponytail: client keeps flip-flopping the validity requirement — swapped // the validity dropdown for explicit start/end dates, kept above commented // instead of deleted so it's a one-line revert if they flip back. const [validityStart, setValidityStart] = useState(null); const [validityEnd, setValidityEnd] = useState(null); // Seed the editor from the loaded draft whenever the dialog (re)opens. useEffect(() => { if (!opened || !draft) return; setDocumentTitle(draft.documentTitle ?? ""); setWhereasClauses(draft.whereasClauses ?? []); setArticles( (draft.articles ?? []).map((a) => ({ id: a.id || newArticleId(), title: a.title, body: a.body, })), ); }, [opened, draft]); // Accept mode opens on today — a contract never starts in the past, and the // pickers below refuse earlier days. useEffect(() => { if (!opened || mode !== "accept") return; setValidityStart(startOfToday()); setValidityEnd(null); }, [opened, mode]); // Default validity to the first configured option (accept mode). // useEffect(() => { // if (mode === "accept" && !validityDays && validityOptions.length > 0) { // setValidityDays(validityOptions[0].value); // } // }, [mode, validityDays, validityOptions]); // Editing rights belong to the approver whose turn it is, so the server // decides per-caller — the client cannot derive this from the contract alone. const locked = mode === "edit" && !draft?.editableByMe; const moveArticle = (index: number, delta: number) => { setArticles((prev) => { const next = [...prev]; const target = index + delta; if (target < 0 || target >= next.length) return prev; [next[index], next[target]] = [next[target], next[index]]; return next; }); }; const updateArticle = (id: string, patch: Partial) => setArticles((prev) => prev.map((a) => (a.id === id ? { ...a, ...patch } : a)), ); const removeArticle = (id: string) => setArticles((prev) => prev.filter((a) => a.id !== id)); const addArticle = () => setArticles((prev) => [ ...prev, { id: newArticleId(), title: "", body: "" }, ]); const buildSnapshot = (): Freight.IContractDocumentSnapshot => ({ code: draft?.code ?? null, name: draft?.name ?? null, documentTitle: documentTitle.trim() || null, whereasClauses: whereasClauses .map((c) => c.trim()) .filter((c) => c.length > 0), articles: articles .filter((a) => a.title.trim().length > 0 || a.body.trim().length > 0) .map((a, index) => ({ id: a.id, title: a.title.trim(), body: a.body, order: index + 1, })), }); const hasArticles = useMemo( () => articles.some((a) => a.title.trim() || a.body.trim()), [articles], ); const submit = () => { const snapshot = buildSnapshot(); if (mode === "accept") { // const days = Number(validityDays); // if (!days) return; if (!validityStart || !validityEnd) return; const days = Math.ceil( (validityEnd.getTime() - validityStart.getTime()) / (24 * 60 * 60 * 1000), ); if (days <= 0) return; onAccept?.(days, snapshot, { validFrom: validityStart.toISOString(), validUntil: validityEnd.toISOString(), }); } else { onSaveEdit?.(snapshot); } }; const submitting = accepting || saving; const canSubmit = hasArticles && !locked && // (mode === "edit" || Boolean(validityDays)) && (mode === "edit" || Boolean(validityStart && validityEnd)) && !submitting; return ( {mode === "accept" ? "Review contract document & accept" : "Edit contract document"} } > {isLoading ? ( Loading document… ) : ( : } > {locked ? draft?.nextApproverRole ? `Only the current approver (${draft.nextApproverRole}) can edit this document right now.` : "This document can no longer be edited — the contract has advanced beyond approval." : "Edits apply to THIS contract only. The six shared templates are never changed."} setDocumentTitle(e.currentTarget.value)} disabled={locked} /> WHEREAS recitals {whereasClauses.length === 0 ? ( No recitals. ) : ( {whereasClauses.map((clause, i) => (