"use client"; import * as React from "react"; import dynamic from "next/dynamic"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { GenericData } from "./export-excel-form"; import * as GC from "@mescius/spread-sheets"; import { toast } from "sonner"; import { updateFormDataInDB } from "@/lib/forms/services"; import { Loader, Save } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css"; // Dynamically load the SpreadSheets component (disable SSR) const SpreadSheets = dynamic( () => import("@mescius/spread-sheets-react").then((mod) => mod.SpreadSheets), { ssr: false, loading: () => (
Loading SpreadSheets...
), } ); // Apply license key on the client only if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } interface TemplateItem { TMPL_ID: string; NAME: string; TMPL_TYPE: string; SPR_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; GRD_LST_SETUP: { REG_TYPE_ID: string; SPR_ITM_IDS: Array; ATTS: Array<{}>; }; SPR_ITM_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; } interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; templateData: TemplateItem[] | any; selectedRow: GenericData; formCode: string; contractItemId: number; editableFieldsMap?: Map; // editable field info per tag onUpdateSuccess?: (updatedValues: Record) => void; } export function TemplateViewDialog({ isOpen, onClose, templateData, selectedRow, formCode, contractItemId, editableFieldsMap = new Map(), onUpdateSuccess, }: TemplateViewDialogProps) { /* ------------------------- local state ------------------------- */ const [hostStyle] = React.useState({ width: "100%", height: "100%" }); const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); const [currentSpread, setCurrentSpread] = React.useState( null ); const [selectedTemplateId, setSelectedTemplateId] = React.useState(""); const [cellMappings, setCellMappings] = React.useState< Array<{ attId: string; cellAddress: string; isEditable: boolean }> >([]); const [isClient, setIsClient] = React.useState(false); // Render only on client side React.useEffect(() => { setIsClient(true); }, []); /* ------------------------- helpers ------------------------- */ // Normalize template list and keep only those with CONTENT const normalizedTemplates = React.useMemo((): TemplateItem[] => { if (!templateData) return []; const list = Array.isArray(templateData) ? (templateData as TemplateItem[]) : ([templateData] as TemplateItem[]); return list.filter( (t) => t.SPR_LST_SETUP?.CONTENT || t.SPR_ITM_LST_SETUP?.CONTENT ); }, [templateData]); // Choose currently selected template const selectedTemplate = React.useMemo(() => { if (!selectedTemplateId) return normalizedTemplates[0]; return ( normalizedTemplates.find((t) => t.TMPL_ID === selectedTemplateId) || normalizedTemplates[0] ); }, [normalizedTemplates, selectedTemplateId]); // Editable fields for the current TAG_NO const editableFields = React.useMemo(() => { if (!selectedRow?.TAG_NO) return []; return editableFieldsMap.get(selectedRow.TAG_NO) || []; }, [selectedRow?.TAG_NO, editableFieldsMap]); const isFieldEditable = React.useCallback( (attId: string) => { // TAG_NO and TAG_DESC are always editable if (attId === "TAG_NO" || attId === "TAG_DESC") return true; if (!selectedRow?.TAG_NO) return false; return editableFields.includes(attId); }, [selectedRow?.TAG_NO, editableFields] ); /** Convert a cell address like "M1" into {row:0,col:12}. */ const parseCellAddress = (addr: string): { row: number; col: number } | null => { if (!addr) return null; const match = addr.match(/^([A-Z]+)(\d+)$/); if (!match) return null; const [, colStr, rowStr] = match; let col = 0; for (let i = 0; i < colStr.length; i++) { col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); } col -= 1; const row = parseInt(rowStr, 10) - 1; return { row, col }; }; // Auto‑select first template React.useEffect(() => { if (normalizedTemplates.length && !selectedTemplateId) { setSelectedTemplateId(normalizedTemplates[0].TMPL_ID); } }, [normalizedTemplates, selectedTemplateId]); /* ------------------------- init spread ------------------------- */ const initSpread = React.useCallback( (spread: GC.Spread.Sheets.Workbook | undefined) => { if (!spread || !selectedTemplate || !selectedRow) return; setCurrentSpread(spread); setHasChanges(false); // Pick content JSON and data‑sheet mapping const contentJson = selectedTemplate.SPR_LST_SETUP?.CONTENT ?? selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT; const dataSheets = selectedTemplate.SPR_LST_SETUP?.DATA_SHEETS ?? selectedTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS; if (!contentJson) return; // Prepare shared styles once const editableStyle = new GC.Spread.Sheets.Style(); editableStyle.backColor = "#f0fdf4"; editableStyle.locked = false; const readOnlyStyle = new GC.Spread.Sheets.Style(); readOnlyStyle.backColor = "#f9fafb"; readOnlyStyle.foreColor = "#6b7280"; readOnlyStyle.locked = true; const jsonObj = typeof contentJson === "string" ? JSON.parse(contentJson) : contentJson; const sheet = spread.getActiveSheet(); /* -------- batch load + style -------- */ sheet.suspendPaint(); sheet.suspendCalcService(true); try { spread.fromJSON(jsonObj); sheet.options.isProtected = false; const mappings: Array<{ attId: string; cellAddress: string; isEditable: boolean }> = []; if (dataSheets?.length) { dataSheets.forEach((ds) => { ds.MAP_CELL_ATT?.forEach(({ ATT_ID, IN }) => { if (!IN) return; const pos = parseCellAddress(IN); if (!pos) return; const editable = isFieldEditable(ATT_ID); mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: editable }); }); }); } // Apply values + style in chunks for large templates const CHUNK = 500; let idx = 0; const applyChunk = () => { const end = Math.min(idx + CHUNK, mappings.length); for (; idx < end; idx++) { const { attId, cellAddress, isEditable } = mappings[idx]; const pos = parseCellAddress(cellAddress)!; if (selectedRow[attId] !== undefined && selectedRow[attId] !== null) { sheet.setValue(pos.row, pos.col, selectedRow[attId]); } sheet.setStyle(pos.row, pos.col, isEditable ? editableStyle : readOnlyStyle); } if (idx < mappings.length) { requestAnimationFrame(applyChunk); } else { // enable protection & events after styling done sheet.options.isProtected = true; sheet.options.protectionOptions = { allowSelectLockedCells: true, allowSelectUnlockedCells: true, } as any; // Cell/value change events sheet.bind(GC.Spread.Sheets.Events.ValueChanged, () => setHasChanges(true)); sheet.bind(GC.Spread.Sheets.Events.CellChanged, () => setHasChanges(true)); // Prevent editing read‑only fields sheet.bind( GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { const map = mappings.find((m) => { const pos = parseCellAddress(m.cellAddress); return pos && pos.row === info.row && pos.col === info.col; }); if (map && !map.isEditable) { toast.warning(`${map.attId} field is read‑only`); info.cancel = true; } } ); setCellMappings(mappings); sheet.resumeCalcService(false); sheet.resumePaint(); } }; applyChunk(); } catch (err) { console.error(err); toast.error("Failed to load template"); sheet.resumeCalcService(false); sheet.resumePaint(); } }, [selectedTemplate, selectedRow, isFieldEditable] ); /* ------------------------- handlers ------------------------- */ const handleTemplateChange = (id: string) => { setSelectedTemplateId(id); setHasChanges(false); if (currentSpread) { // re‑init after a short tick so component remounts SpreadSheets setTimeout(() => initSpread(currentSpread), 50); } }; const handleSaveChanges = React.useCallback(async () => { if (!currentSpread || !hasChanges || !selectedRow) { toast.info("No changes to save"); return; } setIsPending(true); try { const sheet = currentSpread.getActiveSheet(); const payload: Record = { ...selectedRow }; cellMappings.forEach((m) => { if (m.isEditable) { const pos = parseCellAddress(m.cellAddress); if (pos) payload[m.attId] = sheet.getValue(pos.row, pos.col); } }); payload.TAG_NO = selectedRow.TAG_NO; // never change TAG_NO const { success, message } = await updateFormDataInDB( formCode, contractItemId, payload ); if (!success) { toast.error(message); return; } toast.success("Changes saved successfully!"); onUpdateSuccess?.({ ...selectedRow, ...payload }); setHasChanges(false); } catch (err) { console.error(err); toast.error("An unexpected error occurred while saving"); } finally { setIsPending(false); } }, [currentSpread, hasChanges, selectedRow, cellMappings, formCode, contractItemId, onUpdateSuccess]); /* ------------------------- render ------------------------- */ if (!isOpen) return null; return ( SEDP Template – {formCode} {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || "N/A"}`} {hasChanges && • Unsaved changes}
Editable fields Read‑only fields {!!cellMappings.length && ( {cellMappings.filter((m) => m.isEditable).length} of {cellMappings.length} fields editable )}
{/* Template selector */} {normalizedTemplates.length > 1 && (
({normalizedTemplates.length} templates available)
)} {/* Spreadsheet */}
{selectedTemplate && isClient ? ( ) : (
{!isClient ? ( <> Loading... ) : ( "No template available" )}
)}
{/* footer */} {hasChanges && ( )}
); }