import * as z from "zod"; const extraSheetId = z.string().min(1).max(128); export const extraSheetSchema = z.strictObject({ columns: z.array(z.strictObject({ id: extraSheetId, label: z.string().trim().min(1).max(120), })).min(1), rows: z.array(z.strictObject({ id: extraSheetId, cells: z.record(z.string(), z.string().max(10_000)), })).min(1), }).superRefine((sheet, context) => { const columnIds = new Set(sheet.columns.map((column) => column.id)); if (columnIds.size !== sheet.columns.length) { context.addIssue({ code: "custom", path: ["columns"], message: "รหัสคอลัมน์ต้องไม่ซ้ำกัน" }); } const columnLabels = new Set(sheet.columns.map((column) => column.label.toLocaleLowerCase())); if (columnLabels.size !== sheet.columns.length) { context.addIssue({ code: "custom", path: ["columns"], message: "ชื่อคอลัมน์ต้องไม่ซ้ำกัน" }); } const rowIds = new Set(sheet.rows.map((row) => row.id)); if (rowIds.size !== sheet.rows.length) { context.addIssue({ code: "custom", path: ["rows"], message: "รหัสแถวต้องไม่ซ้ำกัน" }); } sheet.rows.forEach((row, rowIndex) => { Object.keys(row.cells).forEach((columnId) => { if (!columnIds.has(columnId)) { context.addIssue({ code: "custom", path: ["rows", rowIndex, "cells", columnId], message: "ไม่พบคอลัมน์ของเซลล์นี้", }); } }); }); }); export type ExtraSheet = z.output; export type ExtraSheetCell = { value: string }; export type ExtraSheetMatrix = Array>; export function nextExtraSheetColumnLabel(columns: ExtraSheet["columns"]): string { const labels = new Set(columns.map((column) => column.label.toLocaleLowerCase())); let index = columns.length + 1; while (labels.has(`คอลัมน์ ${index}`.toLocaleLowerCase())) index += 1; return `คอลัมน์ ${index}`; } export function createExtraSheet(createId: (prefix: string) => string): ExtraSheet { const columns = [1, 2].map((index) => ({ id: createId("extra-sheet-column"), label: `คอลัมน์ ${index}`, })); return { columns, rows: [1, 2].map(() => ({ id: createId("extra-sheet-row"), cells: Object.fromEntries(columns.map((column) => [column.id, ""])), })), }; } export function extraSheetToMatrix(sheet: ExtraSheet): ExtraSheetMatrix { return sheet.rows.map((row) => sheet.columns.map((column) => ({ value: row.cells[column.id] ?? "" })), ); } export function matrixToExtraSheet( sheet: ExtraSheet, matrix: ExtraSheetMatrix, createId: (prefix: string) => string, ): ExtraSheet { const columnCount = Math.max( sheet.columns.length, ...matrix.map((row) => row.length), ); const columns = [...sheet.columns]; while (columns.length < columnCount) { columns.push({ id: createId("extra-sheet-column"), label: nextExtraSheetColumnLabel(columns), }); } const rowCount = Math.max(sheet.rows.length, matrix.length); const rows = Array.from({ length: rowCount }, (_, rowIndex) => ({ id: sheet.rows[rowIndex]?.id ?? createId("extra-sheet-row"), cells: Object.fromEntries(columns.map((column, columnIndex) => [ column.id, String(matrix[rowIndex]?.[columnIndex]?.value ?? ""), ])), })); return { columns, rows }; } export function addExtraSheetColumn( sheet: ExtraSheet, label: string, id: string, ): ExtraSheet { return { columns: [...sheet.columns, { id, label }], rows: sheet.rows.map((row) => ({ ...row, cells: { ...row.cells, [id]: "" } })), }; } export function addExtraSheetRow(sheet: ExtraSheet, id: string): ExtraSheet { return { ...sheet, rows: [...sheet.rows, { id, cells: Object.fromEntries(sheet.columns.map((column) => [column.id, ""])), }], }; } export function moveExtraSheetColumn( sheet: ExtraSheet, from: number, to: number, ): ExtraSheet { if (to < 0 || to >= sheet.columns.length || from === to) return sheet; const columns = [...sheet.columns]; const [column] = columns.splice(from, 1); columns.splice(to, 0, column); return { ...sheet, columns }; } export function moveExtraSheetRow( sheet: ExtraSheet, from: number, to: number, ): ExtraSheet { if (to < 0 || to >= sheet.rows.length || from === to) return sheet; const rows = [...sheet.rows]; const [row] = rows.splice(from, 1); rows.splice(to, 0, row); return { ...sheet, rows }; } export function deleteExtraSheetColumn(sheet: ExtraSheet, index: number): ExtraSheet { if (sheet.columns.length === 1) return sheet; const columnId = sheet.columns[index]?.id; if (!columnId) return sheet; return { columns: sheet.columns.filter((_, columnIndex) => columnIndex !== index), rows: sheet.rows.map((row) => ({ ...row, cells: Object.fromEntries(Object.entries(row.cells).filter(([key]) => key !== columnId)), })), }; } export function deleteExtraSheetRow(sheet: ExtraSheet, index: number): ExtraSheet { if (sheet.rows.length === 1) return sheet; return { ...sheet, rows: sheet.rows.filter((_, rowIndex) => rowIndex !== index) }; }