import { useState, useRef, useEffect } from "react"; // ── Palette & helpers ────────────────────────────────────────────────────── const STATUS_META = { DRAFT: { label: "Draft", bg: "#1E293B", text: "#94A3B8", border: "#334155" }, SUBOWN: { label: "Submitted", bg: "#0C1A2E", text: "#38BDF8", border: "#0369A1" }, PENDING: { label: "Pending Reply", bg: "#1C0A00", text: "#FB923C", border: "#C2410C" }, APPROVED: { label: "Approved", bg: "#052E16", text: "#4ADE80", border: "#15803D" }, REJECTED: { label: "Rejected", bg: "#2D0A0A", text: "#F87171", border: "#B91C1C" }, CLOSED: { label: "Closed", bg: "#1A1A2E", text: "#A78BFA", border: "#7C3AED" }, }; const PROJECTS = [ { code: "LCBP3", name: "Laem Chabang Basin Phase 3" }, { code: "LCBP2", name: "Laem Chabang Basin Phase 2" }, { code: "PILOT", name: "Pilot Project" }, ]; const TYPES = ["Letter", "RFI", "Notice", "Instruction", "Report", "Memo"]; const DISCIPLINES = ["GEN – General", "STR – Structural", "MEP – Mechanical", "ARC – Architecture", "CIV – Civil"]; const ORGS = ["กทท. (PAT)", "TEAM Consulting", "CHINA HARBOUR", "Third Party Inspector"]; const MOCK_DATA = [ { id: 1, uuid: "c1a2b3c4", number: "LCBP3-LTR-2026-0042", type: "Letter", discipline: "GEN", isInternal: false, originator: "CHINA HARBOUR", project: "LCBP3", recipients: [{ org: "กทท. (PAT)", type: "TO" }, { org: "TEAM Consulting", type: "CC" }], tags: ["Urgent", "Schedule"], revisions: [ { id: 10, revNo: 0, label: "0", isCurrent: false, status: "APPROVED", subject: "Request for Extension of Time – Phase 2 Piling Works", description: "Initial submission requesting EOT due to unforeseen ground conditions.", body: "In accordance with Clause 44 of the Contract, we hereby formally request an extension of time for the Phase 2 piling works...", remarks: "Supporting geotechnical report attached.", docDate: "2026-01-10", issuedDate: "2026-01-12", receivedDate: "2026-01-13", dueDate: "2026-02-12", createdBy: "John Smith", updatedBy: null, attachments: [ { id: 1, name: "EOT-Request-Rev0.pdf", size: 2457600, mime: "application/pdf", isMain: true }, { id: 2, name: "Geotech-Report.pdf", size: 8912000, mime: "application/pdf", isMain: false }, ], }, { id: 11, revNo: 1, label: "1", isCurrent: true, status: "PENDING", subject: "Request for Extension of Time – Phase 2 Piling Works (Rev.1)", description: "Revised submission incorporating PAT's comments dated 2026-02-01.", body: "Following PAT's comments dated 2026-02-01, we have revised our EOT request as follows...", remarks: "Updated programme attached. Previous geotechnical report remains unchanged.", docDate: "2026-02-08", issuedDate: "2026-02-10", receivedDate: null, dueDate: "2026-03-10", createdBy: "John Smith", updatedBy: "Jane Doe", attachments: [ { id: 3, name: "EOT-Request-Rev1.pdf", size: 3012000, mime: "application/pdf", isMain: true }, { id: 4, name: "Updated-Programme.xlsx", size: 512000, mime: "application/vnd.ms-excel", isMain: false }, { id: 2, name: "Geotech-Report.pdf", size: 8912000, mime: "application/pdf", isMain: false }, ], }, ], references: ["LCBP3-LTR-2026-0018", "LCBP3-RFI-2025-0091"], createdAt: "2026-01-10", }, { id: 2, uuid: "d2e3f4a5", number: "LCBP3-RFI-2026-0011", type: "RFI", discipline: "STR", isInternal: false, originator: "CHINA HARBOUR", project: "LCBP3", recipients: [{ org: "TEAM Consulting", type: "TO" }], tags: ["Technical"], revisions: [ { id: 20, revNo: 0, label: "0", isCurrent: true, status: "SUBOWN", subject: "Clarification on Pile Cap Reinforcement Detail – Drawing STR-PC-105", description: "Request for information regarding conflicting dimensions in pile cap drawing.", body: "Please clarify the discrepancy between the plan dimension (4500mm) and section detail (4200mm) on Drawing STR-PC-105...", remarks: "", docDate: "2026-03-05", issuedDate: "2026-03-06", receivedDate: null, dueDate: "2026-03-20", createdBy: "Alice Chen", updatedBy: null, attachments: [ { id: 5, name: "RFI-011-Query.pdf", size: 1245000, mime: "application/pdf", isMain: true }, { id: 6, name: "STR-PC-105-marked.pdf", size: 3100000, mime: "application/pdf", isMain: false }, ], }, ], references: [], createdAt: "2026-03-05", }, { id: 3, uuid: "e5f6a7b8", number: "LCBP3-NTC-2026-0007", type: "Notice", discipline: "GEN", isInternal: true, originator: "กทท. (PAT)", project: "LCBP3", recipients: [{ org: "CHINA HARBOUR", type: "TO" }, { org: "TEAM Consulting", type: "CC" }], tags: ["HSE", "Urgent"], revisions: [ { id: 30, revNo: 0, label: "0", isCurrent: true, status: "CLOSED", subject: "Non-Conformance Notice – Safety Barrier Installation at Gate 3", description: "Formal notice of non-conformance regarding inadequate safety barriers.", body: "This is to formally notify that the safety barrier installation at Gate 3 does not comply with approved method statement MS-HSE-012...", remarks: "Immediate remedial action required within 48 hours.", docDate: "2026-02-20", issuedDate: "2026-02-20", receivedDate: "2026-02-21", dueDate: "2026-02-22", createdBy: "PAT Safety", updatedBy: "PAT Safety", attachments: [ { id: 7, name: "NCN-007-Notice.pdf", size: 980000, mime: "application/pdf", isMain: true }, { id: 8, name: "Photo-Evidence.zip", size: 15000000, mime: "application/zip", isMain: false }, ], }, ], references: ["LCBP3-LTR-2026-0031"], createdAt: "2026-02-20", }, ]; // ── Utility components ───────────────────────────────────────────────────── function StatusBadge({ code }) { const m = STATUS_META[code] || { label: code, bg: "#1E293B", text: "#94A3B8", border: "#334155" }; return ( {m.label} ); } function TypeBadge({ type }) { const colors = { Letter: "#F59E0B", RFI: "#38BDF8", Notice: "#F87171", Instruction: "#A78BFA", Report: "#4ADE80", Memo: "#94A3B8" }; return ( {type.toUpperCase()} ); } function TagPill({ label }) { return ( {label} ); } function FileIcon({ mime }) { if (mime?.includes("pdf")) return 📄; if (mime?.includes("excel") || mime?.includes("sheet")) return 📊; if (mime?.includes("zip") || mime?.includes("compressed")) return 📦; return 📎; } function formatBytes(b) { if (b < 1024) return b + " B"; if (b < 1048576) return (b / 1024).toFixed(0) + " KB"; return (b / 1048576).toFixed(1) + " MB"; } // ── Flow Diagram Component ───────────────────────────────────────────────── function FlowDiagram({ onClose }) { const flows = [ { step: "1", icon: "📝", title: "สร้าง Master Record", sub: "correspondences table", desc: "กำหนด Type, Number, Originator, Project, Recipients (TO/CC), Tags", color: "#F59E0B" }, { step: "2", icon: "📋", title: "สร้าง Revision 0", sub: "correspondence_revisions (is_current=true)", desc: "ใส่เนื้อหา: Subject, Body, Dates, Status=DRAFT, Details JSON", color: "#38BDF8" }, { step: "3", icon: "⬆️", title: "Upload ไฟล์แนบ (Phase 1)", sub: "attachments (is_temporary=true)", desc: "อัปโหลดล่วงหน้า ได้ temp_id กลับมา ไฟล์ยังไม่ Commit", color: "#A78BFA" }, { step: "4", icon: "🔗", title: "Commit ไฟล์ (Phase 2)", sub: "correspondence_revision_attachments", desc: "ผูก temp attachment → revision ด้วย is_main_document flag, set is_temporary=false", color: "#4ADE80" }, { step: "5", icon: "✉️", title: "Submit / ส่งเอกสาร", sub: "status: DRAFT → SUBOWN", desc: "เปลี่ยน Status, บันทึก issued_date, แจ้งเตือน Recipient", color: "#FB923C" }, { step: "6", icon: "🔄", title: "เพิ่ม Revision ใหม่", sub: "correspondence_revisions (revision_number++)", desc: "set is_current=false บน Rev เก่า, สร้าง Rev ใหม่ is_current=true", color: "#F87171" }, ]; return (
UX FLOW DIAGRAM

