From 4e63d8427d26d0d1b366ddc53650e15f3481fc75 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 24 Jun 2025 01:44:03 +0000 Subject: (대표님/최겸) 20250624 작업사항 10시43분 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/reg-eval-criteria-columns.tsx | 364 +++++++++++++ .../table/reg-eval-criteria-delete-dialog.tsx | 147 ++++++ .../table/reg-eval-criteria-form-sheet.tsx | 586 +++++++++++++++++++++ .../reg-eval-criteria-table-toolbar-actions.tsx | 161 ++++++ .../table/reg-eval-criteria-table.tsx | 189 +++++++ 5 files changed, 1447 insertions(+) create mode 100644 lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx create mode 100644 lib/evaluation-criteria/table/reg-eval-criteria-delete-dialog.tsx create mode 100644 lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx create mode 100644 lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx create mode 100644 lib/evaluation-criteria/table/reg-eval-criteria-table.tsx (limited to 'lib/evaluation-criteria/table') diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx new file mode 100644 index 00000000..7367fabb --- /dev/null +++ b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx @@ -0,0 +1,364 @@ +'use client'; + +/* IMPORT */ +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { DataTableColumnHeaderSimple } from '@/components/data-table/data-table-column-simple-header'; +import { Dispatch, SetStateAction } from 'react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { PenToolIcon, TrashIcon } from 'lucide-react'; +import { + REG_EVAL_CRITERIA_CATEGORY, + REG_EVAL_CRITERIA_ITEM, + REG_EVAL_CRITERIA_CATEGORY2, + type RegEvalCriteriaView, +} from '@/db/schema'; +import { type ColumnDef } from '@tanstack/react-table'; +import { type DataTableRowAction } from '@/types/table'; + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +interface GetColumnsProps { + setRowAction: Dispatch | null>>, +}; + +// ---------------------------------------------------------------------------------------------------- + +/* FUNCTION FOR GETTING COLUMNS SETTING */ +function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + + // [1] SELECT COLUMN - CHECKBOX + const selectColumn: ColumnDef = { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="select-all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="select-row" + className="translate-y-0.5" + /> + ), + enableSorting: false, + enableHiding: false, + size:40, + }; + + // [2] CRITERIA COLUMNS + const criteriaColumns: ColumnDef[] = [ + { + accessorKey: 'category', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue('category'); + const label = REG_EVAL_CRITERIA_CATEGORY.find(item => item.value === value)?.label ?? value; + return ( + + {label} + + ); + }, + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Category', + type: 'select', + }, + }, + { + accessorKey: 'scoreCategory', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue('scoreCategory'); + const label = REG_EVAL_CRITERIA_CATEGORY2.find(item => item.value === value)?.label ?? value; + return ( + + {label} + + ); + }, + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Score Category', + type: 'select', + }, + }, + { + accessorKey: 'item', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue('item'); + const label = REG_EVAL_CRITERIA_ITEM.find(item => item.value === value)?.label ?? value; + return ( + + {label} + + ); + }, + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Item', + type: 'select', + }, + }, + { + accessorKey: 'classification', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('classification')} +
+ ), + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Classification', + type: 'text', + }, + }, + { + accessorKey: 'range', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('range') || '-'} +
+ ), + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Range', + type: 'text', + }, + }, + { + accessorKey: 'detail', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('detail')} +
+ ), + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Detail', + type: 'text', + }, + }, + ]; + + // [3] SCORE COLUMNS + const scoreEquipColumns: ColumnDef[] = [ + { + accessorKey: 'scoreEquipShip', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue('scoreEquipShip'); + const displayValue = typeof value === 'string' + ? parseFloat(parseFloat(value).toFixed(2)).toString() + : '-'; + return ( +
+ {displayValue} +
+ ); + }, + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Equipment-Shipbuilding Score', + group: 'Equipment Score', + type: 'number', + }, + }, + { + accessorKey: 'scoreEquipMarine', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue('scoreEquipMarine'); + const displayValue = typeof value === 'string' + ? parseFloat(parseFloat(value).toFixed(2)).toString() + : '-'; + return ( +
+ {displayValue} +
+ ); + }, + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Equipment-Marine Engineering Score', + group: 'Equipment Score', + type: 'number', + }, + }, + ]; + const scoreBulkColumns: ColumnDef[] = [ + { + accessorKey: 'scoreBulkShip', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue('scoreBulkShip'); + const displayValue = typeof value === 'string' + ? parseFloat(parseFloat(value).toFixed(2)).toString() + : '-'; + return ( +
+ {displayValue} +
+ ); + }, + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Bulk-Shipbuiling Score', + group: 'Bulk Score', + type: 'number', + }, + }, + { + accessorKey: 'scoreBulkMarine', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue('scoreBulkMarine'); + const displayValue = typeof value === 'string' + ? parseFloat(parseFloat(value).toFixed(2)).toString() + : '-'; + return ( +
+ {displayValue} +
+ ); + }, + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Bulk-Marine Engineering Score', + group: 'Bulk Score', + type: 'number', + }, + }, + ]; + + // [4] REMARKS COLUMN + const remarksColumn: ColumnDef = { + accessorKey: 'remarks', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('remarks') || '-'} +
+ ), + enableSorting: true, + enableHiding: false, + meta: { + excelHeader: 'Remarks', + type: 'text', + }, + }; + + // [5] ACTIONS COLUMN - DROPDOWN MENU + const actionsColumn: ColumnDef = { + id: 'actions', + header: '작업', + enableHiding: false, + cell: function Cell({ row }) { + return ( + + + + + + setRowAction({ row, type: 'update' })} + > + + Modify Criteria + + setRowAction({ row, type: 'delete' })} + className="text-destructive" + > + + Delete Criteria + + + + ) + }, + size: 80, + }; + + return [ + selectColumn, + ...criteriaColumns, + { + id: 'score', + header: '배점', + columns: [ + { + id: 'scoreEquip', + header: '기자재', + columns: scoreEquipColumns, + }, + { + id: 'scoreBulk', + header: '벌크', + columns: scoreBulkColumns, + }, + ], + }, + remarksColumn, + actionsColumn, + ]; +}; + +// ---------------------------------------------------------------------------------------------------- + +/* EXPORT */ +export default getColumns; \ No newline at end of file diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-delete-dialog.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-delete-dialog.tsx new file mode 100644 index 00000000..aac7db29 --- /dev/null +++ b/lib/evaluation-criteria/table/reg-eval-criteria-delete-dialog.tsx @@ -0,0 +1,147 @@ +'use client'; + +/* IMPORT */ +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + getRegEvalCriteriaWithDetails, + removeRegEvalCriteria, +} from '../service'; +import { LoaderCircle } from 'lucide-react'; +import { toast } from 'sonner'; +import { + REG_EVAL_CRITERIA_CATEGORY, + REG_EVAL_CRITERIA_ITEM, + REG_EVAL_CRITERIA_SCORE_CATEGORY, + type RegEvalCriteriaView, + type RegEvalCriteriaWithDetails, +} from '@/db/schema'; +import { useEffect, useState } from 'react'; + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +interface RegEvalCriteriaDeleteDialogProps { + open: boolean, + onOpenChange: (open: boolean) => void, + criteriaViewData: RegEvalCriteriaView, + onSuccess: () => void, +} + +// ---------------------------------------------------------------------------------------------------- + +/* REGULAR EVALUATION CRITERIA DELETE DIALOG COMPONENT */ +function RegEvalCriteriaDeleteDialog(props: RegEvalCriteriaDeleteDialogProps) { + const { open, onOpenChange, criteriaViewData, onSuccess } = props; + const [isLoading, setIsLoading] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [targetData, setTargetData] = useState(); + + useEffect(() => { + const fetchData = async () => { + if (!criteriaViewData?.criteriaId) { + return; + } + setIsLoading(true); + try { + const result = await getRegEvalCriteriaWithDetails(criteriaViewData.criteriaId); + setTargetData(result); + } catch (error) { + console.error('Error in Loading Target Data for Deletion: ', error); + } finally { + setIsLoading(false); + } + } + fetchData(); + }, [criteriaViewData.criteriaId]); + + const handleDelete = async () => { + if (!criteriaViewData || !criteriaViewData.criteriaId) { + return; + } + + try { + setIsDeleting(true); + await removeRegEvalCriteria(criteriaViewData.criteriaId); + toast.success('평가 기준이 삭제되었습니다.'); + onSuccess(); + } catch (error) { + console.error('Error in Deleting Regular Evaluation Criteria: ', error); + toast.error( + error instanceof Error ? error.message : '삭제 중 오류가 발생했습니다.' + ); + } finally { + setIsDeleting(false); + } + } + + if (!criteriaViewData) { + return null; + } + + return ( + + + {isLoading ? ( +
+ +

