summaryrefslogtreecommitdiff
path: root/lib/vendor-regular-registrations/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-14 11:54:47 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-14 11:54:47 +0000
commit969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 (patch)
tree551d335e850e6163792ded0e7a75fa41d96d612a /lib/vendor-regular-registrations/table
parentdd20ba9785cdbd3d61f6b014d003d3bd9646ad13 (diff)
(대표님) 정규벤더등록, 벤더문서관리, 벤더데이터입력, 첨부파일관리
Diffstat (limited to 'lib/vendor-regular-registrations/table')
-rw-r--r--lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx143
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx105
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table-toolbar-actions.tsx108
3 files changed, 284 insertions, 72 deletions
diff --git a/lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx b/lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx
new file mode 100644
index 00000000..c2aeba70
--- /dev/null
+++ b/lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { z } from "zod"
+import { toast } from "sonner"
+
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { updateSafetyQualification } from "../service"
+
+const formSchema = z.object({
+ safetyQualificationContent: z.string().min(1, "안전적격성 평가 내용을 입력해주세요."),
+})
+
+interface SafetyQualificationUpdateSheetProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ registrationId?: number
+ vendorName?: string
+ currentContent?: string | null
+ onSuccess?: () => void
+}
+
+export function SafetyQualificationUpdateSheet({
+ open,
+ onOpenChange,
+ registrationId,
+ vendorName,
+ currentContent,
+ onSuccess,
+}: SafetyQualificationUpdateSheetProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ const form = useForm<z.infer<typeof formSchema>>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ safetyQualificationContent: currentContent || "",
+ },
+ })
+
+ // 폼 값 초기화
+ React.useEffect(() => {
+ if (open) {
+ form.reset({
+ safetyQualificationContent: currentContent || "",
+ })
+ }
+ }, [open, currentContent, form])
+
+ async function onSubmit(values: z.infer<typeof formSchema>) {
+ if (!registrationId) {
+ toast.error("등록 ID가 없습니다.")
+ return
+ }
+
+ setIsLoading(true)
+ try {
+ const result = await updateSafetyQualification(
+ registrationId,
+ values.safetyQualificationContent
+ )
+
+ if (result.success) {
+ toast.success("안전적격성 평가가 등록되었습니다.")
+ onOpenChange(false)
+ onSuccess?.()
+ } else {
+ toast.error(result.error || "안전적격성 평가 등록에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("안전적격성 평가 등록 오류:", error)
+ toast.error("안전적격성 평가 등록 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="w-[400px] sm:w-[540px]">
+ <SheetHeader>
+ <SheetTitle>안전적격성 평가 입력</SheetTitle>
+ <SheetDescription>
+ {vendorName && `${vendorName}의 `}안전적격성 평가 내용을 입력해주세요.
+ </SheetDescription>
+ </SheetHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 mt-6">
+ <FormField
+ control={form.control}
+ name="safetyQualificationContent"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>안전적격성 평가 내용</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="안전적격성 평가 결과 및 내용을 입력해주세요..."
+ className="min-h-[200px]"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <div className="flex justify-end space-x-2">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isLoading}>
+ {isLoading ? "저장 중..." : "저장"}
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ )
+}
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
index 023bcfba..765b0279 100644
--- a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
+++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
@@ -10,9 +10,12 @@ import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrati
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 { Eye, FileText, Ellipsis, Shield, Package } from "lucide-react"
import { toast } from "sonner"
import { useState } from "react"
+import { SafetyQualificationUpdateSheet } from "./safety-qualification-update-sheet"
+import { MajorItemsUpdateSheet } from "../major-items-update-sheet"
+
const statusLabels = {
audit_pass: "실사통과",
@@ -20,6 +23,7 @@ const statusLabels = {
cp_review: "CP검토",
cp_finished: "CP완료",
approval_ready: "조건충족",
+ registration_requested: "등록요청됨",
in_review: "정규등록검토",
pending_approval: "장기미등록",
}
@@ -30,6 +34,7 @@ const statusColors = {
cp_review: "bg-yellow-100 text-yellow-800",
cp_finished: "bg-purple-100 text-purple-800",
approval_ready: "bg-emerald-100 text-emerald-800",
+ registration_requested: "bg-indigo-100 text-indigo-800",
in_review: "bg-orange-100 text-orange-800",
pending_approval: "bg-red-100 text-red-800",
}
@@ -159,22 +164,58 @@ export function getColumns(): ColumnDef<VendorRegularRegistration>[] {
},
{
id: "documentStatus",
- header: "문서/자료 접수 현황",
+ header: "진행현황",
cell: ({ row }) => {
const DocumentStatusCell = () => {
const [documentDialogOpen, setDocumentDialogOpen] = useState(false)
const registration = row.original
+
+ // 문서 현황 계산 (국가별 요구사항 적용)
+ const isForeign = registration.country !== 'KR'
+ const requiredDocs = isForeign ? 4 : 3 // 외자: 4개(통장사본 포함), 내자: 3개(통장사본 제외)
+ const submittedDocs = Object.values(registration.documentSubmissions).filter(Boolean).length
+ const incompleteDocs = requiredDocs - submittedDocs
+
+ // 기본계약 현황 계산
+ const totalContracts = registration.basicContracts?.length || 0
+ const completedContracts = registration.basicContracts?.filter(c => c.status === "COMPLETED").length || 0
+ const incompleteContracts = totalContracts - completedContracts
+
+ // 안전적격성 평가 현황
+ const safetyCompleted = !!registration.safetyQualificationContent
+
+ // 추가정보 현황
+ const additionalInfoCompleted = registration.additionalInfo
+
+ // 전체 미완료 항목 계산
+ const totalIncomplete =
+ (incompleteDocs > 0 ? 1 : 0) +
+ (incompleteContracts > 0 ? 1 : 0) +
+ (!safetyCompleted ? 1 : 0) +
+ (!additionalInfoCompleted ? 1 : 0)
+
+ const isAllComplete = totalIncomplete === 0
return (
<>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => setDocumentDialogOpen(true)}
- >
- <Eye className="w-4 h-4" />
- 현황보기
- </Button>
+ <div className="space-y-1">
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => setDocumentDialogOpen(true)}
+ className="h-auto p-1 text-left justify-start"
+ >
+ <div className="space-y-0.5">
+ {isAllComplete ? (
+ <div className="text-xs text-green-600 font-medium">모든 항목 완료</div>
+ ) : (
+ <div className="text-xs text-orange-600 font-medium">
+ 총 {totalIncomplete}건 미완료
+ </div>
+ )}
+ </div>
+ </Button>
+ </div>
<DocumentStatusDialog
open={documentDialogOpen}
onOpenChange={setDocumentDialogOpen}
@@ -222,7 +263,8 @@ export function getColumns(): ColumnDef<VendorRegularRegistration>[] {
id: "actions",
cell: ({ row }) => {
const ActionsDropdownCell = () => {
- const [documentDialogOpen, setDocumentDialogOpen] = useState(false)
+ const [safetyQualificationSheetOpen, setSafetyQualificationSheetOpen] = useState(false)
+ const [majorItemsSheetOpen, setMajorItemsSheetOpen] = useState(false)
const registration = row.original
return (
@@ -239,26 +281,43 @@ export function getColumns(): ColumnDef<VendorRegularRegistration>[] {
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]">
<DropdownMenuItem
- onClick={() => setDocumentDialogOpen(true)}
+ onClick={() => setSafetyQualificationSheetOpen(true)}
>
- <Eye className="mr-2 h-4 w-4" />
- 현황보기
+ <Shield className="mr-2 h-4 w-4" />
+ 안전적격성 평가
</DropdownMenuItem>
<DropdownMenuItem
- onClick={() => {
- toast.info("정규업체 등록 요청 기능은 준비 중입니다.")
- }}
+ onClick={() => setMajorItemsSheetOpen(true)}
>
- <FileText className="mr-2 h-4 w-4" />
- 등록요청
+ <Package className="mr-2 h-4 w-4" />
+ 주요품목 등록
</DropdownMenuItem>
+
</DropdownMenuContent>
</DropdownMenu>
- <DocumentStatusDialog
- open={documentDialogOpen}
- onOpenChange={setDocumentDialogOpen}
- registration={registration}
+ <SafetyQualificationUpdateSheet
+ open={safetyQualificationSheetOpen}
+ onOpenChange={setSafetyQualificationSheetOpen}
+ registrationId={registration.id}
+ vendorName={registration.companyName}
+ currentContent={registration.safetyQualificationContent}
+ onSuccess={() => {
+ // 페이지 새로고침 또는 데이터 리페치
+ window.location.reload()
+ }}
+ />
+ <MajorItemsUpdateSheet
+ open={majorItemsSheetOpen}
+ onOpenChange={setMajorItemsSheetOpen}
+ registrationId={registration.id}
+ vendorName={registration.companyName}
+ currentItems={registration.majorItems}
+ onSuccess={() => {
+ // 페이지 새로고침 또는 데이터 리페치
+ window.location.reload()
+ }}
/>
+
</>
)
}
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
index c3b4739a..3a1216f2 100644
--- 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
@@ -2,18 +2,20 @@
import { type Table } from "@tanstack/react-table"
import { toast } from "sonner"
+import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
-import { FileText, RefreshCw, Download, Mail, FileWarning, Scale, Shield } from "lucide-react"
+import { Mail, FileWarning, Scale, FileText } from "lucide-react"
import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"
import {
sendMissingContractRequestEmails,
sendAdditionalInfoRequestEmails,
skipLegalReview,
- skipSafetyQualification
+ submitRegistrationRequest
} from "../service"
import { useState } from "react"
import { SkipReasonDialog } from "@/components/vendor-regular-registrations/skip-reason-dialog"
+import { RegistrationRequestDialog } from "@/components/vendor-regular-registrations/registration-request-dialog"
interface VendorRegularRegistrationsTableToolbarActionsProps {
table: Table<VendorRegularRegistration>
@@ -22,24 +24,31 @@ interface VendorRegularRegistrationsTableToolbarActionsProps {
export function VendorRegularRegistrationsTableToolbarActions({
table,
}: VendorRegularRegistrationsTableToolbarActionsProps) {
+ const router = useRouter()
const [syncLoading, setSyncLoading] = useState<{
missingContract: boolean;
additionalInfo: boolean;
legalSkip: boolean;
- safetySkip: boolean;
+ registrationRequest: boolean;
}>({
missingContract: false,
additionalInfo: false,
legalSkip: false,
- safetySkip: false,
+ registrationRequest: false,
})
const [skipDialogs, setSkipDialogs] = useState<{
legalReview: boolean;
- safetyQualification: boolean;
}>({
legalReview: false,
- safetyQualification: false,
+ })
+
+ const [registrationRequestDialog, setRegistrationRequestDialog] = useState<{
+ open: boolean;
+ registration: VendorRegularRegistration | null;
+ }>({
+ open: false,
+ registration: null,
})
const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original)
@@ -108,7 +117,7 @@ export function VendorRegularRegistrationsTableToolbarActions({
if (result.success) {
toast.success(result.message);
- window.location.reload();
+ router.refresh();
} else {
toast.error(result.error);
}
@@ -120,33 +129,52 @@ export function VendorRegularRegistrationsTableToolbarActions({
}
};
- const handleSafetyQualificationSkip = async (reason: string) => {
- if (selectedRows.length === 0) {
- toast.error("업체를 선택해주세요.");
+ // 등록요청 핸들러
+ const handleRegistrationRequest = () => {
+ const approvalReadyRows = selectedRows.filter(row => row.status === "approval_ready");
+
+ if (approvalReadyRows.length === 0) {
+ toast.error("조건충족 상태의 벤더를 선택해주세요.");
+ return;
+ }
+
+ if (approvalReadyRows.length > 1) {
+ toast.error("정규업체 등록 요청은 한 번에 하나씩만 가능합니다.");
return;
}
- setSyncLoading(prev => ({ ...prev, safetySkip: true }));
+ setRegistrationRequestDialog({
+ open: true,
+ registration: approvalReadyRows[0],
+ });
+ };
+
+ const handleRegistrationRequestSubmit = async (requestData: any) => {
+ if (!registrationRequestDialog.registration) return;
+
+ setSyncLoading(prev => ({ ...prev, registrationRequest: true }));
try {
- const vendorIds = selectedRows.map(row => row.vendorId);
- const result = await skipSafetyQualification(vendorIds, reason);
-
+ const result = await submitRegistrationRequest(registrationRequestDialog.registration.id, requestData);
if (result.success) {
toast.success(result.message);
- window.location.reload();
+ setRegistrationRequestDialog({ open: false, registration: null });
+ window.location.reload(); // 데이터 새로고침
} else {
toast.error(result.error);
}
} catch (error) {
- console.error("Error skipping safety qualification:", error);
- toast.error("안전적격성평가 Skip 처리 중 오류가 발생했습니다.");
+ console.error("등록요청 오류:", error);
+ toast.error("등록요청 중 오류가 발생했습니다.");
} finally {
- setSyncLoading(prev => ({ ...prev, safetySkip: false }));
+ setSyncLoading(prev => ({ ...prev, registrationRequest: false }));
}
};
// CP검토 상태인 선택된 행들 개수
const cpReviewCount = selectedRows.filter(row => row.status === "cp_review").length;
+
+ // 조건충족 상태인 선택된 행들 개수
+ const approvalReadyCount = selectedRows.filter(row => row.status === "approval_ready").length;
return (
<div className="flex items-center gap-2">
@@ -193,55 +221,37 @@ export function VendorRegularRegistrationsTableToolbarActions({
<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"}
+ {syncLoading.legalSkip ? "처리 중..." : "GTC Skip"}
</Button>
<Button
- variant="outline"
+ variant="default"
size="sm"
- onClick={() => setSkipDialogs(prev => ({ ...prev, safetyQualification: true }))}
- disabled={syncLoading.safetySkip || selectedRows.length === 0}
+ onClick={handleRegistrationRequest}
+ disabled={syncLoading.registrationRequest || approvalReadyCount === 0}
>
- <Shield className="mr-2 h-4 w-4" />
- {syncLoading.safetySkip ? "처리 중..." : "안전 Skip"}
+ <FileText className="mr-2 h-4 w-4" />
+ {syncLoading.registrationRequest ? "처리 중..." : "등록요청"}
</Button>
<SkipReasonDialog
open={skipDialogs.legalReview}
onOpenChange={(open) => setSkipDialogs(prev => ({ ...prev, legalReview: open }))}
- title="법무검토 Skip"
- description={`선택된 ${cpReviewCount}개 업체의 법무검토를 Skip하고 CP완료 상태로 변경합니다. Skip 사유를 입력해주세요.`}
+ title="GTC Skip"
+ description={`선택된 ${cpReviewCount}개 업체의 GTC를 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}
+ <RegistrationRequestDialog
+ open={registrationRequestDialog.open}
+ onOpenChange={(open) => setRegistrationRequestDialog(prev => ({ ...prev, open }))}
+ registration={registrationRequestDialog.registration}
+ onSubmit={handleRegistrationRequestSubmit}
/>
</div>
)