summaryrefslogtreecommitdiff
path: root/lib/vendor-regular-registrations/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-regular-registrations/table')
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx270
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx248
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table.tsx104
3 files changed, 622 insertions, 0 deletions
diff --git a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
new file mode 100644
index 00000000..023bcfba
--- /dev/null
+++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
@@ -0,0 +1,270 @@
+"use client"
+
+import { type ColumnDef } from "@tanstack/react-table"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Badge } from "@/components/ui/badge"
+import { format } from "date-fns"
+
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"
+import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog"
+import { Button } from "@/components/ui/button"
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
+import { Eye, FileText, Ellipsis } from "lucide-react"
+import { toast } from "sonner"
+import { useState } from "react"
+
+const statusLabels = {
+ audit_pass: "실사통과",
+ cp_submitted: "CP등록",
+ cp_review: "CP검토",
+ cp_finished: "CP완료",
+ approval_ready: "조건충족",
+ in_review: "정규등록검토",
+ pending_approval: "장기미등록",
+}
+
+const statusColors = {
+ audit_pass: "bg-blue-100 text-blue-800",
+ cp_submitted: "bg-green-100 text-green-800",
+ cp_review: "bg-yellow-100 text-yellow-800",
+ cp_finished: "bg-purple-100 text-purple-800",
+ approval_ready: "bg-emerald-100 text-emerald-800",
+ in_review: "bg-orange-100 text-orange-800",
+ pending_approval: "bg-red-100 text-red-800",
+}
+
+export function getColumns(): ColumnDef<VendorRegularRegistration>[] {
+
+ return [
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-[2px]"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-[2px]"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "status",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Status" />
+ ),
+ cell: ({ row }) => {
+ const status = row.getValue("status") as string
+ return (
+ <Badge
+ variant="secondary"
+ className={statusColors[status as keyof typeof statusColors]}
+ >
+ {statusLabels[status as keyof typeof statusLabels] || status}
+ </Badge>
+ )
+ },
+ filterFn: (row, id, value) => {
+ return Array.isArray(value) && value.includes(row.getValue(id))
+ },
+ },
+ {
+ accessorKey: "potentialCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="잠재코드" />
+ ),
+ cell: ({ row }) => row.getValue("potentialCode") || "-",
+ },
+ {
+ accessorKey: "businessNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="사업자번호" />
+ ),
+ },
+ {
+ accessorKey: "companyName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="업체명" />
+ ),
+ },
+ {
+ accessorKey: "majorItems",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="주요품목" />
+ ),
+ cell: ({ row }) => {
+ const majorItems = row.getValue("majorItems") as string
+ try {
+ const items = majorItems ? JSON.parse(majorItems) : []
+ if (items.length === 0) return "-"
+
+ // 첫 번째 아이템을 itemCode-itemName 형태로 표시
+ const firstItem = items[0]
+ let displayText = ""
+
+ if (typeof firstItem === 'string') {
+ displayText = firstItem
+ } else if (typeof firstItem === 'object') {
+ const code = firstItem.itemCode || firstItem.code || ""
+ const name = firstItem.itemName || firstItem.name || firstItem.materialGroupName || ""
+ if (code && name) {
+ displayText = `${code}-${name}`
+ } else {
+ displayText = name || code || String(firstItem)
+ }
+ } else {
+ displayText = String(firstItem)
+ }
+
+ // 나머지 개수 표시
+ if (items.length > 1) {
+ displayText += ` 외 ${items.length - 1}개`
+ }
+
+ return displayText
+ } catch {
+ return majorItems || "-"
+ }
+ },
+ },
+ {
+ accessorKey: "establishmentDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="설립일자" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("establishmentDate") as string
+ return date ? format(new Date(date), "yyyy.MM.dd") : "-"
+ },
+ },
+ {
+ accessorKey: "representative",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="대표자명" />
+ ),
+ cell: ({ row }) => row.getValue("representative") || "-",
+ },
+ {
+ id: "documentStatus",
+ header: "문서/자료 접수 현황",
+ cell: ({ row }) => {
+ const DocumentStatusCell = () => {
+ const [documentDialogOpen, setDocumentDialogOpen] = useState(false)
+ const registration = row.original
+
+ return (
+ <>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => setDocumentDialogOpen(true)}
+ >
+ <Eye className="w-4 h-4" />
+ 현황보기
+ </Button>
+ <DocumentStatusDialog
+ open={documentDialogOpen}
+ onOpenChange={setDocumentDialogOpen}
+ registration={registration}
+ />
+ </>
+ )
+ }
+
+ return <DocumentStatusCell />
+ },
+ },
+ {
+ accessorKey: "registrationRequestDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="등록요청일" />
+ ),
+ cell: ({ row }) => {
+ const date = row.getValue("registrationRequestDate") as string
+ return date ? format(new Date(date), "yyyy.MM.dd") : "-"
+ },
+ },
+ {
+ accessorKey: "assignedDepartment",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="담당부서" />
+ ),
+ cell: ({ row }) => row.getValue("assignedDepartment") || "-",
+ },
+ {
+ accessorKey: "assignedUser",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="담당자" />
+ ),
+ cell: ({ row }) => row.getValue("assignedUser") || "-",
+ },
+ {
+ accessorKey: "remarks",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="비고" />
+ ),
+ cell: ({ row }) => row.getValue("remarks") || "-",
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ const ActionsDropdownCell = () => {
+ const [documentDialogOpen, setDocumentDialogOpen] = useState(false)
+ const registration = row.original
+
+ return (
+ <>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-[160px]">
+ <DropdownMenuItem
+ onClick={() => setDocumentDialogOpen(true)}
+ >
+ <Eye className="mr-2 h-4 w-4" />
+ 현황보기
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onClick={() => {
+ toast.info("정규업체 등록 요청 기능은 준비 중입니다.")
+ }}
+ >
+ <FileText className="mr-2 h-4 w-4" />
+ 등록요청
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ <DocumentStatusDialog
+ open={documentDialogOpen}
+ onOpenChange={setDocumentDialogOpen}
+ registration={registration}
+ />
+ </>
+ )
+ }
+
+ return <ActionsDropdownCell />
+ },
+ },
+ ]
+}
diff --git a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx
new file mode 100644
index 00000000..c3b4739a
--- /dev/null
+++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx
@@ -0,0 +1,248 @@
+"use client"
+
+import { type Table } from "@tanstack/react-table"
+import { toast } from "sonner"
+
+import { Button } from "@/components/ui/button"
+import { FileText, RefreshCw, Download, Mail, FileWarning, Scale, Shield } from "lucide-react"
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"
+import {
+ sendMissingContractRequestEmails,
+ sendAdditionalInfoRequestEmails,
+ skipLegalReview,
+ skipSafetyQualification
+} from "../service"
+import { useState } from "react"
+import { SkipReasonDialog } from "@/components/vendor-regular-registrations/skip-reason-dialog"
+
+interface VendorRegularRegistrationsTableToolbarActionsProps {
+ table: Table<VendorRegularRegistration>
+}
+
+export function VendorRegularRegistrationsTableToolbarActions({
+ table,
+}: VendorRegularRegistrationsTableToolbarActionsProps) {
+ const [syncLoading, setSyncLoading] = useState<{
+ missingContract: boolean;
+ additionalInfo: boolean;
+ legalSkip: boolean;
+ safetySkip: boolean;
+ }>({
+ missingContract: false,
+ additionalInfo: false,
+ legalSkip: false,
+ safetySkip: false,
+ })
+
+ const [skipDialogs, setSkipDialogs] = useState<{
+ legalReview: boolean;
+ safetyQualification: boolean;
+ }>({
+ legalReview: false,
+ safetyQualification: false,
+ })
+
+ const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original)
+
+
+
+ const handleSendMissingContractRequest = async () => {
+ if (selectedRows.length === 0) {
+ toast.error("이메일을 발송할 업체를 선택해주세요.")
+ return
+ }
+
+ setSyncLoading(prev => ({ ...prev, missingContract: true }))
+ try {
+ const vendorIds = selectedRows.map(row => row.vendorId)
+ const result = await sendMissingContractRequestEmails(vendorIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ console.error("Error sending missing contract request:", error)
+ toast.error("누락계약요청 이메일 발송 중 오류가 발생했습니다.")
+ } finally {
+ setSyncLoading(prev => ({ ...prev, missingContract: false }))
+ }
+ }
+
+ const handleSendAdditionalInfoRequest = async () => {
+ if (selectedRows.length === 0) {
+ toast.error("이메일을 발송할 업체를 선택해주세요.")
+ return
+ }
+
+ setSyncLoading(prev => ({ ...prev, additionalInfo: true }))
+ try {
+ const vendorIds = selectedRows.map(row => row.vendorId)
+ const result = await sendAdditionalInfoRequestEmails(vendorIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ console.error("Error sending additional info request:", error)
+ toast.error("추가정보요청 이메일 발송 중 오류가 발생했습니다.")
+ } finally {
+ setSyncLoading(prev => ({ ...prev, additionalInfo: false }))
+ }
+ }
+
+ const handleLegalReviewSkip = async (reason: string) => {
+ const cpReviewRows = selectedRows.filter(row => row.status === "cp_review");
+ if (cpReviewRows.length === 0) {
+ toast.error("CP검토 상태인 업체를 선택해주세요.");
+ return;
+ }
+
+ setSyncLoading(prev => ({ ...prev, legalSkip: true }));
+ try {
+ const vendorIds = cpReviewRows.map(row => row.vendorId);
+ const result = await skipLegalReview(vendorIds, reason);
+
+ if (result.success) {
+ toast.success(result.message);
+ window.location.reload();
+ } else {
+ toast.error(result.error);
+ }
+ } catch (error) {
+ console.error("Error skipping legal review:", error);
+ toast.error("법무검토 Skip 처리 중 오류가 발생했습니다.");
+ } finally {
+ setSyncLoading(prev => ({ ...prev, legalSkip: false }));
+ }
+ };
+
+ const handleSafetyQualificationSkip = async (reason: string) => {
+ if (selectedRows.length === 0) {
+ toast.error("업체를 선택해주세요.");
+ return;
+ }
+
+ setSyncLoading(prev => ({ ...prev, safetySkip: true }));
+ try {
+ const vendorIds = selectedRows.map(row => row.vendorId);
+ const result = await skipSafetyQualification(vendorIds, reason);
+
+ if (result.success) {
+ toast.success(result.message);
+ window.location.reload();
+ } else {
+ toast.error(result.error);
+ }
+ } catch (error) {
+ console.error("Error skipping safety qualification:", error);
+ toast.error("안전적격성평가 Skip 처리 중 오류가 발생했습니다.");
+ } finally {
+ setSyncLoading(prev => ({ ...prev, safetySkip: false }));
+ }
+ };
+
+ // CP검토 상태인 선택된 행들 개수
+ const cpReviewCount = selectedRows.filter(row => row.status === "cp_review").length;
+
+ return (
+ <div className="flex items-center gap-2">
+ {/* <Button
+ variant="outline"
+ size="sm"
+ onClick={handleSyncDocuments}
+ disabled={syncLoading.documents || selectedRows.length === 0}
+ >
+ <FileText className="mr-2 h-4 w-4" />
+ {syncLoading.documents ? "동기화 중..." : "문서 동기화"}
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleSyncAgreements}
+ disabled={syncLoading.agreements || selectedRows.length === 0}
+ >
+ <RefreshCw className="mr-2 h-4 w-4" />
+ {syncLoading.agreements ? "동기화 중..." : "계약 동기화"}
+ </Button> */}
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleSendMissingContractRequest}
+ disabled={syncLoading.missingContract || selectedRows.length === 0}
+ >
+ <FileWarning className="mr-2 h-4 w-4" />
+ {syncLoading.missingContract ? "발송 중..." : "누락계약요청"}
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleSendAdditionalInfoRequest}
+ disabled={syncLoading.additionalInfo || selectedRows.length === 0}
+ >
+ <Mail className="mr-2 h-4 w-4" />
+ {syncLoading.additionalInfo ? "발송 중..." : "추가정보요청"}
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ if (selectedRows.length === 0) {
+ toast.error("내보낼 항목을 선택해주세요.")
+ return
+ }
+ toast.info("엑셀 내보내기 기능은 준비 중입니다.")
+ }}
+ disabled={selectedRows.length === 0}
+ >
+ <Download className="mr-2 h-4 w-4" />
+ 엑셀 내보내기
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setSkipDialogs(prev => ({ ...prev, legalReview: true }))}
+ disabled={syncLoading.legalSkip || cpReviewCount === 0}
+ >
+ <Scale className="mr-2 h-4 w-4" />
+ {syncLoading.legalSkip ? "처리 중..." : "법무검토 Skip"}
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setSkipDialogs(prev => ({ ...prev, safetyQualification: true }))}
+ disabled={syncLoading.safetySkip || selectedRows.length === 0}
+ >
+ <Shield className="mr-2 h-4 w-4" />
+ {syncLoading.safetySkip ? "처리 중..." : "안전 Skip"}
+ </Button>
+
+ <SkipReasonDialog
+ open={skipDialogs.legalReview}
+ onOpenChange={(open) => setSkipDialogs(prev => ({ ...prev, legalReview: open }))}
+ title="법무검토 Skip"
+ description={`선택된 ${cpReviewCount}개 업체의 법무검토를 Skip하고 CP완료 상태로 변경합니다. Skip 사유를 입력해주세요.`}
+ onConfirm={handleLegalReviewSkip}
+ loading={syncLoading.legalSkip}
+ />
+
+ <SkipReasonDialog
+ open={skipDialogs.safetyQualification}
+ onOpenChange={(open) => setSkipDialogs(prev => ({ ...prev, safetyQualification: open }))}
+ title="안전적격성평가 Skip"
+ description={`선택된 ${selectedRows.length}개 업체의 안전적격성평가를 Skip하고 완료 상태로 변경합니다. Skip 사유를 입력해주세요.`}
+ onConfirm={handleSafetyQualificationSkip}
+ loading={syncLoading.safetySkip}
+ />
+ </div>
+ )
+}
diff --git a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table.tsx b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table.tsx
new file mode 100644
index 00000000..8b477dba
--- /dev/null
+++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table.tsx
@@ -0,0 +1,104 @@
+"use client"
+
+import * as React from "react"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+} 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 { getColumns } from "./vendor-regular-registrations-table-columns"
+import { VendorRegularRegistrationsTableToolbarActions } from "./vendor-regular-registrations-table-toolbar-actions"
+import { fetchVendorRegularRegistrations } from "../service"
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"
+
+interface VendorRegularRegistrationsTableProps {
+ promises: Promise<
+ [
+ Awaited<ReturnType<typeof fetchVendorRegularRegistrations>>,
+ ]
+ >
+}
+
+export function VendorRegularRegistrationsTable({ promises }: VendorRegularRegistrationsTableProps) {
+ // Suspense로 받아온 데이터
+ const [result] = React.use(promises)
+
+ if (!result.success || !result.data) {
+ throw new Error(result.error || "데이터를 불러오는데 실패했습니다.")
+ }
+
+ const data = result.data
+ const pageCount = Math.ceil(data.length / 10) // 임시로 10개씩 페이징
+
+
+
+ const columns = React.useMemo(
+ () => getColumns(),
+ []
+ )
+
+ const filterFields: DataTableFilterField<VendorRegularRegistration>[] = [
+ { id: "companyName", label: "업체명" },
+ { id: "businessNumber", label: "사업자번호" },
+ { id: "status", label: "상태" },
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<VendorRegularRegistration>[] = [
+ { id: "companyName", label: "업체명", type: "text" },
+ { id: "businessNumber", label: "사업자번호", type: "text" },
+ { id: "potentialCode", label: "잠재코드", type: "text" },
+ { id: "representative", label: "대표자명", type: "text" },
+ {
+ id: "status",
+ label: "상태",
+ type: "select",
+ options: [
+ { label: "실사통과", value: "audit_pass" },
+ { label: "CP등록", value: "cp_submitted" },
+ { label: "CP검토", value: "cp_review" },
+ { label: "CP완료", value: "cp_finished" },
+ { label: "조건충족", value: "approval_ready" },
+ { label: "정규등록검토", value: "in_review" },
+ { label: "장기미등록", value: "pending_approval" },
+ ]
+ },
+ { id: "assignedDepartment", label: "담당부서", type: "text" },
+ { id: "assignedUser", label: "담당자", type: "text" },
+ { id: "registrationRequestDate", label: "등록요청일", type: "date" },
+ ]
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "id", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ return (
+ <>
+ <DataTable
+ table={table}
+ >
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <VendorRegularRegistrationsTableToolbarActions table={table} />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+ </>
+ )
+}