Loading...

+
+ ) : ( + <> + + 협력업체 평가 기준 삭제 + + 정말로 이 협력업체 평가 기준을 삭제하시겠습니까? +
+
+ 삭제될 평가 기준: +
+ • 평가부문: {REG_EVAL_CRITERIA_CATEGORY.find((c) => c.value === criteriaViewData.category)?.label ?? '-'} +
+ • 점수구분: {REG_EVAL_CRITERIA_SCORE_CATEGORY.find((c) => c.value === criteriaViewData.scoreCategory)?.label ?? '-'} +
+ • 항목: {REG_EVAL_CRITERIA_ITEM.find((c) => c.value === criteriaViewData.item)?.label ?? '-'} +
+ • 구분: {criteriaViewData.classification || '-'} +
+ • 범위: {criteriaViewData.range || '-'} +
+
+ 이 작업은 되돌릴 수 없으며, 평가기준과 그에 속한 {targetData?.criteriaDetails.length}개의 평가항목도 함께 삭제됩니다. +
+
+ + + + + + )} +
+
+ ) +} + +// ---------------------------------------------------------------------------------------------------- + +/* EXPORT */ +export default RegEvalCriteriaDeleteDialog; \ No newline at end of file diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx new file mode 100644 index 00000000..d33e7d29 --- /dev/null +++ b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx @@ -0,0 +1,586 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +'use client'; + +/* IMPORT */ +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + createRegEvalCriteriaWithDetails, + getRegEvalCriteriaWithDetails, + modifyRegEvalCriteriaWithDetails, +} from '../service'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Plus, Trash2 } from 'lucide-react'; +import { + REG_EVAL_CRITERIA_CATEGORY, + REG_EVAL_CRITERIA_CATEGORY2, + REG_EVAL_CRITERIA_ITEM, + type RegEvalCriteriaDetails, + type RegEvalCriteriaView, +} from '@/db/schema'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { Textarea } from '@/components/ui/textarea'; +import { toast } from 'sonner'; +import { useForm, useFieldArray } from 'react-hook-form'; +import { useEffect, useTransition } from 'react'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +const regEvalCriteriaFormSchema = z.object({ + category: z.string().min(1, '평가부문은 필수 항목입니다.'), + scoreCategory: z.string().min(1, '점수구분은 필수 항목입니다.'), + item: z.string().min(1, '항목은 필수 항목입니다.'), + classification: z.string().min(1, '구분은 필수 항목입니다.'), + range: z.string().nullable().optional(), + remarks: z.string().nullable().optional(), + criteriaDetails: z.array( + z.object({ + id: z.number().optional(), + detail: z.string().min(1, '평가내용은 필수 항목입니다.'), + scoreEquipShip: z.coerce.number().nullable().optional(), + scoreEquipMarine: z.coerce.number().nullable().optional(), + scoreBulkShip: z.coerce.number().nullable().optional(), + scoreBulkMarine: z.coerce.number().nullable().optional(), + }) + ).min(1, '최소 1개의 평가 내용이 필요합니다.'), +}); +type RegEvalCriteriaFormData = z.infer; +interface CriteriaDetailFormProps { + index: number + form: any + onRemove: () => void + canRemove: boolean + disabled?: boolean +} +interface RegEvalCriteriaFormSheetProps { + open: boolean, + onOpenChange: (open: boolean) => void, + criteriaViewData: RegEvalCriteriaView | null, + onSuccess: () => void, +}; + +// ---------------------------------------------------------------------------------------------------- + +/* CRITERIA DETAIL FORM COPONENT */ +function CriteriaDetailForm({ + index, + form, + onRemove, + canRemove, + disabled = false, +}: CriteriaDetailFormProps) { + + return ( + + +
+ Detail Item - {index + 1} + {canRemove && ( + + )} +
+
+ + ( + + )} + /> + ( + + 평가내용 + +