"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 (
);
}