From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Apr 2025 02:13:30 +0000 Subject: ~20250428 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vendor-type/repository.ts | 130 ++++++++++ lib/vendor-type/service.ts | 239 +++++++++++++++++++ lib/vendor-type/table/add-vendorTypes-dialog.tsx | 158 ++++++++++++ .../table/delete-vendorTypes-dialog.tsx | 149 ++++++++++++ lib/vendor-type/table/feature-flags-provider.tsx | 108 +++++++++ lib/vendor-type/table/feature-flags.tsx | 96 ++++++++ lib/vendor-type/table/import-excel-button.tsx | 265 +++++++++++++++++++++ .../table/import-vendorTypes-handler.tsx | 114 +++++++++ lib/vendor-type/table/update-vendorTypes-sheet.tsx | 151 ++++++++++++ .../table/vendorTypes-excel-template.tsx | 78 ++++++ .../table/vendorTypes-table-columns.tsx | 179 ++++++++++++++ .../table/vendorTypes-table-toolbar-actions.tsx | 162 +++++++++++++ lib/vendor-type/table/vendorTypes-table.tsx | 129 ++++++++++ lib/vendor-type/validations.ts | 46 ++++ 14 files changed, 2004 insertions(+) create mode 100644 lib/vendor-type/repository.ts create mode 100644 lib/vendor-type/service.ts create mode 100644 lib/vendor-type/table/add-vendorTypes-dialog.tsx create mode 100644 lib/vendor-type/table/delete-vendorTypes-dialog.tsx create mode 100644 lib/vendor-type/table/feature-flags-provider.tsx create mode 100644 lib/vendor-type/table/feature-flags.tsx create mode 100644 lib/vendor-type/table/import-excel-button.tsx create mode 100644 lib/vendor-type/table/import-vendorTypes-handler.tsx create mode 100644 lib/vendor-type/table/update-vendorTypes-sheet.tsx create mode 100644 lib/vendor-type/table/vendorTypes-excel-template.tsx create mode 100644 lib/vendor-type/table/vendorTypes-table-columns.tsx create mode 100644 lib/vendor-type/table/vendorTypes-table-toolbar-actions.tsx create mode 100644 lib/vendor-type/table/vendorTypes-table.tsx create mode 100644 lib/vendor-type/validations.ts (limited to 'lib/vendor-type') diff --git a/lib/vendor-type/repository.ts b/lib/vendor-type/repository.ts new file mode 100644 index 00000000..7e0be35e --- /dev/null +++ b/lib/vendor-type/repository.ts @@ -0,0 +1,130 @@ +import { vendorTypes } from "@/db/schema"; +import { SQL, eq, inArray, sql,asc, desc } from "drizzle-orm"; +import { PgTransaction } from "drizzle-orm/pg-core"; + +/** + * 협력업체 타입 조회 (복잡한 where + order + limit + offset 지원) + */ +export async function selectVendorTypes( + tx: PgTransaction, + params: { + where?: any; // drizzle-orm의 조건식 (and, eq...) 등 + orderBy?: (ReturnType | ReturnType)[]; + offset?: number; + limit?: number; + } +) { + const { where, orderBy, offset = 0, limit = 10 } = params; + + return tx + .select() + .from(vendorTypes) + .where(where) + .orderBy(...(orderBy ?? [])) + .offset(offset) + .limit(limit); +} +/** + * 전체 협력업체 타입 조회 + */ +export async function findAllVendorTypes(tx: any) { + return tx.select() + .from(vendorTypes) + .orderBy(vendorTypes.nameKo); +} + +/** + * 협력업체 타입 개수 카운트 + */ +export async function countVendorTypes(tx: any, where?: SQL | undefined) { + const result = await tx + .select({ count: sql`count(*)` }) + .from(vendorTypes) + .where(where || undefined); + + return Number(result[0]?.count || 0); +} + +/** + * 협력업체 타입 추가 + */ +export async function insertVendorType( + tx: any, + data: { + code: string; + nameKo: string; + nameEn: string; + } +) { + const insertedRows = await tx + .insert(vendorTypes) + .values(data); + + // 삽입된 데이터 가져오기 + return tx.select() + .from(vendorTypes) + .where(eq(vendorTypes.code, data.code)) + .limit(1); +} + +/** + * 단일 협력업체 타입 업데이트 + */ +export async function updateVendorType( + tx: any, + id: number, + data: Partial<{ + code: string; + nameKo: string; + nameEn: string; + }> +) { + await tx + .update(vendorTypes) + .set({ + ...data, + updatedAt: new Date() + }) + .where(eq(vendorTypes.id, id)); + + // 업데이트된 데이터 가져오기 + return tx.select() + .from(vendorTypes) + .where(eq(vendorTypes.id, id)) + .limit(1); +} + +/** + * ID로 단일 협력업체 타입 삭제 + */ +export async function deleteVendorTypeById(tx: any, id: number) { + // 삭제 전 데이터 가져오기 (필요한 경우) + const deletedRecord = await tx.select() + .from(vendorTypes) + .where(eq(vendorTypes.id, id)) + .limit(1); + + // 데이터 삭제 + await tx + .delete(vendorTypes) + .where(eq(vendorTypes.id, id)); + + return deletedRecord; +} + +/** + * 다수의 ID로 여러 협력업체 타입 삭제 + */ +export async function deleteVendorTypesByIds(tx: any, ids: number[]) { + // 삭제 전 데이터 가져오기 (필요한 경우) + const deletedRecords = await tx.select() + .from(vendorTypes) + .where(inArray(vendorTypes.id, ids)); + + // 데이터 삭제 + await tx + .delete(vendorTypes) + .where(inArray(vendorTypes.id, ids)); + + return deletedRecords; +} \ No newline at end of file diff --git a/lib/vendor-type/service.ts b/lib/vendor-type/service.ts new file mode 100644 index 00000000..8624bb0e --- /dev/null +++ b/lib/vendor-type/service.ts @@ -0,0 +1,239 @@ +"use server"; // Next.js 서버 액션에서 직접 import하려면 (선택) + +import { revalidateTag, unstable_noStore } from "next/cache"; +import db from "@/db/db"; +import { customAlphabet } from "nanoid"; + +import { filterColumns } from "@/lib/filter-columns"; +import { unstable_cache } from "@/lib/unstable-cache"; +import { getErrorMessage } from "@/lib/handle-error"; + +import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq } from "drizzle-orm"; +import { CreateVendorTypeSchema, GetVendorTypesSchema, UpdateVendorTypeSchema } from "./validations"; +import { + countVendorTypes, + deleteVendorTypeById, + deleteVendorTypesByIds, + findAllVendorTypes, + insertVendorType, + selectVendorTypes, + updateVendorType +} from "./repository"; +import { vendorTypes } from "@/db/schema"; + +/* ----------------------------------------------------- + 1) 조회 관련 +----------------------------------------------------- */ + +/** + * 복잡한 조건으로 VendorType 목록을 조회 (+ pagination) 하고, + * 총 개수에 따라 pageCount를 계산해서 리턴. + * Next.js의 unstable_cache를 사용해 일정 시간 캐시. + */ +export async function getVendorTypes(input: GetVendorTypesSchema) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + // advancedTable 모드면 filterColumns()로 where 절 구성 + const advancedWhere = filterColumns({ + table: vendorTypes, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(vendorTypes.nameKo, s), + ilike(vendorTypes.nameEn, s), + ilike(vendorTypes.code, s) + ); + } + + const finalWhere = and( + advancedWhere, + globalWhere + ); + + // 아니면 ilike, inArray, gte 등으로 where 절 구성 + const where = finalWhere; + + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(vendorTypes[item.id]) : asc(vendorTypes[item.id]) + ) + : [asc(vendorTypes.createdAt)]; + + // 트랜잭션 내부에서 Repository 호출 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectVendorTypes(tx, { + where, + orderBy, + offset, + limit: input.perPage, + }); + + const total = await countVendorTypes(tx, where); + return { data, total }; + }); + + const pageCount = Math.ceil(total / input.perPage); + + return { data, pageCount }; + } catch (err) { + console.log(err, "err") + // 에러 발생 시 디폴트 + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input)], // 캐싱 키 + { + revalidate: 3600, + tags: ["vendorTypes"], // revalidateTag("vendorTypes") 호출 시 무효화 + } + )(); +} + +/* ----------------------------------------------------- + 2) 생성(Create) +----------------------------------------------------- */ +export interface VendorTypeCreateData { + code?: string; + nameKo: string; + nameEn: string; +} + +/** + * VendorType 생성 + */ +export async function createVendorType(input: VendorTypeCreateData) { + unstable_noStore(); // Next.js 서버 액션 캐싱 방지 + + try { + if (!input.nameKo || !input.nameEn) { + return { + success: false, + message: "한국어 이름과 영어 이름은 필수입니다", + data: null, + error: "필수 필드 누락" + }; + } + + // 코드가 없으면 자동 생성 (예: nameEn의 소문자화 + nanoid) + const code = input.code || `${input.nameEn.toLowerCase().replace(/\s+/g, '-')}-${customAlphabet('1234567890abcdef', 6)()}`; + + // result 변수에 명시적으로 타입과 초기값 할당 + let result: any[] = []; + + // 트랜잭션 결과를 result에 할당 + result = await db.transaction(async (tx) => { + // 기존 코드 확인 (code는 unique) + const existingVendorType = input.code ? await tx.query.vendorTypes.findFirst({ + where: eq(vendorTypes.code, input.code), + }) : null; + + let txResult; + if (existingVendorType) { + // 기존 vendorType 업데이트 + txResult = await updateVendorType(tx, existingVendorType.id, { + nameKo: input.nameKo, + nameEn: input.nameEn, + }); + } else { + // 새 vendorType 생성 + txResult = await insertVendorType(tx, { + code, + nameKo: input.nameKo, + nameEn: input.nameEn, + }); + } + + return txResult; + }); + + // 캐시 무효화 + revalidateTag("vendorTypes"); + + return { + success: true, + data: result[0] || null, + error: null + }; + } catch (err) { + console.error("협력업체 타입 생성/업데이트 오류:", err); + + // 중복 키 오류 처리 + if (err instanceof Error && err.message.includes("unique constraint")) { + return { + success: false, + message: "이미 존재하는 협력업체 타입 코드입니다", + data: null, + error: "중복 키 오류" + }; + } + + return { + success: false, + message: getErrorMessage(err), + data: null, + error: getErrorMessage(err) + }; + } +} + +/* ----------------------------------------------------- + 3) 업데이트 +----------------------------------------------------- */ + +/** 단건 업데이트 */ +export async function modifyVendorType(input: UpdateVendorTypeSchema & { id: number }) { + unstable_noStore(); + try { + const data = await db.transaction(async (tx) => { + const [res] = await updateVendorType(tx, input.id, { + nameKo: input.nameKo, + nameEn: input.nameEn, + }); + return res; + }); + + revalidateTag("vendorTypes"); + return { data, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/** 단건 삭제 */ +export async function removeVendorType(input: { id: number }) { + unstable_noStore(); + try { + await db.transaction(async (tx) => { + await deleteVendorTypeById(tx, input.id); + }); + + revalidateTag("vendorTypes"); + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/** 복수 삭제 */ +export async function removeVendorTypes(input: { ids: number[] }) { + unstable_noStore(); + try { + await db.transaction(async (tx) => { + await deleteVendorTypesByIds(tx, input.ids); + }); + + revalidateTag("vendorTypes"); + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} \ No newline at end of file diff --git a/lib/vendor-type/table/add-vendorTypes-dialog.tsx b/lib/vendor-type/table/add-vendorTypes-dialog.tsx new file mode 100644 index 00000000..74e1d10c --- /dev/null +++ b/lib/vendor-type/table/add-vendorTypes-dialog.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" + +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { useToast } from "@/hooks/use-toast" +// react-hook-form + shadcn/ui Form +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { createVendorType } from "../service" +import { CreateVendorTypeSchema, createVendorTypeSchema } from "../validations" + +export function AddVendorTypeDialog() { + const [open, setOpen] = React.useState(false) + const { toast } = useToast() + const [isSubmitting, setIsSubmitting] = React.useState(false) + + // react-hook-form 세팅 + const form = useForm({ + resolver: zodResolver(createVendorTypeSchema), + defaultValues: { + nameKo: "", + nameEn: "", + }, + mode: "onChange", // 입력값이 변경될 때마다 유효성 검사 + }) + + // 폼 값 감시 + const nameKo = form.watch("nameKo") + const nameEn = form.watch("nameEn") + + // 두 필드가 모두 입력되었는지 확인 + const isFormValid = nameKo.trim() !== "" && nameEn.trim() !== "" + + async function onSubmit(data: CreateVendorTypeSchema) { + setIsSubmitting(true) + try { + const result = await createVendorType(data) + if (result.error) { + toast({ + title: "오류 발생", + description: result.error, + variant: "destructive", + }) + return + } + + // 성공 시 모달 닫고 폼 리셋 + toast({ + title: "성공", + description: "협력업체 타입이 성공적으로 생성되었습니다.", + }) + form.reset() + setOpen(false) + } catch (error) { + toast({ + title: "오류 발생", + description: "협력업체 타입 생성 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setIsSubmitting(false) + } + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + } + setOpen(nextOpen) + } + + return ( + + {/* 모달을 열기 위한 버튼 */} + + + + + + + 새 협력업체 타입 생성 + + 새 Vendor Type 정보를 입력하고 Create 버튼을 누르세요. + + + + {/* shadcn/ui Form을 이용해 react-hook-form과 연결 */} +
+ +
+ ( + + 업체 유형(한글) * + + + + + + )} + /> + ( + + 업체 유형(영문) * + + + + + + )} + /> +
+ + + + + +
+ +
+
+ ) +} \ No newline at end of file diff --git a/lib/vendor-type/table/delete-vendorTypes-dialog.tsx b/lib/vendor-type/table/delete-vendorTypes-dialog.tsx new file mode 100644 index 00000000..fa9376b6 --- /dev/null +++ b/lib/vendor-type/table/delete-vendorTypes-dialog.tsx @@ -0,0 +1,149 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { VendorTypes } from "@/db/schema" +import { removeVendorTypes } from "../service" + + +interface DeleteItemsDialogProps + extends React.ComponentPropsWithoutRef { + vendorTypes: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteVendorTypesDialog({ + vendorTypes, + showTrigger = true, + onSuccess, + ...props +}: DeleteItemsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + const { error } = await removeVendorTypes({ + ids: vendorTypes.map((item) => item.id), + }) + + if (error) { + toast.error(error) + return + } + + props.onOpenChange?.(false) + toast.success("Tasks deleted") + onSuccess?.() + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your{" "} + {vendorTypes.length} + {vendorTypes.length === 1 ? " task" : " tasks"} from our servers. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your{" "} + {vendorTypes.length} + {vendorTypes.length === 1 ? " item" : " items"} from our servers. + + + + + + + + + + + ) +} diff --git a/lib/vendor-type/table/feature-flags-provider.tsx b/lib/vendor-type/table/feature-flags-provider.tsx new file mode 100644 index 00000000..81131894 --- /dev/null +++ b/lib/vendor-type/table/feature-flags-provider.tsx @@ -0,0 +1,108 @@ +"use client" + +import * as React from "react" +import { useQueryState } from "nuqs" + +import { dataTableConfig, type DataTableConfig } from "@/config/data-table" +import { cn } from "@/lib/utils" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" + +type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"] + +interface FeatureFlagsContextProps { + featureFlags: FeatureFlagValue[] + setFeatureFlags: (value: FeatureFlagValue[]) => void +} + +const FeatureFlagsContext = React.createContext({ + featureFlags: [], + setFeatureFlags: () => {}, +}) + +export function useFeatureFlags() { + const context = React.useContext(FeatureFlagsContext) + if (!context) { + throw new Error( + "useFeatureFlags must be used within a FeatureFlagsProvider" + ) + } + return context +} + +interface FeatureFlagsProviderProps { + children: React.ReactNode +} + +export function FeatureFlagsProvider({ children }: FeatureFlagsProviderProps) { + const [featureFlags, setFeatureFlags] = useQueryState( + "flags", + { + defaultValue: [], + parse: (value) => value.split(",") as FeatureFlagValue[], + serialize: (value) => value.join(","), + eq: (a, b) => + a.length === b.length && a.every((value, index) => value === b[index]), + clearOnDefault: true, + shallow: false, + } + ) + + return ( + void setFeatureFlags(value), + }} + > +
+ setFeatureFlags(value)} + className="w-fit gap-0" + > + {dataTableConfig.featureFlags.map((flag, index) => ( + + + + + + +
{flag.tooltipTitle}
+
+ {flag.tooltipDescription} +
+
+
+ ))} +
+
+ {children} +
+ ) +} diff --git a/lib/vendor-type/table/feature-flags.tsx b/lib/vendor-type/table/feature-flags.tsx new file mode 100644 index 00000000..aaae6af2 --- /dev/null +++ b/lib/vendor-type/table/feature-flags.tsx @@ -0,0 +1,96 @@ +"use client" + +import * as React from "react" +import { useQueryState } from "nuqs" + +import { dataTableConfig, type DataTableConfig } from "@/config/data-table" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" + +type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"] + +interface TasksTableContextProps { + featureFlags: FeatureFlagValue[] + setFeatureFlags: (value: FeatureFlagValue[]) => void +} + +const TasksTableContext = React.createContext({ + featureFlags: [], + setFeatureFlags: () => {}, +}) + +export function useTasksTable() { + const context = React.useContext(TasksTableContext) + if (!context) { + throw new Error("useTasksTable must be used within a TasksTableProvider") + } + return context +} + +export function TasksTableProvider({ children }: React.PropsWithChildren) { + const [featureFlags, setFeatureFlags] = useQueryState( + "featureFlags", + { + defaultValue: [], + parse: (value) => value.split(",") as FeatureFlagValue[], + serialize: (value) => value.join(","), + eq: (a, b) => + a.length === b.length && a.every((value, index) => value === b[index]), + clearOnDefault: true, + } + ) + + return ( + void setFeatureFlags(value), + }} + > +
+ setFeatureFlags(value)} + className="w-fit" + > + {dataTableConfig.featureFlags.map((flag) => ( + + + + + + +
{flag.tooltipTitle}
+
+ {flag.tooltipDescription} +
+
+
+ ))} +
+
+ {children} +
+ ) +} diff --git a/lib/vendor-type/table/import-excel-button.tsx b/lib/vendor-type/table/import-excel-button.tsx new file mode 100644 index 00000000..bba9a117 --- /dev/null +++ b/lib/vendor-type/table/import-excel-button.tsx @@ -0,0 +1,265 @@ +"use client" + +import * as React from "react" +import { Upload } from "lucide-react" +import { toast } from "sonner" +import * as ExcelJS from 'exceljs' + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Progress } from "@/components/ui/progress" +import { processFileImport } from "./import-vendorTypes-handler" // 별도 파일로 분리 + +interface ImportVendorTypeButtonProps { + onSuccess?: () => void +} + +export function ImportVendorTypeButton({ onSuccess }: ImportVendorTypeButtonProps) { + const [open, setOpen] = React.useState(false) + const [file, setFile] = React.useState(null) + const [isUploading, setIsUploading] = React.useState(false) + const [progress, setProgress] = React.useState(0) + const [error, setError] = React.useState(null) + const fileInputRef = React.useRef(null) + + // 파일 선택 처리 + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0] + if (!selectedFile) return + + if (!selectedFile.name.endsWith('.xlsx') && !selectedFile.name.endsWith('.xls')) { + setError("Excel 파일(.xlsx 또는 .xls)만 가능합니다.") + return + } + + setFile(selectedFile) + setError(null) + } + + // 데이터 가져오기 처리 + const handleImport = async () => { + if (!file) { + setError("가져올 파일을 선택해주세요.") + return + } + + try { + setIsUploading(true) + setProgress(0) + setError(null) + + // 파일을 ArrayBuffer로 읽기 + const arrayBuffer = await file.arrayBuffer(); + + // ExcelJS 워크북 로드 + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(arrayBuffer); + + // 첫 번째 워크시트 가져오기 + const worksheet = workbook.worksheets[0]; + if (!worksheet) { + throw new Error("Excel 파일에 워크시트가 없습니다."); + } + + // 헤더 행 찾기 + let headerRowIndex = 1; + let headerRow: ExcelJS.Row | undefined; + let headerValues: (string | null)[] = []; + + worksheet.eachRow((row, rowNumber) => { + const values = row.values as (string | null)[]; + if (!headerRow && values.some(v => v === "업체 유형(한글)" || v === "업체 유형 (한글)" ||v === "업체 유형(영어)" || v === "업체 유형 (영어)"||v === "업체 유형(영문)" || v === "업체 유형 (영문)")) { + headerRowIndex = rowNumber; + headerRow = row; + headerValues = [...values]; + } + }); + + if (!headerRow) { + throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다."); + } + + // 헤더를 기반으로 인덱스 매핑 생성 + const headerMapping: Record = {}; + headerValues.forEach((value, index) => { + if (typeof value === 'string') { + headerMapping[value] = index; + } + }); + + // 필수 헤더 확인 + const requiredHeaders = ["업체 유형(한글)", "업체 유형(영문)"]; + const alternativeHeaders = { + "업체 유형(한글)": ["업체 유형 (한글)"], + "업체 유형(영문)": ["업체 유형(영어)", "업체 유형 (영어)", "업체 유형(영문)", "업체 유형 (영문)"], + }; + + // 헤더 매핑 확인 (대체 이름 포함) + const missingHeaders = requiredHeaders.filter(header => { + const alternatives = alternativeHeaders[header as keyof typeof alternativeHeaders] || []; + return !(header in headerMapping) && + !alternatives.some(alt => alt in headerMapping); + }); + + if (missingHeaders.length > 0) { + throw new Error(`다음 필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`); + } + + // 데이터 행 추출 (헤더 이후 행부터) + const dataRows: Record[] = []; + + worksheet.eachRow((row, rowNumber) => { + if (rowNumber > headerRowIndex) { + const rowData: Record = {}; + const values = row.values as (string | null | undefined)[]; + + // 헤더 매핑에 따라 데이터 추출 + Object.entries(headerMapping).forEach(([header, index]) => { + rowData[header] = values[index] || ""; + }); + + // 빈 행이 아닌 경우만 추가 + if (Object.values(rowData).some(value => value && value.toString().trim() !== "")) { + dataRows.push(rowData); + } + } + }); + + if (dataRows.length === 0) { + throw new Error("Excel 파일에 가져올 데이터가 없습니다."); + } + + // 진행 상황 업데이트를 위한 콜백 + const updateProgress = (current: number, total: number) => { + const percentage = Math.round((current / total) * 100); + setProgress(percentage); + }; + + // 실제 데이터 처리는 별도 함수에서 수행 + const result = await processFileImport( + dataRows, + updateProgress + ); + + // 처리 완료 + toast.success(`${result.successCount}개의 아이템이 성공적으로 가져와졌습니다.`); + + if (result.errorCount > 0) { + toast.warning(`${result.errorCount}개의 항목은 처리할 수 없었습니다.`); + } + + // 상태 초기화 및 다이얼로그 닫기 + setFile(null); + setOpen(false); + + // 성공 콜백 호출 + if (onSuccess) { + onSuccess(); + } + } catch (error) { + console.error("Excel 파일 처리 중 오류 발생:", error); + setError(error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다."); + } finally { + setIsUploading(false); + } + }; + + // 다이얼로그 열기/닫기 핸들러 + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + // 닫을 때 상태 초기화 + setFile(null) + setError(null) + setProgress(0) + if (fileInputRef.current) { + fileInputRef.current.value = "" + } + } + setOpen(newOpen) + } + + return ( + <> + + + + + + 업체유형 가져오기 + + 업체유형을 Excel 파일에서 가져옵니다. +
+ 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요. +
+
+ +
+
+ +
+ + {file && ( +
+ 선택된 파일: {file.name} ({(file.size / 1024).toFixed(1)} KB) +
+ )} + + {isUploading && ( +
+ +

+ {progress}% 완료 +

+
+ )} + + {error && ( +
+ {error} +
+ )} +
+ + + + + +
+
+ + ) +} \ No newline at end of file diff --git a/lib/vendor-type/table/import-vendorTypes-handler.tsx b/lib/vendor-type/table/import-vendorTypes-handler.tsx new file mode 100644 index 00000000..85e03e5e --- /dev/null +++ b/lib/vendor-type/table/import-vendorTypes-handler.tsx @@ -0,0 +1,114 @@ +"use client" + +import { z } from "zod" +import { createVendorType } from "../service"; + +// 아이템 데이터 검증을 위한 Zod 스키마 +const itemSchema = z.object({ + nameKo: z.string().min(1, "업체 유형(한글)은 필수입니다"), + nameEn: z.string().min(1, "업체 유형(영문)은 필수입니다"), +}); + +interface ProcessResult { + successCount: number; + errorCount: number; + errors?: Array<{ row: number; message: string }>; +} + +/** + * Excel 파일에서 가져온 아이템 데이터 처리하는 함수 + */ +export async function processFileImport( + jsonData: any[], + progressCallback?: (current: number, total: number) => void +): Promise { + // 결과 카운터 초기화 + let successCount = 0; + let errorCount = 0; + const errors: Array<{ row: number; message: string }> = []; + + // 빈 행 등 필터링 + const dataRows = jsonData.filter(row => { + // 빈 행 건너뛰기 + if (Object.values(row).every(val => !val)) { + return false; + } + return true; + }); + + // 데이터 행이 없으면 빈 결과 반환 + if (dataRows.length === 0) { + return { successCount: 0, errorCount: 0 }; + } + + // 각 행에 대해 처리 + for (let i = 0; i < dataRows.length; i++) { + const row = dataRows[i]; + const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작 + + // 진행 상황 콜백 호출 + if (progressCallback) { + progressCallback(i + 1, dataRows.length); + } + + try { + // 필드 매핑 (한글/영문 필드명 모두 지원) + const nameKo = row["업체 유형(한글)"] || row["업체 유형 (한글)"] || ""; + const nameEn = row["업체 유형(영문)"] || row["업체 유형 (영문)"] || row["업체 유형(영어)"] || row["업체 유형 (영어)"] || ""; + + // 데이터 정제 + const cleanedRow = { + nameKo: typeof nameKo === 'string' ? nameKo.trim() : String(nameKo).trim(), + nameEn: typeof nameEn === 'string' ? nameEn.trim() : String(nameEn).trim(), + }; + + // 데이터 유효성 검사 + const validationResult = itemSchema.safeParse(cleanedRow); + + if (!validationResult.success) { + const errorMessage = validationResult.error.errors.map( + err => `${err.path.join('.')}: ${err.message}` + ).join(', '); + + errors.push({ row: rowIndex, message: errorMessage }); + errorCount++; + continue; + } + + // 아이템 생성 서버 액션 호출 + const result = await createVendorType({ + nameKo: cleanedRow.nameKo, + nameEn: cleanedRow.nameEn, + }); + + if (result.success || !result.error) { + successCount++; + } else { + errors.push({ + row: rowIndex, + message: result.message || result.error || "알 수 없는 오류" + }); + errorCount++; + } + } catch (error) { + console.error(`${rowIndex}행 처리 오류:`, error); + errors.push({ + row: rowIndex, + message: error instanceof Error ? error.message : "알 수 없는 오류" + }); + errorCount++; + } + + // 비동기 작업 쓰로틀링 + if (i % 5 === 0) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + } + + // 처리 결과 반환 + return { + successCount, + errorCount, + errors: errors.length > 0 ? errors : undefined + }; +} \ No newline at end of file diff --git a/lib/vendor-type/table/update-vendorTypes-sheet.tsx b/lib/vendor-type/table/update-vendorTypes-sheet.tsx new file mode 100644 index 00000000..d096a706 --- /dev/null +++ b/lib/vendor-type/table/update-vendorTypes-sheet.tsx @@ -0,0 +1,151 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader } from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" + +import { Input } from "@/components/ui/input" +import { UpdateVendorTypeSchema, updateVendorTypeSchema } from "../validations" +import { modifyVendorType } from "../service" +import { VendorTypes } from "@/db/schema" + +interface UpdateTypeSheetProps + extends React.ComponentPropsWithRef { + vendorType: VendorTypes | null +} + +export function UpdateTypeSheet({ vendorType, ...props }: UpdateTypeSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(updateVendorTypeSchema), + defaultValues: { + nameKo: vendorType?.nameKo ?? "", + nameEn: vendorType?.nameEn ?? "", + + }, + }) + + + React.useEffect(() => { + if (vendorType) { + form.reset({ + nameKo: vendorType.nameKo ?? "", + nameEn: vendorType.nameEn ?? "", + }); + } + }, [vendorType, form]); + + function onSubmit(input: UpdateVendorTypeSchema) { + startUpdateTransition(async () => { + if (!vendorType) return + + const { error } = await modifyVendorType({ + id: vendorType.id, + ...input, + }) + + if (error) { + toast.error(error) + return + } + + form.reset() + props.onOpenChange?.(false) + toast.success("Item updated") + }) + } + + return ( + + + + Update vendorType + + Update the vendorType details and save the changes + + +
+ + + ( + + 업체 유형 (한글) + + + + + + )} + /> + + ( + + 업체 유형 (영문) + + + + + + )} + /> + + + + + + + + + +
+
+ ) +} diff --git a/lib/vendor-type/table/vendorTypes-excel-template.tsx b/lib/vendor-type/table/vendorTypes-excel-template.tsx new file mode 100644 index 00000000..a48e807e --- /dev/null +++ b/lib/vendor-type/table/vendorTypes-excel-template.tsx @@ -0,0 +1,78 @@ +import * as ExcelJS from 'exceljs'; +import { saveAs } from "file-saver"; + +/** + * 업체 유형 데이터 가져오기를 위한 Excel 템플릿 파일 생성 및 다운로드 + */ +export async function exportVendorTypeTemplate() { + // 워크북 생성 + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'Item Management System'; + workbook.created = new Date(); + + // 워크시트 생성 + const worksheet = workbook.addWorksheet('업체 유형'); + + // 컬럼 헤더 정의 및 스타일 적용 + worksheet.columns = [ + { header: '업체 유형(한글)', key: 'nameKo', width: 50 }, + { header: '업체 유형(영문)', key: 'nameEn', width: 50 }, + ]; + + // 헤더 스타일 적용 + const headerRow = worksheet.getRow(1); + headerRow.font = { bold: true }; + headerRow.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + }; + headerRow.alignment = { vertical: 'middle', horizontal: 'center' }; + + // 테두리 스타일 적용 + headerRow.eachCell((cell) => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + + // 샘플 데이터 추가 + const sampleData = [ + { nameKo: 'ITEM001', nameEn: '샘플 업체 유형 1', }, + { nameKo: 'ITEM002', nameEn: '샘플 업체 유형 2', } + ]; + + // 데이터 행 추가 + sampleData.forEach(item => { + worksheet.addRow(item); + }); + + // 데이터 행 스타일 적용 + worksheet.eachRow((row, rowNumber) => { + if (rowNumber > 1) { // 헤더를 제외한 데이터 행 + row.eachCell((cell) => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + } + }); + + + try { + // 워크북을 Blob으로 변환 + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + saveAs(blob, 'vendor-type-template.xlsx'); + return true; + } catch (error) { + console.error('Excel 템플릿 생성 오류:', error); + throw error; + } +} \ No newline at end of file diff --git a/lib/vendor-type/table/vendorTypes-table-columns.tsx b/lib/vendor-type/table/vendorTypes-table-columns.tsx new file mode 100644 index 00000000..b5cfca71 --- /dev/null +++ b/lib/vendor-type/table/vendorTypes-table-columns.tsx @@ -0,0 +1,179 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis } from "lucide-react" +import { toast } from "sonner" + +import { getErrorMessage } from "@/lib/handle-error" +import { formatDate } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { VendorTypes } from "@/db/schema" +import { VendorTypesColumnsConfig } from "@/config/VendorTypesColumnsConfig" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +/** + * tanstack table 컬럼 정의 (중첩 헤더 버전) + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + 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" + /> + ), + size:40, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + return ( + + + + + + setRowAction({ row, type: "update" })} + > + Edit + + + + setRowAction({ row, type: "delete" })} + > + Delete + ⌘⌫ + + + + ) + }, + size: 40, + } + + // ---------------------------------------------------------------- + // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 + // ---------------------------------------------------------------- + // 3-1) groupMap: { [groupName]: ColumnDef[] } + const groupMap: Record[]> = {} + + VendorTypesColumnsConfig.forEach((cfg) => { + // 만약 group가 없으면 "_noGroup" 처리 + const groupName = cfg.group || "_noGroup" + + if (!groupMap[groupName]) { + groupMap[groupName] = [] + } + + // child column 정의 + const childCol: ColumnDef = { + accessorKey: cfg.id, + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + cell: ({ row, cell }) => { + + if (cfg.id === "createdAt"||cfg.id === "updatedAt") { + const dateVal = cell.getValue() as Date + return formatDate(dateVal) + } + + return row.getValue(cfg.id) ?? "" + }, + } + + groupMap[groupName].push(childCol) + }) + + // ---------------------------------------------------------------- + // 3-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 + // ---------------------------------------------------------------- + const nestedColumns: ColumnDef[] = [] + + // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 + // 여기서는 그냥 Object.entries 순서 + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + if (groupName === "_noGroup") { + // 그룹 없음 → 그냥 최상위 레벨 컬럼 + nestedColumns.push(...colDefs) + } else { + // 상위 컬럼 + nestedColumns.push({ + id: groupName, + header: groupName, // "Basic Info", "Metadata" 등 + columns: colDefs, + }) + } + }) + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열: select, nestedColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + ...nestedColumns, + actionsColumn, + ] +} \ No newline at end of file diff --git a/lib/vendor-type/table/vendorTypes-table-toolbar-actions.tsx b/lib/vendor-type/table/vendorTypes-table-toolbar-actions.tsx new file mode 100644 index 00000000..de56c42f --- /dev/null +++ b/lib/vendor-type/table/vendorTypes-table-toolbar-actions.tsx @@ -0,0 +1,162 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, FileDown } from "lucide-react" +import * as ExcelJS from 'exceljs' +import { saveAs } from "file-saver" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { AddVendorTypeDialog } from "./add-vendorTypes-dialog" +import { exportVendorTypeTemplate } from "./vendorTypes-excel-template" +import { VendorTypes } from "@/db/schema" +import { DeleteVendorTypesDialog } from "./delete-vendorTypes-dialog" +import { ImportVendorTypeButton } from "./import-excel-button" + +interface ItemsTableToolbarActionsProps { + table: Table +} + +export function ItemsTableToolbarActions({ table }: ItemsTableToolbarActionsProps) { + const [refreshKey, setRefreshKey] = React.useState(0) + + // 가져오기 성공 후 테이블 갱신 + const handleImportSuccess = () => { + setRefreshKey(prev => prev + 1) + } + + // Excel 내보내기 함수 + const exportTableToExcel = async ( + table: Table, + options: { + filename: string; + excludeColumns?: string[]; + sheetName?: string; + } + ) => { + const { filename, excludeColumns = [], sheetName = "업체 유형 목록" } = options; + + // 워크북 생성 + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'vendorType Management System'; + workbook.created = new Date(); + + // 워크시트 생성 + const worksheet = workbook.addWorksheet(sheetName); + + // 테이블 데이터 가져오기 + const data = table.getFilteredRowModel().rows.map(row => row.original); + + // 테이블 헤더 가져오기 + const headers = table.getAllColumns() + .filter(column => !excludeColumns.includes(column.id)) + .map(column => ({ + key: column.id, + header: column.columnDef.header?.toString() || column.id + })); + + // 컬럼 정의 + worksheet.columns = headers.map(header => ({ + header: header.header, + key: header.key, + width: 20 // 기본 너비 + })); + + // 스타일 적용 + const headerRow = worksheet.getRow(1); + headerRow.font = { bold: true }; + headerRow.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + }; + headerRow.alignment = { vertical: 'middle', horizontal: 'center' }; + + // 데이터 행 추가 + data.forEach(item => { + const row: Record = {}; + headers.forEach(header => { + row[header.key] = item[header.key]; + }); + worksheet.addRow(row); + }); + + // 전체 셀에 테두리 추가 + worksheet.eachRow((row, rowNumber) => { + row.eachCell((cell) => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + }); + + try { + // 워크북을 Blob으로 변환 + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + saveAs(blob, `${filename}.xlsx`); + return true; + } catch (error) { + console.error("Excel 내보내기 오류:", error); + return false; + } + } + + return ( +
+ {/* 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + row.original)} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + ) : null} + + {/* 새 업체 유형 추가 다이얼로그 */} + + + {/* Import 버튼 */} + + + {/* Export 드롭다운 메뉴 */} + + + + + + + exportTableToExcel(table, { + filename: "items", + excludeColumns: ["select", "actions"], + sheetName: "업체 유형 목록" + }) + } + > + + 현재 데이터 내보내기 + + exportVendorTypeTemplate()}> + + 템플릿 다운로드 + + + +
+ ) +} \ No newline at end of file diff --git a/lib/vendor-type/table/vendorTypes-table.tsx b/lib/vendor-type/table/vendorTypes-table.tsx new file mode 100644 index 00000000..67c9d632 --- /dev/null +++ b/lib/vendor-type/table/vendorTypes-table.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { useFeatureFlags } from "./feature-flags-provider" + +import { getColumns } from "./vendorTypes-table-columns" +import { ItemsTableToolbarActions } from "./vendorTypes-table-toolbar-actions" +import { UpdateTypeSheet } from "./update-vendorTypes-sheet" +import { getVendorTypes } from "../service" +import { VendorTypes } from "@/db/schema" +import { DeleteVendorTypesDialog } from "./delete-vendorTypes-dialog" + +interface ItemsTableProps { + promises: Promise< + [ + Awaited>, + ] + > +} + +export function VendorTypesTable({ promises }: ItemsTableProps) { + const { featureFlags } = useFeatureFlags() + + const [{ data, pageCount }] = + React.use(promises) + + console.log(data) + + const [rowAction, setRowAction] = + React.useState | null>(null) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + /** + * This component can render either a faceted filter or a search filter based on the `options` prop. + * + * @prop options - An array of objects, each representing a filter option. If provided, a faceted filter is rendered. If not, a search filter is rendered. + * + * Each `option` object has the following properties: + * @prop {string} label - The label for the filter option. + * @prop {string} value - The value for the filter option. + * @prop {React.ReactNode} [icon] - An optional icon to display next to the label. + * @prop {boolean} [withCount] - An optional boolean to display the count of the filter option. + */ + const filterFields: DataTableFilterField[] = [ + ] + + /** + * Advanced filter fields for the data table. + * These fields provide more complex filtering options compared to the regular filterFields. + * + * Key differences from regular filterFields: + * 1. More field types: Includes 'text', 'multi-select', 'date', and 'boolean'. + * 2. Enhanced flexibility: Allows for more precise and varied filtering options. + * 3. Used with DataTableAdvancedToolbar: Enables a more sophisticated filtering UI. + * 4. Date and boolean types: Adds support for filtering by date ranges and boolean values. + */ + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { + id: "nameKo", + label: "업체 유형(한글)", + type: "text", + }, + { + id: "nameEn", + label: "업체 유형(En)", + type: "text", + }, + ] + + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + + + + + + + + setRowAction(null)} + vendorType={rowAction?.row.original ?? null} + /> + setRowAction(null)} + vendorTypes={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => rowAction?.row.toggleSelected(false)} + /> + + ) +} diff --git a/lib/vendor-type/validations.ts b/lib/vendor-type/validations.ts new file mode 100644 index 00000000..146c404e --- /dev/null +++ b/lib/vendor-type/validations.ts @@ -0,0 +1,46 @@ +import { + createSearchParamsCache, + parseAsArrayOf, + parseAsInteger, + parseAsString, + parseAsStringEnum, +} from "nuqs/server" +import * as z from "zod" + +import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers" +import { VendorTypes } from "@/db/schema" + +export const searchParamsCache = createSearchParamsCache({ + flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault( + [] + ), + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + sort: getSortingStateParser().withDefault([ + { id: "createdAt", desc: true }, + ]), + nameKo: parseAsString.withDefault(""), + nameEn: parseAsString.withDefault(""), + from: parseAsString.withDefault(""), + to: parseAsString.withDefault(""), + // advanced filter + filters: getFiltersStateParser().withDefault([]), + joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), + search: parseAsString.withDefault(""), + +}) + +export const createVendorTypeSchema = z.object({ + nameKo: z.string(), + nameEn: z.string(), + +}) + +export const updateVendorTypeSchema = z.object({ + nameKo: z.string().optional(), + nameEn: z.string().optional(), +}) + +export type GetVendorTypesSchema = Awaited> +export type CreateVendorTypeSchema = z.infer +export type UpdateVendorTypeSchema = z.infer -- cgit v1.2.3