diff options
Diffstat (limited to 'components/form-data')
| -rw-r--r-- | components/form-data/form-data-table-columns.tsx | 138 | ||||
| -rw-r--r-- | components/form-data/form-data-table.tsx | 545 | ||||
| -rw-r--r-- | components/form-data/update-form-sheet.tsx | 239 |
3 files changed, 922 insertions, 0 deletions
diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx new file mode 100644 index 00000000..d44616f8 --- /dev/null +++ b/components/form-data/form-data-table-columns.tsx @@ -0,0 +1,138 @@ +import type { ColumnDef, Row } from "@tanstack/react-table" +import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header" +import { Button } from "@/components/ui/button" +import { Ellipsis } from "lucide-react" +import { formatDate } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +/** row 액션 관련 타입 */ +export interface DataTableRowAction<TData> { + row: Row<TData> + type: "open" | "edit" | "update" +} + +/** 컬럼 타입 (필요에 따라 확장) */ +export type ColumnType = "STRING" | "NUMBER" | "LIST" + + +export interface DataTableColumnJSON { + key: string + /** 실제 Excel 등에서 구분용으로 쓰이는 label (고정) */ + label: string + + /** UI 표시용 label (예: 단위를 함께 표시) */ + displayLabel?: string + + type: ColumnType + options?: string[] + uom?: string +} +/** + * getColumns 함수에 필요한 props + * - TData: 테이블에 표시할 행(Row)의 타입 + */ +interface GetColumnsProps<TData> { + columnsJSON: DataTableColumnJSON[] + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<TData> | null>> +} + +/** + * getColumns 함수 + * 1) columnsJSON 배열을 순회하면서 accessorKey / header / cell 등을 설정 + * 2) 마지막에 "Action" 칼럼(예: update 버튼) 추가 + */ +export function getColumns<TData extends object>({ + columnsJSON, + setRowAction, +}: GetColumnsProps<TData>): ColumnDef<TData>[] { + + // (1) 기본 컬럼들 + const baseColumns: ColumnDef<TData>[] = columnsJSON.map((col) => ({ + accessorKey: col.key, + header: ({ column }) => ( + <ClientDataTableColumnHeaderSimple + column={column} + title={col.displayLabel || col.label} + /> + ), + + meta: { + excelHeader: col.label, + minWidth: 80, + paddingFactor: 1.2, + maxWidth: col.key ==="tagNumber"?120:150, + }, + // (2) 실제 셀(cell) 렌더링: type에 따라 분기 가능 + cell: ({ row }) => { + const cellValue = row.getValue(col.key) + + // 데이터 타입별 처리 + switch (col.type) { + case "NUMBER": + // 예: number인 경우 콤마 등 표시 + return <div>{cellValue ? Number(cellValue).toLocaleString() : ""}</div> + + // case "date": + // // 예: 날짜 포맷팅 + // // 실제론 dayjs / date-fns 등으로 포맷 + // if (!cellValue) return <div></div> + // const dateString = cellValue as string + // if (!dateString) return null + // return formatDate(new Date(dateString)) + + case "LIST": + // 예: select인 경우 label만 표시 + return <div>{String(cellValue ?? "")}</div> + + case "STRING": + default: + return <div>{String(cellValue ?? "")}</div> + } + }, + })) + + // (3) 액션 칼럼 - update 버튼 예시 + const actionColumn: ColumnDef<TData> = { + id: "update", + header: "", + cell: ({ row }) => ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-40"> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "update" })} + > + Edit + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ), + size:40, + meta:{ + maxWidth:40 + }, + enablePinning: true, + } + + // (4) 최종 반환 + return [...baseColumns, actionColumn] +}
\ No newline at end of file diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx new file mode 100644 index 00000000..14fff12e --- /dev/null +++ b/components/form-data/form-data-table.tsx @@ -0,0 +1,545 @@ +"use client" + +import * as React from "react" +import { useParams } from "next/navigation" +import { useTranslation } from "@/i18n/client" + +import { ClientDataTable } from "../client-data-table/data-table" +import { + getColumns, + DataTableRowAction, + DataTableColumnJSON, + ColumnType, +} from "./form-data-table-columns" + +import type { DataTableAdvancedFilterField } from "@/types/table" +import { Button } from "../ui/button" +import { Download, Loader, Save, Upload } from "lucide-react" +import { toast } from "sonner" +import { syncMissingTags, updateFormDataInDB } from "@/lib/forms/services" +import { UpdateTagSheet } from "./update-form-sheet" + +import ExcelJS from "exceljs" +import { saveAs } from "file-saver" + +interface GenericData { + [key: string]: any +} + +export interface DynamicTableProps { + dataJSON: GenericData[] + columnsJSON: DataTableColumnJSON[] + contractItemId: number + formCode: string +} + +export default function DynamicTable({ + dataJSON, + columnsJSON, + contractItemId, + formCode, +}: DynamicTableProps) { + const params = useParams() + const lng = (params?.lng as string) || "ko" + const { t } = useTranslation(lng, "translation") + + const [rowAction, setRowAction] = React.useState<DataTableRowAction<GenericData> | null>(null) + const [tableData, setTableData] = React.useState<GenericData[]>(() => dataJSON) + const [isPending, setIsPending] = React.useState(false) + const [isSaving, setIsSaving] = React.useState(false) + + // Reference to the table instance + const tableRef = React.useRef(null) + + const columns = React.useMemo( + () => getColumns<GenericData>({ columnsJSON, setRowAction }), + [columnsJSON, setRowAction] + ) + + function mapColumnTypeToAdvancedFilterType( + columnType: ColumnType + ): DataTableAdvancedFilterField<GenericData>["type"] { + switch (columnType) { + case "STRING": + return "text" + case "NUMBER": + return "number" + case "LIST": + // "select"로 하셔도 되고 "multi-select"로 하셔도 됩니다. + return "select" + // 그 외 다른 타입들도 적절히 추가 매핑 + default: + // 예: 못 매핑한 경우 기본적으로 "text" 적용 + return "text" + } + } + + const advancedFilterFields = React.useMemo<DataTableAdvancedFilterField<GenericData>[]>( + () => { + return columnsJSON.map((col) => ({ + id: col.key, + label: col.label, + type: mapColumnTypeToAdvancedFilterType(col.type), + options: + col.type === "LIST" + ? col.options?.map((v) => ({ label: v, value: v })) + : undefined, + })) + }, + [columnsJSON] + ) + + // 1) 태그 불러오기 (기존) + async function handleSyncTags() { + try { + setIsPending(true) + const result = await syncMissingTags(contractItemId, formCode) + + // Prepare the toast messages based on what changed + const changes = [] + if (result.createdCount > 0) changes.push(`${result.createdCount}건 태그 생성`) + if (result.updatedCount > 0) changes.push(`${result.updatedCount}건 태그 업데이트`) + if (result.deletedCount > 0) changes.push(`${result.deletedCount}건 태그 삭제`) + + if (changes.length > 0) { + // If any changes were made, show success message and reload + toast.success(`동기화 완료: ${changes.join(', ')}`) + location.reload() + } else { + // If no changes were made, show an info message + toast.info("변경사항이 없습니다. 모든 태그가 최신 상태입니다.") + } + } catch (err) { + console.error(err) + toast.error("태그 동기화 중 에러가 발생했습니다.") + } finally { + setIsPending(false) + } + } + // 2) Excel Import (새로운 기능) + async function handleImportExcel(e: React.ChangeEvent<HTMLInputElement>) { + const file = e.target.files?.[0] + if (!file) return + + try { + setIsPending(true) + + // 기존 테이블 데이터의 tagNumber 목록 (이미 있는 태그) + const existingTagNumbers = new Set(tableData.map((d) => d.tagNumber)) + + const workbook = new ExcelJS.Workbook() + const arrayBuffer = await file.arrayBuffer() + await workbook.xlsx.load(arrayBuffer) + + const worksheet = workbook.worksheets[0] + + // (A) 헤더 파싱 (Error 열 추가 전에 먼저 체크) + const headerRow = worksheet.getRow(1) + const headerRowValues = headerRow.values as ExcelJS.CellValue[] + + // 디버깅용 로그 + console.log("원본 헤더 값:", headerRowValues) + + // Excel의 헤더와 columnsJSON의 label 매핑 생성 + // Excel의 행은 1부터 시작하므로 headerRowValues[0]은 undefined + const headerToIndexMap = new Map<string, number>() + for (let i = 1; i < headerRowValues.length; i++) { + const headerValue = String(headerRowValues[i] || "").trim() + if (headerValue) { + headerToIndexMap.set(headerValue, i) + } + } + + // (B) 헤더 검사 + let headerErrorMessage = "" + + // (1) "columnsJSON에 있는데 엑셀에 없는" 라벨 + columnsJSON.forEach((col) => { + const label = col.label + if (!headerToIndexMap.has(label)) { + headerErrorMessage += `Column "${label}" is missing. ` + } + }) + + // (2) "엑셀에는 있는데 columnsJSON에 없는" 라벨 검사 + headerToIndexMap.forEach((index, headerLabel) => { + const found = columnsJSON.some((col) => col.label === headerLabel) + if (!found) { + headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. ` + } + }) + + // (C) 이제 Error 열 추가 + const lastColIndex = worksheet.columnCount + 1 + worksheet.getRow(1).getCell(lastColIndex).value = "Error" + + // 헤더 에러가 있으면 기록 후 다운로드하고 중단 + if (headerErrorMessage) { + headerRow.getCell(lastColIndex).value = headerErrorMessage.trim() + + const outBuffer = await workbook.xlsx.writeBuffer() + saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`) + + toast.error(`Header mismatch found. Please check downloaded file.`) + return + } + + // -- 여기까지 왔다면, 헤더는 문제 없음 -- + + // 컬럼 키-인덱스 매핑 생성 (실제 데이터 행 파싱용) + // columnsJSON의 key와 Excel 열 인덱스 간의 매핑 + const keyToIndexMap = new Map<string, number>() + columnsJSON.forEach((col) => { + const index = headerToIndexMap.get(col.label) + if (index !== undefined) { + keyToIndexMap.set(col.key, index) + } + }) + + // 데이터 파싱 + const importedData: GenericData[] = [] + const lastRowNumber = worksheet.lastRow?.number || 1 + let errorCount = 0 + + // 실제 데이터 행 파싱 + for (let rowNum = 2; rowNum <= lastRowNumber; rowNum++) { + const row = worksheet.getRow(rowNum) + const rowValues = row.values as ExcelJS.CellValue[] + if (!rowValues || rowValues.length <= 1) continue // 빈 행 스킵 + + let errorMessage = "" + const rowObj: Record<string, any> = {} + + // 각 열에 대해 처리 + columnsJSON.forEach((col) => { + const colIndex = keyToIndexMap.get(col.key) + if (colIndex === undefined) return + + const cellValue = rowValues[colIndex] ?? "" + let stringVal = String(cellValue).trim() + + // 타입별 검사 + switch (col.type) { + case "STRING": + if (!stringVal && col.key === "tagNumber") { + errorMessage += `[${col.label}] is empty. ` + } + rowObj[col.key] = stringVal + break + + case "NUMBER": + if (stringVal) { + const num = parseFloat(stringVal) + if (isNaN(num)) { + errorMessage += `[${col.label}] '${stringVal}' is not a valid number. ` + } else { + rowObj[col.key] = num + } + } else { + rowObj[col.key] = null + } + break + + case "LIST": + if (stringVal && col.options && !col.options.includes(stringVal)) { + errorMessage += `[${col.label}] '${stringVal}' not in ${col.options.join(", ")}. ` + } + rowObj[col.key] = stringVal + break + + default: + rowObj[col.key] = stringVal + break + } + }) + + // tagNumber 검사 + const tagNum = rowObj["tagNumber"] + if (!tagNum) { + errorMessage += `No tagNumber found. ` + } else if (!existingTagNumbers.has(tagNum)) { + errorMessage += `TagNumber '${tagNum}' is not in current data. ` + } + + if (errorMessage) { + row.getCell(lastColIndex).value = errorMessage.trim() + errorCount++ + } else { + importedData.push(rowObj) + } + } + + // 에러가 있으면 재다운로드 후 import 중단 + if (errorCount > 0) { + const outBuffer = await workbook.xlsx.writeBuffer() + saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`) + toast.error(`There are ${errorCount} error row(s). Please check downloaded file.`) + return + } + + // 에러 없으니 tableData 병합 + setTableData((prev) => { + const newDataMap = new Map<string, GenericData>() + + // 기존 데이터를 맵에 추가 + prev.forEach((item) => { + if (item.tagNumber) { + newDataMap.set(item.tagNumber, { ...item }) + } + }) + + // 임포트 데이터로 기존 데이터 업데이트 + importedData.forEach((item) => { + const tag = item.tagNumber + if (!tag) return + const oldItem = newDataMap.get(tag) || {} + newDataMap.set(tag, { ...oldItem, ...item }) + }) + + return Array.from(newDataMap.values()) + }) + + toast.success(`Imported ${importedData.length} rows successfully.`) + } catch (err) { + console.error("Excel import error:", err) + toast.error("Excel import failed.") + } finally { + setIsPending(false) + e.target.value = "" + } + } + + // 3) Save -> 서버에 전체 tableData를 저장 + async function handleSave() { + try { + setIsSaving(true) + + // 유효성 검사 + const invalidData = tableData.filter(item => !item.tagNumber?.trim()) + if (invalidData.length > 0) { + toast.error(`태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`) + return + } + + // 서버 액션 호출 + const result = await updateFormDataInDB(formCode, contractItemId, tableData) + + if (result.success) { + toast.success(result.message) + } else { + toast.error(result.message) + } + } catch (err) { + console.error("Save error:", err) + toast.error("데이터 저장 중 오류가 발생했습니다.") + } finally { + setIsSaving(false) + } + } + + // 4) NEW: Excel Export with data validation for select columns using a hidden validation sheet + async function handleExportExcel() { + try { + setIsPending(true) + + // Create a new workbook + const workbook = new ExcelJS.Workbook() + + // 데이터 시트 생성 + const worksheet = workbook.addWorksheet("Data") + + // 유효성 검사용 숨김 시트 생성 + const validationSheet = workbook.addWorksheet("ValidationData") + validationSheet.state = 'hidden' // 시트 숨김 처리 + + // 1. 유효성 검사 시트에 select 옵션 추가 + const selectColumns = columnsJSON.filter(col => + col.type === "LIST" && col.options && col.options.length > 0 + ) + + // 유효성 검사 범위 저장 맵 (컬럼 키 -> 유효성 검사 범위) + const validationRanges = new Map<string, string>() + + selectColumns.forEach((col, idx) => { + const colIndex = idx + 1 + const colLetter = validationSheet.getColumn(colIndex).letter + + // 헤더 추가 (컬럼 레이블) + validationSheet.getCell(`${colLetter}1`).value = col.label + + // 옵션 추가 + if (col.options) { + col.options.forEach((option, optIdx) => { + validationSheet.getCell(`${colLetter}${optIdx + 2}`).value = option + }) + + // 유효성 검사 범위 저장 (ValidationData!$A$2:$A$4 형식) + validationRanges.set( + col.key, + `ValidationData!${colLetter}$2:${colLetter}${col.options.length + 1}` + ) + } + }) + + // 2. 데이터 시트에 헤더 추가 + const headers = columnsJSON.map(col => col.label) + worksheet.addRow(headers) + + // 헤더 스타일 적용 + const headerRow = worksheet.getRow(1) + headerRow.font = { bold: true } + headerRow.alignment = { horizontal: 'center' } + headerRow.eachCell((cell) => { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFCCCCCC' } + } + }) + + // 3. 데이터 행 추가 + tableData.forEach(row => { + const rowValues = columnsJSON.map(col => { + const value = row[col.key] + return value !== undefined && value !== null ? value : '' + }) + worksheet.addRow(rowValues) + }) + + // 4. 데이터 유효성 검사 적용 + const maxRows = 5000 // 데이터 유효성 검사를 적용할 최대 행 수 + + columnsJSON.forEach((col, idx) => { + if (col.type === "LIST" && validationRanges.has(col.key)) { + const colLetter = worksheet.getColumn(idx + 1).letter + const validationRange = validationRanges.get(col.key)! + + // 유효성 검사 정의 + const validation = { + type: 'list' as const, + allowBlank: true, + formulae: [validationRange], + showErrorMessage: true, + errorStyle: 'warning' as const, + errorTitle: '유효하지 않은 값', + error: '목록에서 값을 선택해주세요.' + } + + // 모든 데이터 행에 유효성 검사 적용 (최대 maxRows까지) + for (let rowIdx = 2; rowIdx <= Math.min(tableData.length + 1, maxRows); rowIdx++) { + worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation = validation + } + + // 빈 행에도 적용 (최대 maxRows까지) + if (tableData.length + 1 < maxRows) { + for (let rowIdx = tableData.length + 2; rowIdx <= maxRows; rowIdx++) { + worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation = validation + } + } + } + }) + + // 5. 컬럼 너비 자동 조정 + columnsJSON.forEach((col, idx) => { + const column = worksheet.getColumn(idx + 1) + + // 최적 너비 계산 + let maxLength = col.label.length + tableData.forEach(row => { + const value = row[col.key] + if (value !== undefined && value !== null) { + const valueLength = String(value).length + if (valueLength > maxLength) { + maxLength = valueLength + } + } + }) + + // 너비 설정 (최소 10, 최대 50) + column.width = Math.min(Math.max(maxLength + 2, 10), 50) + }) + + // 6. 파일 다운로드 + const buffer = await workbook.xlsx.writeBuffer() + saveAs(new Blob([buffer]), `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`) + + toast.success("Excel 내보내기 완료!") + } catch (err) { + console.error("Excel export error:", err) + toast.error("Excel 내보내기 실패.") + } finally { + setIsPending(false) + } + } + + return ( + <> + <ClientDataTable + data={tableData} + columns={columns} + advancedFilterFields={advancedFilterFields} + // tableRef={tableRef} + > + {/* 버튼 그룹 */} + <div className="flex items-center gap-2"> + {/* 태그 불러오기 버튼 */} + <Button variant="default" size="sm" onClick={handleSyncTags} disabled={isPending}> + {isPending && <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />} + Sync Tags + </Button> + + {/* IMPORT 버튼 (파일 선택) */} + <Button asChild variant="outline" size="sm" disabled={isPending}> + <label> + <Upload className="size-4" /> + Import + <input + type="file" + accept=".xlsx,.xls" + onChange={handleImportExcel} + style={{ display: "none" }} + /> + </label> + </Button> + + {/* EXPORT 버튼 (새로 추가) */} + <Button variant="outline" size="sm" onClick={handleExportExcel} disabled={isPending}> + <Download className="mr-2 size-4" /> + Export Template + </Button> + + {/* SAVE 버튼 */} + <Button + variant="outline" + size="sm" + onClick={handleSave} + disabled={isPending || isSaving} + > + {isSaving ? ( + <> + <Loader className="mr-2 size-4 animate-spin" /> + 저장 중... + </> + ) : ( + <> + <Save className="mr-2 size-4" /> + Save + </> + )} + </Button> + </div> + </ClientDataTable> + + <UpdateTagSheet + open={rowAction?.type === "update"} + onOpenChange={(open) => { + if (!open) setRowAction(null) + }} + columns={columnsJSON} + rowData={rowAction?.row.original ?? null} + formCode={formCode} + contractItemId={contractItemId} + /> + </> + ) +}
\ No newline at end of file diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx new file mode 100644 index 00000000..d5f7d21b --- /dev/null +++ b/components/form-data/update-form-sheet.tsx @@ -0,0 +1,239 @@ +"use client" + +import * as React from "react" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { Loader } from "lucide-react" +import { toast } from "sonner" + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/form" +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select" + +import { DataTableColumnJSON } from "./form-data-table-columns" +import { updateFormDataInDB } from "@/lib/forms/services" + +interface UpdateTagSheetProps extends React.ComponentPropsWithoutRef<typeof Sheet> { + open: boolean + onOpenChange: (open: boolean) => void + columns: DataTableColumnJSON[] + rowData: Record<string, any> | null + formCode: string + contractItemId: number + /** 업데이트 성공 시 호출될 콜백 */ + onUpdateSuccess?: (updatedValues: Record<string, any>) => void +} + +export function UpdateTagSheet({ + open, + onOpenChange, + columns, + rowData, + formCode, + contractItemId, + onUpdateSuccess, + ...props +}: UpdateTagSheetProps) { + const [isPending, startTransition] = React.useTransition() + + // 1) zod 스키마 + const dynamicSchema = React.useMemo(() => { + const shape: Record<string, z.ZodType<any>> = {} + for (const col of columns) { + if (col.type === "NUMBER") { + shape[col.key] = z + .union([z.coerce.number(), z.nan()]) + .transform((val) => (isNaN(val) ? undefined : val)) + .optional() + } else { + shape[col.key] = z.string().optional() + } + } + return z.object(shape) + }, [columns]) + + // 2) form init + const form = useForm({ + resolver: zodResolver(dynamicSchema), + defaultValues: React.useMemo(() => { + if (!rowData) return {} + const defaults: Record<string, any> = {} + for (const col of columns) { + defaults[col.key] = rowData[col.key] ?? "" + } + return defaults + }, [rowData, columns]), + }) + + React.useEffect(() => { + if (!rowData) { + form.reset({}) + return + } + const defaults: Record<string, any> = {} + for (const col of columns) { + defaults[col.key] = rowData[col.key] ?? "" + } + form.reset(defaults) + }, [rowData, columns, form]) + + async function onSubmit(values: Record<string, any>) { + startTransition(async () => { + const { success, message } = await updateFormDataInDB(formCode, contractItemId, values) + if (!success) { + toast.error(message) + return + } + toast.success("Updated successfully!") + + // (A) 수정된 값(폼 데이터)을 부모 콜백에 전달 + onUpdateSuccess?.({ + // rowData(원본)와 values를 합쳐서 최종 "수정된 row"를 만든다. + // tagNumber는 기존 그대로 + ...rowData, + ...values, + tagNumber: rowData?.tagNumber, + }) + + onOpenChange(false) + }) + } + + return ( + <Sheet open={open} onOpenChange={onOpenChange} {...props}> + <SheetContent className="sm:max-w-xl md:max-w-3xl lg:max-w-4xl xl:max-w-5xl flex flex-col"> + <SheetHeader className="text-left"> + <SheetTitle>Update Row</SheetTitle> + <SheetDescription> + Modify the fields below and save changes + </SheetDescription> + </SheetHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4"> + <div className="overflow-y-auto max-h-[80vh] flex-1 pr-4 -mr-4"> + <div className="flex flex-col gap-4 pt-2"> + {columns.map((col) => { + const isTagNumberField = col.key === "tagNumber" || col.key === "tagDescription" + return ( + <FormField + key={col.key} + control={form.control} + name={col.key} + render={({ field }) => { + switch (col.type) { + case "NUMBER": + return ( + <FormItem> + <FormLabel>{col.displayLabel}</FormLabel> + <FormControl> + <Input + type="number" + readOnly={isTagNumberField} + onChange={(e) => { + const num = parseFloat(e.target.value) + field.onChange(isNaN(num) ? "" : num) + }} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + ) + + case "LIST": + return ( + <FormItem> + <FormLabel>{col.label}</FormLabel> + <Select + disabled={isTagNumberField} + value={field.value ?? ""} + onValueChange={(val) => field.onChange(val)} + > + <SelectTrigger> + <SelectValue placeholder="Select an option" /> + </SelectTrigger> + <SelectContent> + {col.options?.map((opt) => ( + <SelectItem key={opt} value={opt}> + {opt} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + ) + + // case "date": + // return ( + // <FormItem> + // <FormLabel>{col.label}</FormLabel> + // <FormControl> + // <Input + // type="date" + // readOnly={isTagNumberField} + // onChange={field.onChange} + // value={field.value ?? ""} + // /> + // </FormControl> + // <FormMessage /> + // </FormItem> + // ) + + case "STRING": + default: + return ( + <FormItem> + <FormLabel>{col.label}</FormLabel> + <FormControl> + <Input readOnly={isTagNumberField} {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + ) + } + }} + /> + ) + })} + + </div> + </div> + + <SheetFooter className="gap-2 pt-2"> + <SheetClose asChild> + <Button type="button" variant="outline"> + Cancel + </Button> + </SheetClose> + + <Button type="submit" disabled={isPending}> + {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />} + Save + </Button> + </SheetFooter> + </form> + </Form> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