Correspondence Lifecycle

{/* DB Relationship */}
TABLE RELATIONSHIP (Master-Revision Pattern)
{[ { label: "correspondences", role: "MASTER", color: "#F59E0B" }, { arrow: "1:N" }, { label: "correspondence_revisions", role: "REVISIONS", color: "#38BDF8" }, { arrow: "M:N" }, { label: "attachments", role: "FILES", color: "#A78BFA" }, ].map((item, i) => item.arrow ? (
{item.arrow}
) : (
{item.label}
{item.role}
))}
{["correspondence_recipients (M:N)", "correspondence_tags (M:N)", "correspondence_references (M:N)", "correspondence_revision_attachments (Junction)"].map(t => ( {t} ))}
{/* Flow Steps */}
{flows.map((f, i) => (
{f.step}
{f.icon} {f.title} {f.sub}
{f.desc}
))}
{/* Key UX Rules */}
KEY UX RULES
{[ "correspondence_number สร้างอัตโนมัติ (DocumentNumberingModule) — ผู้ใช้เลือก Type เท่านั้น", "ไฟล์แนบ upload ก่อน → ได้ temp_id → commit หลัง save form (Two-Phase)", "is_current เปลี่ยนได้เพียง 1 Revision ต่อ Correspondence เท่านั้น", "แต่ละ Revision มีชุดไฟล์แนบเป็นของตัวเอง ผ่าน correspondence_revision_attachments", "Revision ใหม่ copy เนื้อหาจาก Rev ล่าสุด เพื่อลดการพิมพ์ซ้ำ", "Recipients (TO/CC) ผูกกับ Master ไม่ใช่ Revision — เปลี่ยนได้ตลอด", ].map((r, i) => (
{r}
))}
); } // ── Create Correspondence Wizard ─────────────────────────────────────────── function CreateWizard({ onClose, onCreated }) { const [step, setStep] = useState(1); const [form, setForm] = useState({ type: "", discipline: "", isInternal: false, originator: "", project: "LCBP3", toOrgs: [], ccOrgs: [], subject: "", body: "", remarks: "", docDate: "", dueDate: "", status: "DRAFT", files: [], mainFileIdx: null, }); const fileRef = useRef(); const STEPS = ["Basic Info", "Content", "Attachments", "Review"]; const total = 4; const update = (k, v) => setForm(f => ({ ...f, [k]: v })); const addFile = (e) => { const newFiles = Array.from(e.target.files).map(file => ({ name: file.name, size: file.size, mime: file.type, tempId: "tmp_" + Math.random().toString(36).slice(2), isMain: false, })); setForm(f => ({ ...f, files: [...f.files, ...newFiles] })); }; const toggleRecipient = (org, type) => { const key = type === "TO" ? "toOrgs" : "ccOrgs"; const arr = form[key]; setForm(f => ({ ...f, [key]: arr.includes(org) ? arr.filter(x => x !== org) : [...arr, org] })); }; const inputStyle = { width: "100%", background: "#0A1525", border: "1px solid #1E293B", color: "#F1F5F9", padding: "8px 12px", borderRadius: 6, fontSize: 13, outline: "none", boxSizing: "border-box", }; const labelStyle = { color: "#64748B", fontSize: 11, fontWeight: 600, letterSpacing: "0.05em", marginBottom: 4, display: "block" }; return (
{/* Header */}
NEW DOCUMENT

Create Correspondence

{/* Step indicators */}
{STEPS.map((s, i) => ( ))}
{/* Body */}
{step === 1 && (
{form.type ? `LCBP3-${form.type.slice(0,3).toUpperCase()}-2026-XXXX` : "เลือก Type เพื่อ Preview เลขที่"} Auto-generated
{ORGS.filter(o => o !== form.originator).map(org => (
{org} {["TO", "CC"].map(type => ( ))}
))}
update("isInternal", e.target.checked)} style={{ accentColor: "#F59E0B", width: 16, height: 16 }} />
)} {step === 2 && (
REVISION
Rev. 0 • First submission — Revision 0 จะสร้างอัตโนมัติ (is_current=true)
update("subject", e.target.value)} />