summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/plant
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/plant')
-rw-r--r--lib/vendor-document-list/plant/document-stage-dialogs.tsx446
-rw-r--r--lib/vendor-document-list/plant/document-stage-toolbar.tsx103
-rw-r--r--lib/vendor-document-list/plant/document-stages-columns.tsx10
-rw-r--r--lib/vendor-document-list/plant/document-stages-expanded-content.tsx4
-rw-r--r--lib/vendor-document-list/plant/document-stages-service.ts400
-rw-r--r--lib/vendor-document-list/plant/document-stages-table.tsx107
6 files changed, 738 insertions, 332 deletions
diff --git a/lib/vendor-document-list/plant/document-stage-dialogs.tsx b/lib/vendor-document-list/plant/document-stage-dialogs.tsx
index 732a4bed..726ea101 100644
--- a/lib/vendor-document-list/plant/document-stage-dialogs.tsx
+++ b/lib/vendor-document-list/plant/document-stage-dialogs.tsx
@@ -3,11 +3,13 @@
import React from "react"
import {
Dialog,
+ DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
+ DialogTrigger,
} from "@/components/ui/dialog"
import {
Sheet,
@@ -30,19 +32,36 @@ import {
} from "@/components/ui/select"
import { Badge } from "@/components/ui/badge"
import { DocumentStagesOnlyView } from "@/db/schema"
-import { Upload, FileSpreadsheet, Calendar, User, Target, Loader2 } from "lucide-react"
+import { Upload, FileSpreadsheet, Calendar, User, Target, Loader2, AlertTriangle, Loader ,Trash} from "lucide-react"
import { toast } from "sonner"
import {
getDocumentNumberTypes,
getDocumentNumberTypeConfigs,
getComboBoxOptions,
getDocumentClasses,
+ getDocumentClassOptions,
createDocument,
+ updateDocument,
+ deleteDocuments,
updateStage
} from "./document-stages-service"
+import { type Row } from "@tanstack/react-table"
+
+import { useMediaQuery } from "@/hooks/use-media-query"
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer"
+import { useRouter } from "next/navigation"
// =============================================================================
-// 1. Add Document Dialog (Updated with fixed header/footer and English text)
+// 1. Add Document Dialog
// =============================================================================
interface AddDocumentDialogProps {
open: boolean
@@ -62,14 +81,15 @@ export function AddDocumentDialog({
const [documentClasses, setDocumentClasses] = React.useState<any[]>([])
const [selectedTypeConfigs, setSelectedTypeConfigs] = React.useState<any[]>([])
const [comboBoxOptions, setComboBoxOptions] = React.useState<Record<number, any[]>>({})
+ const [documentClassOptions, setDocumentClassOptions] = React.useState<any[]>([])
const [formData, setFormData] = React.useState({
documentNumberTypeId: "",
documentClassId: "",
title: "",
- pic: "",
vendorDocNumber: "",
- fieldValues: {} as Record<string, string>
+ fieldValues: {} as Record<string, string>,
+ planDates: {} as Record<number, string> // optionId -> planDate
})
// Load initial data
@@ -150,6 +170,35 @@ export function AddDocumentDialog({
})
}
+ // Handle document class change
+ const handleDocumentClassChange = async (documentClassId: string) => {
+ setFormData({
+ ...formData,
+ documentClassId,
+ planDates: {}
+ })
+
+ if (documentClassId) {
+ const optionsResult = await getDocumentClassOptions(Number(documentClassId))
+ if (optionsResult.success) {
+ setDocumentClassOptions(optionsResult.data)
+ }
+ } else {
+ setDocumentClassOptions([])
+ }
+ }
+
+ // Handle plan date change
+ const handlePlanDateChange = (optionId: number, date: string) => {
+ setFormData({
+ ...formData,
+ planDates: {
+ ...formData.planDates,
+ [optionId]: date
+ }
+ })
+ }
+
// Generate document number preview
const generatePreviewDocNumber = () => {
if (selectedTypeConfigs.length === 0) return ""
@@ -166,12 +215,44 @@ export function AddDocumentDialog({
return preview
}
+ // Check if form is valid for submission
+ const isFormValid = () => {
+ // Check basic required fields
+ if (!formData.documentNumberTypeId || !formData.documentClassId || !formData.title.trim()) {
+ return false
+ }
+
+ // Check if all required document number components are filled
+ const requiredConfigs = selectedTypeConfigs.filter(config => config.required)
+ for (const config of requiredConfigs) {
+ const fieldKey = `field_${config.sdq}`
+ const value = formData.fieldValues[fieldKey]
+ if (!value || !value.trim()) {
+ return false
+ }
+ }
+
+ // Check if document number can be generated
+ const docNumber = generatePreviewDocNumber()
+ if (!docNumber || docNumber === "" || docNumber.includes("[value]")) {
+ return false
+ }
+
+ return true
+ }
+
const handleSubmit = async () => {
- if (!formData.documentNumberTypeId || !formData.documentClassId || !formData.title) {
+ if (!isFormValid()) {
toast.error("Please fill in all required fields.")
return
}
+ const docNumber = generatePreviewDocNumber()
+ if (!docNumber) {
+ toast.error("Cannot generate document number.")
+ return
+ }
+
setIsLoading(true)
try {
const result = await createDocument({
@@ -179,8 +260,9 @@ export function AddDocumentDialog({
documentNumberTypeId: Number(formData.documentNumberTypeId),
documentClassId: Number(formData.documentClassId),
title: formData.title,
+ docNumber: docNumber, // 미리 생성된 문서번호 전송
fieldValues: formData.fieldValues,
- pic: formData.pic,
+ planDates: formData.planDates,
vendorDocNumber: formData.vendorDocNumber,
})
@@ -203,12 +285,13 @@ export function AddDocumentDialog({
documentNumberTypeId: "",
documentClassId: "",
title: "",
- pic: "",
vendorDocNumber: "",
- fieldValues: {}
+ fieldValues: {},
+ planDates: {}
})
setSelectedTypeConfigs([])
setComboBoxOptions({})
+ setDocumentClassOptions([])
}
const isPlantProject = projectType === "plant"
@@ -317,7 +400,7 @@ export function AddDocumentDialog({
</Label>
<Select
value={formData.documentClassId}
- onValueChange={(value) => setFormData({ ...formData, documentClassId: value })}
+ onValueChange={handleDocumentClassChange}
>
<SelectTrigger>
<SelectValue placeholder="Select document class" />
@@ -337,6 +420,38 @@ export function AddDocumentDialog({
)}
</div>
+ {/* Document Class Options with Plan Dates */}
+ {documentClassOptions.length > 0 && (
+ <div className="border rounded-lg p-4 bg-green-50/30">
+ <Label className="text-sm font-medium text-green-800 mb-3 block">
+ Document Class Stages with Plan Dates
+ </Label>
+ <div className="grid gap-3">
+ {documentClassOptions.map((option) => (
+ <div key={option.id} className="grid grid-cols-2 gap-3 items-center">
+ <div>
+ <Label className="text-sm font-medium">
+ {option.optionValue}
+ </Label>
+ {option.optionCode && (
+ <p className="text-xs text-gray-500">Code: {option.optionCode}</p>
+ )}
+ </div>
+ <div className="grid gap-1">
+ <Label className="text-xs text-gray-600">Plan Date</Label>
+ <Input
+ type="date"
+ value={formData.planDates[option.id] || ""}
+ onChange={(e) => handlePlanDateChange(option.id, e.target.value)}
+ className="text-sm"
+ />
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
{/* Document Title */}
<div className="grid gap-2">
<Label htmlFor="title">
@@ -351,17 +466,17 @@ export function AddDocumentDialog({
</div>
{/* Additional Information */}
- {isPlantProject && (
- <div className="grid gap-2">
- <Label htmlFor="vendorDocNumber">Vendor Document Number</Label>
- <Input
- id="vendorDocNumber"
- value={formData.vendorDocNumber}
- onChange={(e) => setFormData({ ...formData, vendorDocNumber: e.target.value })}
- placeholder="Vendor provided document number"
- />
- </div>
- )}
+ {isPlantProject && (
+ <div className="grid gap-2">
+ <Label htmlFor="vendorDocNumber">Vendor Document Number</Label>
+ <Input
+ id="vendorDocNumber"
+ value={formData.vendorDocNumber}
+ onChange={(e) => setFormData({ ...formData, vendorDocNumber: e.target.value })}
+ placeholder="Vendor provided document number"
+ />
+ </div>
+ )}
</div>
</div>
)}
@@ -370,7 +485,7 @@ export function AddDocumentDialog({
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
Cancel
</Button>
- <Button onClick={handleSubmit} disabled={isLoading}>
+ <Button onClick={handleSubmit} disabled={isLoading || !isFormValid()}>
{isLoading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
Add Document
</Button>
@@ -381,7 +496,7 @@ export function AddDocumentDialog({
}
// =============================================================================
-// 2. Edit Document Dialog (Updated with English text)
+// 2. Edit Document Dialog
// =============================================================================
interface EditDocumentDialogProps {
open: boolean
@@ -398,29 +513,77 @@ export function EditDocumentDialog({
contractId,
projectType
}: EditDocumentDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
const [formData, setFormData] = React.useState({
title: "",
- pic: "",
vendorDocNumber: "",
+ stagePlanDates: {} as Record<number, string> // stageId -> planDate
})
React.useEffect(() => {
if (document) {
+ // 기본 문서 정보 설정
setFormData({
title: document.title || "",
- pic: document.pic || "",
vendorDocNumber: document.vendorDocNumber || "",
+ stagePlanDates: {}
})
+
+ // 현재 스테이지들의 plan date 설정
+ if (document.allStages) {
+ const planDates: Record<number, string> = {}
+ document.allStages.forEach(stage => {
+ if (stage.planDate) {
+ planDates[stage.id] = stage.planDate
+ }
+ })
+ setFormData(prev => ({
+ ...prev,
+ stagePlanDates: planDates
+ }))
+ }
}
}, [document])
+ const handleStagePlanDateChange = (stageId: number, date: string) => {
+ setFormData({
+ ...formData,
+ stagePlanDates: {
+ ...formData.stagePlanDates,
+ [stageId]: date
+ }
+ })
+ }
+
+ const isFormValid = () => {
+ return formData.title.trim() !== ""
+ }
+
const handleSubmit = async () => {
+ if (!isFormValid() || !document) {
+ toast.error("Please fill in all required fields.")
+ return
+ }
+
+ setIsLoading(true)
try {
- // TODO: API call to update document
- toast.success("Document updated successfully.")
- onOpenChange(false)
+ const result = await updateDocument({
+ documentId: document.id,
+ title: formData.title,
+ vendorDocNumber: formData.vendorDocNumber,
+ stagePlanDates: formData.stagePlanDates,
+ })
+
+ if (result.success) {
+ toast.success("Document updated successfully.")
+ onOpenChange(false)
+ } else {
+ toast.error(result.error || "Error updating document.")
+ }
} catch (error) {
toast.error("Error updating document.")
+ } finally {
+ setIsLoading(false)
}
}
@@ -428,35 +591,38 @@ export function EditDocumentDialog({
return (
<Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="sm:max-w-[500px]">
- <SheetHeader>
+ <SheetContent className="sm:max-w-[600px] h-full flex flex-col">
+ <SheetHeader className="flex-shrink-0">
<SheetTitle>Edit Document</SheetTitle>
<SheetDescription>
- You can modify the basic information of the document.
+ You can modify the document information and stage plan dates.
</SheetDescription>
</SheetHeader>
- <div className="grid gap-4 py-4">
- <div className="grid gap-2">
- <Label>Document Number</Label>
- <div className="p-2 bg-gray-100 rounded text-sm font-mono">
- {document?.docNumber}
+ <div className="flex-1 overflow-y-auto pr-2">
+ <div className="grid gap-4 py-4">
+ {/* Document Number (Read-only) */}
+ <div className="grid gap-2">
+ <Label>Document Number</Label>
+ <div className="p-2 bg-gray-100 rounded text-sm font-mono">
+ {document?.docNumber}
+ </div>
</div>
- </div>
- <div className="grid gap-2">
- <Label htmlFor="edit-title">
- Document Title <span className="text-red-500">*</span>
- </Label>
- <Input
- id="edit-title"
- value={formData.title}
- onChange={(e) => setFormData({ ...formData, title: e.target.value })}
- placeholder="Enter document title"
- />
- </div>
+ {/* Document Title */}
+ <div className="grid gap-2">
+ <Label htmlFor="edit-title">
+ Document Title <span className="text-red-500">*</span>
+ </Label>
+ <Input
+ id="edit-title"
+ value={formData.title}
+ onChange={(e) => setFormData({ ...formData, title: e.target.value })}
+ placeholder="Enter document title"
+ />
+ </div>
- <div className="grid grid-cols-2 gap-4">
+ {/* Vendor Document Number (Plant project only) */}
{isPlantProject && (
<div className="grid gap-2">
<Label htmlFor="edit-vendorDocNumber">Vendor Document Number</Label>
@@ -468,23 +634,50 @@ export function EditDocumentDialog({
/>
</div>
)}
- <div className="grid gap-2">
- <Label htmlFor="edit-pic">PIC</Label>
- <Input
- id="edit-pic"
- value={formData.pic}
- onChange={(e) => setFormData({ ...formData, pic: e.target.value })}
- placeholder="Person in charge"
- />
- </div>
+
+ {/* Current Document Stages with Plan Dates */}
+ {document?.allStages && document.allStages.length > 0 && (
+ <div className="border rounded-lg p-4 bg-green-50/30">
+ <Label className="text-sm font-medium text-green-800 mb-3 block">
+ Document Stages - Plan Dates
+ </Label>
+ <div className="grid gap-3">
+ {document.allStages
+ .sort((a, b) => (a.stageOrder || 0) - (b.stageOrder || 0))
+ .map((stage) => (
+ <div key={stage.id} className="grid grid-cols-2 gap-3 items-center">
+ <div>
+ <Label className="text-sm font-medium">
+ {stage.stageName}
+ </Label>
+ <p className="text-xs text-gray-500">
+ Status: {stage.stageStatus}
+ {stage.actualDate && ` | Completed: ${stage.actualDate}`}
+ </p>
+ </div>
+ <div className="grid gap-1">
+ <Label className="text-xs text-gray-600">Plan Date</Label>
+ <Input
+ type="date"
+ value={formData.stagePlanDates[stage.id] || ""}
+ onChange={(e) => handleStagePlanDateChange(stage.id, e.target.value)}
+ className="text-sm"
+ />
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
</div>
</div>
- <SheetFooter>
- <Button variant="outline" onClick={() => onOpenChange(false)}>
+ <SheetFooter className="flex-shrink-0">
+ <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
Cancel
</Button>
- <Button onClick={handleSubmit}>
+ <Button onClick={handleSubmit} disabled={isLoading || !isFormValid()}>
+ {isLoading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
Save Changes
</Button>
</SheetFooter>
@@ -494,7 +687,7 @@ export function EditDocumentDialog({
}
// =============================================================================
-// 3. Edit Stage Dialog (Updated with English text)
+// 3. Edit Stage Dialog
// =============================================================================
interface EditStageDialogProps {
open: boolean
@@ -687,7 +880,7 @@ export function EditStageDialog({
}
// =============================================================================
-// 4. Excel Import Dialog (Updated with English text)
+// 4. Excel Import Dialog
// =============================================================================
interface ExcelImportDialogProps {
open: boolean
@@ -767,7 +960,7 @@ export function ExcelImportDialog({
<p>• First row must be header row</p>
<p>• Required columns: Document Number, Document Title, Document Class</p>
{projectType === "plant" && (
- <p>• Optional columns: Vendor Document Number, PIC</p>
+ <p>• Optional columns: Vendor Document Number</p>
)}
<p>• Supported formats: .xlsx, .xls</p>
</div>
@@ -786,4 +979,129 @@ export function ExcelImportDialog({
</DialogContent>
</Dialog>
)
-} \ No newline at end of file
+}
+
+// =============================================================================
+// 5. Delete Documents Confirmation Dialog
+// =============================================================================
+
+interface DeleteDocumentsDialogProps
+ extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ documents: Row<DocumentStagesOnlyView>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+export function DeleteDocumentsDialog({
+ documents,
+ showTrigger = true,
+ onSuccess,
+ ...props
+}: DeleteDocumentsDialogProps) {
+
+ const [isDeletePending, startDeleteTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+ const router = useRouter()
+
+ function onDelete() {
+ startDeleteTransition(async () => {
+ const { error } = await deleteDocuments({
+ ids: documents.map((document) => document.documentId),
+ })
+
+ if (error) {
+ toast.error(error)
+ return
+ }
+
+ props.onOpenChange?.(false)
+ toast.success("Documents deleted")
+
+ router.refresh()
+ onSuccess?.()
+ })
+ }
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger ? (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ Delete ({documents.length})
+ </Button>
+ </DialogTrigger>
+ ) : null}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Are you absolutely sure?</DialogTitle>
+ <DialogDescription>
+ This action cannot be undone. This will permanently delete your{" "}
+ <span className="font-medium">{documents.length}</span>
+ {documents.length === 1 ? " document" : " documents"} from our servers.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="gap-2 sm:space-x-0">
+ <DialogClose asChild>
+ <Button variant="outline">Cancel</Button>
+ </DialogClose>
+ <Button
+ aria-label="Delete selected rows"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ Delete
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger ? (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Trash className="mr-2 size-4" aria-hidden="true" />
+ Delete ({documents.length})
+ </Button>
+ </DrawerTrigger>
+ ) : null}
+ <DrawerContent>
+ <DrawerHeader>
+ <DrawerTitle>Are you absolutely sure?</DrawerTitle>
+ <DrawerDescription>
+ This action cannot be undone. This will permanently delete your{" "}
+ <span className="font-medium">{documents.length}</span>
+ {documents.length === 1 ? " document" : " documents"} from our servers.
+ </DrawerDescription>
+ </DrawerHeader>
+ <DrawerFooter className="gap-2 sm:space-x-0">
+ <DrawerClose asChild>
+ <Button variant="outline">Cancel</Button>
+ </DrawerClose>
+ <Button
+ aria-label="Delete selected rows"
+ variant="destructive"
+ onClick={onDelete}
+ disabled={isDeletePending}
+ >
+ {isDeletePending && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ Delete
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+}
diff --git a/lib/vendor-document-list/plant/document-stage-toolbar.tsx b/lib/vendor-document-list/plant/document-stage-toolbar.tsx
new file mode 100644
index 00000000..87b221b7
--- /dev/null
+++ b/lib/vendor-document-list/plant/document-stage-toolbar.tsx
@@ -0,0 +1,103 @@
+"use client"
+
+import * as React from "react"
+import { type DocumentStagesOnlyView } from "@/db/schema"
+import { type Table } from "@tanstack/react-table"
+import { Download, Upload, Plus, FileSpreadsheet } from "lucide-react"
+import { toast } from "sonner"
+
+import { exportTableToExcel } from "@/lib/export"
+import { Button } from "@/components/ui/button"
+
+// 다이얼로그 컴포넌트들 import
+import {
+ DeleteDocumentsDialog,
+ AddDocumentDialog,
+ ExcelImportDialog
+} from "./document-stage-dialogs"
+
+// 서버 액션 import (필요한 경우)
+// import { importDocumentsExcel } from "./document-stages-service"
+
+interface DocumentsTableToolbarActionsProps {
+ table: Table<DocumentStagesOnlyView>
+ contractId: number
+ projectType: "ship" | "plant"
+}
+
+export function DocumentsTableToolbarActions({
+ table,
+ contractId,
+ projectType
+}: DocumentsTableToolbarActionsProps) {
+ // 다이얼로그 상태 관리
+ const [showAddDialog, setShowAddDialog] = React.useState(false)
+ const [showExcelImportDialog, setShowExcelImportDialog] = React.useState(false)
+
+ const handleExcelImport = () => {
+ setShowExcelImportDialog(true)
+ }
+ function handleDeleteSuccess() {
+ // 삭제 성공 후 모든 선택 해제
+ table.toggleAllRowsSelected(false)
+ toast.success("Selected documents deleted successfully")
+ }
+
+ function handleExport() {
+ exportTableToExcel(table, {
+ filename: `documents_contract_${contractId}`,
+ excludeColumns: ["select", "actions"], // 체크박스와 액션 컬럼 제외
+ })
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ {/* 1) 선택된 문서가 있으면 삭제 다이얼로그 */}
+ {table.getFilteredSelectedRowModel().rows.length > 0 ? (
+ <DeleteDocumentsDialog
+ documents={table
+ .getFilteredSelectedRowModel()
+ .rows.map((row) => row.original)}
+ onSuccess={() => table.toggleAllRowsSelected(false)}
+ />
+ ) : null}
+
+
+ {/* 2) 새 문서 추가 다이얼로그 */}
+ <Button onClick={() => setShowAddDialog(true)} size="sm" className="gap-2">
+ <Plus className="h-4 w-4" />
+ <span className="hidden sm:inline">Add Document</span>
+ </Button>
+
+ <AddDocumentDialog
+ open={showAddDialog}
+ onOpenChange={setShowAddDialog}
+ contractId={contractId}
+ projectType={projectType}
+ />
+
+ <Button onClick={handleExcelImport} variant="outline" size="sm">
+ <FileSpreadsheet className="mr-2 h-4 w-4" />
+ Excel Import
+ </Button>
+
+ <ExcelImportDialog
+ open={showExcelImportDialog}
+ onOpenChange={setShowExcelImportDialog}
+ contractId={contractId}
+ projectType={projectType}
+ />
+
+ {/* 4) Export 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleExport}
+ className="gap-2"
+ >
+ <Download className="h-4 w-4" />
+ <span className="hidden sm:inline">Export</span>
+ </Button>
+ </div>
+ )
+}
diff --git a/lib/vendor-document-list/plant/document-stages-columns.tsx b/lib/vendor-document-list/plant/document-stages-columns.tsx
index d39af4e8..7456c2aa 100644
--- a/lib/vendor-document-list/plant/document-stages-columns.tsx
+++ b/lib/vendor-document-list/plant/document-stages-columns.tsx
@@ -328,7 +328,7 @@ export function getDocumentStagesColumns({
return (
<div className="flex items-center gap-2">
- <span className="text-sm">{formatDate(doc.currentStagePlanDate, 'MM/dd')}</span>
+ <span className="text-sm">{formatDate(doc.currentStagePlanDate)}</span>
<DueDateInfo
daysUntilDue={doc.daysUntilDue}
isOverdue={doc.isOverdue || false}
@@ -336,7 +336,7 @@ export function getDocumentStagesColumns({
</div>
)
},
- size: 120,
+ size: 150,
enableResizing: true,
meta: {
excelHeader: "Plan Date"
@@ -425,7 +425,7 @@ export function getDocumentStagesColumns({
key: "edit_document",
label: "Edit Document",
icon: Edit,
- action: () => setRowAction({ row, type: "edit_document" }),
+ action: () => setRowAction({ row, type: "update" }),
show: true
}
]
@@ -502,9 +502,9 @@ export function getDocumentStagesColumns({
>
<action.icon className="mr-2 h-3 w-3" />
<span className="text-xs">{action.label}</span>
- {action.shortcut && (
+ {/* {action.shortcut && (
<DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
- )}
+ )} */}
</DropdownMenuItem>
))}
</>
diff --git a/lib/vendor-document-list/plant/document-stages-expanded-content.tsx b/lib/vendor-document-list/plant/document-stages-expanded-content.tsx
index 2f6b637c..070d6904 100644
--- a/lib/vendor-document-list/plant/document-stages-expanded-content.tsx
+++ b/lib/vendor-document-list/plant/document-stages-expanded-content.tsx
@@ -106,13 +106,13 @@ export function DocumentStagesExpandedContent({
{planDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
- <span>계획: {formatDate(planDate.toISOString(), 'MM/dd')}</span>
+ <span>계획: {formatDate(planDate.toISOString())}</span>
</div>
)}
{actualDate && (
<div className="flex items-center gap-1">
<CheckCircle className="h-3 w-3 text-green-500" />
- <span>실적: {formatDate(actualDate.toISOString(), 'MM/dd')}</span>
+ <span>실적: {formatDate(actualDate.toISOString())}</span>
</div>
)}
</div>
diff --git a/lib/vendor-document-list/plant/document-stages-service.ts b/lib/vendor-document-list/plant/document-stages-service.ts
index 2fd20fa4..108b5869 100644
--- a/lib/vendor-document-list/plant/document-stages-service.ts
+++ b/lib/vendor-document-list/plant/document-stages-service.ts
@@ -4,8 +4,8 @@
import { revalidatePath, revalidateTag } from "next/cache"
import { redirect } from "next/navigation"
import db from "@/db/db"
-import { codeGroups, comboBoxSettings, documentClassOptions, documentClasses, documentNumberTypeConfigs, documentNumberTypes, documentStagesOnlyView, documents, issueStages } from "@/db/schema"
-import { and, eq, asc, desc, sql, inArray, max } from "drizzle-orm"
+import { codeGroups, comboBoxSettings, contracts, documentClassOptions, documentClasses, documentNumberTypeConfigs, documentNumberTypes, documentStagesOnlyView, documents, issueStages } from "@/db/schema"
+import { and, eq, asc, desc, sql, inArray, max, ne ,or, ilike} from "drizzle-orm"
import {
createDocumentSchema,
updateDocumentSchema,
@@ -31,163 +31,57 @@ import { filterColumns } from "@/lib/filter-columns"
import { GetEnhancedDocumentsSchema } from "../enhanced-document-service"
import { countDocumentStagesOnly, selectDocumentStagesOnly } from "../repository"
-// =============================================================================
-// 1. 문서 관련 액션들
-// =============================================================================
+interface UpdateDocumentData {
+ documentId: number
+ title: string
+ vendorDocNumber?: string
+ stagePlanDates: Record<number, string> // stageId -> planDate
+}
-// 문서 생성
-// export async function createDocument(input: CreateDocumentInput) {
-// noStore()
-
-// try {
-// // 입력값 검증
-// const validatedData = createDocumentSchema.parse(input)
-
-// // 프로젝트 타입 확인 (계약 정보에서 가져와야 함)
-// const contract = await db.query.contracts.findFirst({
-// where: eq(documents.contractId, validatedData.contractId),
-// with: { project: true }
-// })
-
-// if (!contract) {
-// throw new Error("계약 정보를 찾을 수 없습니다")
-// }
-
-// const projectType = contract.project?.type === "plant" ? "plant" : "ship"
-
-// // 문서번호 유효성 검사
-// if (!validateDocNumber(validatedData.docNumber, projectType)) {
-// throw new Error(`${projectType === "ship" ? "선박" : "플랜트"} 프로젝트의 문서번호 형식에 맞지 않습니다`)
-// }
-
-// // B4 필드 유효성 검사
-// validateB4Fields(validatedData)
-
-// // 문서번호 중복 검사
-// const existingDoc = await db.query.documents.findFirst({
-// where: and(
-// eq(documents.contractId, validatedData.contractId),
-// eq(documents.docNumber, validatedData.docNumber),
-// eq(documents.status, "ACTIVE")
-// )
-// })
-
-// if (existingDoc) {
-// throw new Error("이미 존재하는 문서번호입니다")
-// }
-
-// // 문서 생성
-// const [newDocument] = await db.insert(documents).values({
-// contractId: validatedData.contractId,
-// docNumber: validatedData.docNumber,
-// title: validatedData.title,
-// drawingKind: validatedData.drawingKind,
-// vendorDocNumber: validatedData.vendorDocNumber || null,
-// pic: validatedData.pic || null,
-// issuedDate: validatedData.issuedDate || null,
-// drawingMoveGbn: validatedData.drawingMoveGbn || null,
-// discipline: validatedData.discipline || null,
-// externalDocumentId: validatedData.externalDocumentId || null,
-// externalSystemType: validatedData.externalSystemType || null,
-// cGbn: validatedData.cGbn || null,
-// dGbn: validatedData.dGbn || null,
-// degreeGbn: validatedData.degreeGbn || null,
-// deptGbn: validatedData.deptGbn || null,
-// jGbn: validatedData.jGbn || null,
-// sGbn: validatedData.sGbn || null,
-// shiDrawingNo: validatedData.shiDrawingNo || null,
-// manager: validatedData.manager || null,
-// managerENM: validatedData.managerENM || null,
-// managerNo: validatedData.managerNo || null,
-// status: "ACTIVE",
-// createdAt: new Date(),
-// updatedAt: new Date(),
-// }).returning()
-
-// // 캐시 무효화
-// revalidateTag(`documents-${validatedData.contractId}`)
-// revalidatePath(`/contracts/${validatedData.contractId}/documents`)
-
-// return {
-// success: true,
-// data: newDocument,
-// message: "문서가 성공적으로 생성되었습니다"
-// }
-
-// } catch (error) {
-// console.error("Error creating document:", error)
-// return {
-// success: false,
-// error: error instanceof Error ? error.message : "문서 생성 중 오류가 발생했습니다"
-// }
-// }
-// }
// 문서 수정
-export async function updateDocument(input: UpdateDocumentInput) {
- noStore()
-
+export async function updateDocument(data: UpdateDocumentData) {
try {
- const validatedData = updateDocumentSchema.parse(input)
-
- // 문서 존재 확인
- const existingDoc = await db.query.documents.findFirst({
- where: eq(documents.id, validatedData.id)
- })
-
- if (!existingDoc) {
- throw new Error("문서를 찾을 수 없습니다")
- }
-
- // B4 필드 유효성 검사 (drawingKind 변경 시)
- if (validatedData.drawingKind) {
- validateB4Fields(validatedData)
- }
-
- // 문서번호 중복 검사 (문서번호 변경 시)
- if (validatedData.docNumber && validatedData.docNumber !== existingDoc.docNumber) {
- const duplicateDoc = await db.query.documents.findFirst({
- where: and(
- eq(documents.contractId, existingDoc.contractId),
- eq(documents.docNumber, validatedData.docNumber),
- eq(documents.status, "ACTIVE")
- )
- })
-
- if (duplicateDoc) {
- throw new Error("이미 존재하는 문서번호입니다")
- }
- }
-
- // 문서 업데이트
+ // 1. 문서 기본 정보 업데이트
const [updatedDocument] = await db
.update(documents)
.set({
- ...validatedData,
+ title: data.title,
+ vendorDocNumber: data.vendorDocNumber || null,
updatedAt: new Date(),
})
- .where(eq(documents.id, validatedData.id))
+ .where(eq(documents.id, data.documentId))
.returning()
-
- // 캐시 무효화
- revalidateTag(`documents-${existingDoc.contractId}`)
- revalidatePath(`/contracts/${existingDoc.contractId}/documents`)
+
+ if (!updatedDocument) {
+ return { success: false, error: "문서를 찾을 수 없습니다." }
+ }
+
+ // 2. 스테이지들의 plan date 업데이트
+ const stageUpdatePromises = Object.entries(data.stagePlanDates).map(([stageId, planDate]) => {
+ return db
+ .update(issueStages)
+ .set({
+ planDate: planDate || null,
+ updatedAt: new Date(),
+ })
+ .where(eq(issueStages.id, Number(stageId)))
+ })
+
+ await Promise.all(stageUpdatePromises)
+
+ // 3. 캐시 무효화
+ revalidatePath(`/contracts/${updatedDocument.contractId}/documents`)
return {
success: true,
- data: updatedDocument,
- message: "문서가 성공적으로 수정되었습니다"
+ data: updatedDocument
}
-
} catch (error) {
- console.error("Error updating document:", error)
- return {
- success: false,
- error: error instanceof Error ? error.message : "문서 수정 중 오류가 발생했습니다"
- }
+ console.error("문서 업데이트 실패:", error)
+ return { success: false, error: "문서 업데이트 중 오류가 발생했습니다." }
}
}
-
// 문서 삭제 (소프트 삭제)
export async function deleteDocument(input: { id: number }) {
noStore()
@@ -240,6 +134,82 @@ export async function deleteDocument(input: { id: number }) {
}
}
+interface DeleteDocumentsData {
+ ids: number[]
+}
+
+export async function deleteDocuments(data: DeleteDocumentsData) {
+ try {
+ if (data.ids.length === 0) {
+ return { success: false, error: "삭제할 문서가 선택되지 않았습니다." }
+ }
+
+ /* 1. 요청한 문서가 존재하는지 확인 ------------------------------------ */
+ const existingDocs = await db
+ .select({ id: documents.id, docNumber: documents.docNumber })
+ .from(documents)
+ .where(and(
+ inArray(documents.id, data.ids),
+ ))
+
+ if (existingDocs.length === 0) {
+ return { success: false, error: "삭제할 문서를 찾을 수 없습니다." }
+ }
+
+ if (existingDocs.length !== data.ids.length) {
+ return {
+ success: false,
+ error: "일부 문서를 찾을 수 없거나 이미 삭제되었습니다."
+ }
+ }
+
+ /* 2. 연관 스테이지 건수 파악(로그·메시지용) --------------------------- */
+ const relatedStages = await db
+ .select({ documentId: issueStages.documentId })
+ .from(issueStages)
+ .where(inArray(issueStages.documentId, data.ids))
+
+ const stagesToDelete = relatedStages.length
+
+ /* 3. 연관 스테이지 삭제 --------------------------------------------- */
+ // ─> FK에 ON DELETE CASCADE 가 있다면 생략 가능.
+ if (stagesToDelete > 0) {
+ await db
+ .delete(issueStages)
+ .where(inArray(issueStages.documentId, data.ids))
+ }
+
+ /* 4. 문서 하드 삭제 --------------------------------------------------- */
+ const deletedDocs = await db
+ .delete(documents)
+ .where(and(
+ inArray(documents.id, data.ids),
+ ))
+ .returning({ id: documents.id, docNumber: documents.docNumber })
+
+ /* 5. 캐시 무효화 ------------------------------------------------------ */
+
+ return {
+ success: true,
+ message: `${deletedDocs.length}개의 문서가 완전히 삭제되었습니다.`,
+ data: {
+ deletedCount: deletedDocs.length,
+ deletedDocuments: deletedDocs,
+ stagesDeletedCount: stagesToDelete
+ }
+ }
+ } catch (error) {
+ console.error("문서 삭제 실패:", error)
+ return {
+ success: false,
+ error: error instanceof Error
+ ? error.message
+ : "문서 삭제 중 오류가 발생했습니다."
+ }
+ }
+}
+
+
// =============================================================================
// 2. 스테이지 관련 액션들
// =============================================================================
@@ -840,64 +810,105 @@ export async function generateDocumentNumber(configs: any[], values: Record<stri
return docNumber.replace(/-$/, "") // 마지막 하이픈 제거
}
-// 문서 생성
-export async function createDocument(data: {
+interface CreateDocumentData {
contractId: number
documentNumberTypeId: number
documentClassId: number
title: string
+ docNumber: string
fieldValues: Record<string, string>
+ planDates: Record<number, string> // optionId -> planDate
pic?: string
vendorDocNumber?: string
-}) {
+}
+
+// 문서 생성
+export async function createDocument(data: CreateDocumentData) {
try {
- // 1. 문서번호 타입 설정 조회
- const configsResult = await getDocumentNumberTypeConfigs(data.documentNumberTypeId)
+ /* ──────────────────────────────── 0. 계약 확인 & projectId 가져오기 ─────────────────────────────── */
+ const contract = await db.query.contracts.findFirst({
+ where: eq(contracts.id, data.contractId),
+ columns: {
+ id: true,
+ projectId: true,
+ },
+ })
+
+ if (!contract) {
+ return { success: false, error: "유효하지 않은 계약(ID)입니다." }
+ }
+ const { projectId } = contract
+
+ /* ──────────────────────────────── 1. 문서번호 타입 설정 조회 ─────────────────────────────── */
+ const configsResult = await getDocumentNumberTypeConfigs(
+ data.documentNumberTypeId
+ )
if (!configsResult.success) {
return { success: false, error: configsResult.error }
}
- // 2. 문서번호 생성
- const documentNumber = generateDocumentNumber(configsResult.data, data.fieldValues)
- // 3. 문서 생성 (실제 documents 테이블에 INSERT)
- // TODO: 실제 documents 테이블 스키마에 맞게 수정 필요
- /*
- const [document] = await db.insert(documents).values({
+ /* ──────────────────────────────── 3. 문서 레코드 삽입 ─────────────────────────────── */
+ const insertData = {
+ // 필수
+ projectId, // ★ 새로 추가
contractId: data.contractId,
- docNumber: documentNumber,
+ docNumber: data.docNumber,
title: data.title,
- documentClassId: data.documentClassId,
- pic: data.pic,
- vendorDocNumber: data.vendorDocNumber,
- }).returning()
- */
+ status: "ACTIVE" as const,
+ createdAt: new Date(),
+ updatedAt: new Date(),
- // 4. 문서 클래스의 옵션들을 스테이지로 자동 생성
- const stageOptionsResult = await getDocumentClassOptions(data.documentClassId)
+ // 선택
+ pic: data.pic ?? null,
+ vendorDocNumber: data.vendorDocNumber ?? null,
+
+ }
+
+ const [document] = await db
+ .insert(documents)
+ .values(insertData)
+ .onConflictDoNothing({
+ // ★ 유니크 키가 projectId 기반이라면 target 도 같이 변경
+ target: [
+ documents.projectId,
+ documents.docNumber,
+ documents.status,
+ ],
+ })
+ .returning()
+
+ if (!document) {
+ return {
+ success: false,
+ error: "같은 프로젝트·문서번호·상태의 문서가 이미 존재합니다.",
+ }
+ }
+
+ /* ──────────────────────────────── 4. 스테이지 자동 생성 ─────────────────────────────── */
+ const stageOptionsResult = await getDocumentClassOptions(
+ data.documentClassId
+ )
if (stageOptionsResult.success && stageOptionsResult.data.length > 0) {
- // TODO: 실제 stages 테이블에 스테이지들 생성
- /*
- const stageInserts = stageOptionsResult.data.map((option, index) => ({
- documentId: document.id,
- stageName: option.optionValue,
- stageOrder: option.sortOrder || index + 1,
- stageStatus: 'PLANNED',
- // 기본값들...
+ const now = new Date()
+ const stageInserts = stageOptionsResult.data.map((opt, idx) => ({
+ documentId: document.id,
+ stageName: opt.optionValue,
+ stageOrder: opt.sortOrder ?? idx + 1,
+ stageStatus: "PLANNED" as const,
+ planDate: data.planDates[opt.id] ?? null,
+ createdAt: now,
+ updatedAt: now,
}))
-
- await db.insert(documentStages).values(stageInserts)
- */
+ await db.insert(issueStages).values(stageInserts)
}
+ /* ──────────────────────────────── 5. 캐시 무효화 및 응답 ─────────────────────────────── */
revalidatePath(`/contracts/${data.contractId}/documents`)
-
- return {
- success: true,
- data: {
- documentNumber,
- // document
- }
+
+ return {
+ success: true,
+ data: { document },
}
} catch (error) {
console.error("문서 생성 실패:", error)
@@ -905,37 +916,6 @@ export async function createDocument(data: {
}
}
-// 스테이지 업데이트
-// export async function updateStage(data: {
-// stageId: number
-// stageName?: string
-// planDate?: string
-// actualDate?: string
-// stageStatus?: string
-// assigneeName?: string
-// priority?: string
-// notes?: string
-// }) {
-// try {
-// // TODO: 실제 stages 테이블 업데이트
-// /*
-// await db
-// .update(documentStages)
-// .set({
-// ...data,
-// updatedAt: new Date(),
-// })
-// .where(eq(documentStages.id, data.stageId))
-// */
-
-// revalidatePath("/contracts/[contractId]/documents", "page")
-
-// return { success: true }
-// } catch (error) {
-// console.error("스테이지 업데이트 실패:", error)
-// return { success: false, error: "스테이지 업데이트 중 오류가 발생했습니다." }
-// }
-// }
export async function getDocumentStagesOnly(
input: GetEnhancedDocumentsSchema,
diff --git a/lib/vendor-document-list/plant/document-stages-table.tsx b/lib/vendor-document-list/plant/document-stages-table.tsx
index 736a7467..7d41277e 100644
--- a/lib/vendor-document-list/plant/document-stages-table.tsx
+++ b/lib/vendor-document-list/plant/document-stages-table.tsx
@@ -13,11 +13,11 @@ import type { DocumentStagesOnlyView } from "@/db/schema"
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import {
- AlertTriangle,
- Clock,
- TrendingUp,
- Target,
+import {
+ AlertTriangle,
+ Clock,
+ TrendingUp,
+ Target,
Users,
Plus,
FileSpreadsheet
@@ -27,10 +27,11 @@ import { ExpandableDataTable } from "@/components/data-table/expandable-data-tab
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { DocumentStagesExpandedContent } from "./document-stages-expanded-content"
-import { AddDocumentDialog } from "./document-stage-dialogs"
+import { AddDocumentDialog, DeleteDocumentsDialog } from "./document-stage-dialogs"
import { EditDocumentDialog } from "./document-stage-dialogs"
import { EditStageDialog } from "./document-stage-dialogs"
import { ExcelImportDialog } from "./document-stage-dialogs"
+import { DocumentsTableToolbarActions } from "./document-stage-toolbar"
interface DocumentStagesTableProps {
promises: Promise<[Awaited<ReturnType<typeof getDocumentStagesOnly>>]>
@@ -45,33 +46,32 @@ export function DocumentStagesTable({
}: DocumentStagesTableProps) {
const [{ data, pageCount, total }] = React.use(promises)
- console.log(data)
// 상태 관리
const [rowAction, setRowAction] = React.useState<DataTableRowAction<DocumentStagesOnlyView> | null>(null)
const [expandedRows, setExpandedRows] = React.useState<Set<string>>(new Set())
const [quickFilter, setQuickFilter] = React.useState<'all' | 'overdue' | 'due_soon' | 'in_progress' | 'high_priority'>('all')
-
+
// 다이얼로그 상태들
const [addDocumentOpen, setAddDocumentOpen] = React.useState(false)
const [editDocumentOpen, setEditDocumentOpen] = React.useState(false)
const [editStageOpen, setEditStageOpen] = React.useState(false)
const [excelImportOpen, setExcelImportOpen] = React.useState(false)
-
+
// 선택된 항목들
const [selectedDocument, setSelectedDocument] = React.useState<DocumentStagesOnlyView | null>(null)
const [selectedStageId, setSelectedStageId] = React.useState<number | null>(null)
// 컬럼 정의
const columns = React.useMemo(
- () => getDocumentStagesColumns({
+ () => getDocumentStagesColumns({
setRowAction: (action) => {
setRowAction(action)
if (action) {
setSelectedDocument(action.row.original)
-
+
switch (action.type) {
- case "edit_document":
+ case "update":
setEditDocumentOpen(true)
break
case "edit_stage":
@@ -102,24 +102,24 @@ export function DocumentStagesTable({
const stats = React.useMemo(() => {
const totalDocs = data.length
const overdue = data.filter(doc => doc.isOverdue).length
- const dueSoon = data.filter(doc =>
- doc.daysUntilDue !== null &&
- doc.daysUntilDue >= 0 &&
+ const dueSoon = data.filter(doc =>
+ doc.daysUntilDue !== null &&
+ doc.daysUntilDue >= 0 &&
doc.daysUntilDue <= 3
).length
const inProgress = data.filter(doc => doc.currentStageStatus === 'IN_PROGRESS').length
const highPriority = data.filter(doc => doc.currentStagePriority === 'HIGH').length
- const avgProgress = totalDocs > 0
+ const avgProgress = totalDocs > 0
? Math.round(data.reduce((sum, doc) => sum + (doc.progressPercentage || 0), 0) / totalDocs)
: 0
-
- return {
- total: totalDocs,
- overdue,
- dueSoon,
- inProgress,
- highPriority,
- avgProgress
+
+ return {
+ total: totalDocs,
+ overdue,
+ dueSoon,
+ inProgress,
+ highPriority,
+ avgProgress
}
}, [data])
@@ -129,9 +129,9 @@ export function DocumentStagesTable({
case 'overdue':
return data.filter(doc => doc.isOverdue)
case 'due_soon':
- return data.filter(doc =>
- doc.daysUntilDue !== null &&
- doc.daysUntilDue >= 0 &&
+ return data.filter(doc =>
+ doc.daysUntilDue !== null &&
+ doc.daysUntilDue >= 0 &&
doc.daysUntilDue <= 3
)
case 'in_progress':
@@ -158,7 +158,7 @@ export function DocumentStagesTable({
const stageIds = selectedRows
.map(row => row.original.currentStageId)
.filter(Boolean)
-
+
if (stageIds.length > 0) {
toast.success(`${stageIds.length}개 스테이지가 완료 처리되었습니다.`)
}
@@ -189,7 +189,7 @@ export function DocumentStagesTable({
},
{
label: "제목",
- value: "title",
+ value: "title",
placeholder: "제목으로 검색...",
},
]
@@ -280,7 +280,7 @@ export function DocumentStagesTable({
</p>
</CardContent>
</Card>
-
+
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('overdue')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">지연 문서</CardTitle>
@@ -291,7 +291,7 @@ export function DocumentStagesTable({
<p className="text-xs text-muted-foreground">즉시 확인 필요</p>
</CardContent>
</Card>
-
+
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setQuickFilter('due_soon')}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">마감 임박</CardTitle>
@@ -302,7 +302,7 @@ export function DocumentStagesTable({
<p className="text-xs text-muted-foreground">3일 이내 마감</p>
</CardContent>
</Card>
-
+
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">평균 진행률</CardTitle>
@@ -317,14 +317,14 @@ export function DocumentStagesTable({
{/* 빠른 필터 */}
<div className="flex gap-2 overflow-x-auto pb-2">
- <Badge
+ <Badge
variant={quickFilter === 'all' ? 'default' : 'outline'}
className="cursor-pointer hover:bg-primary hover:text-primary-foreground whitespace-nowrap"
onClick={() => setQuickFilter('all')}
>
전체 ({stats.total})
</Badge>
- <Badge
+ <Badge
variant={quickFilter === 'overdue' ? 'destructive' : 'outline'}
className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground whitespace-nowrap"
onClick={() => setQuickFilter('overdue')}
@@ -332,7 +332,7 @@ export function DocumentStagesTable({
<AlertTriangle className="w-3 h-3 mr-1" />
지연 ({stats.overdue})
</Badge>
- <Badge
+ <Badge
variant={quickFilter === 'due_soon' ? 'default' : 'outline'}
className="cursor-pointer hover:bg-orange-500 hover:text-white whitespace-nowrap"
onClick={() => setQuickFilter('due_soon')}
@@ -340,7 +340,7 @@ export function DocumentStagesTable({
<Clock className="w-3 h-3 mr-1" />
마감임박 ({stats.dueSoon})
</Badge>
- <Badge
+ <Badge
variant={quickFilter === 'in_progress' ? 'default' : 'outline'}
className="cursor-pointer hover:bg-blue-500 hover:text-white whitespace-nowrap"
onClick={() => setQuickFilter('in_progress')}
@@ -348,7 +348,7 @@ export function DocumentStagesTable({
<Users className="w-3 h-3 mr-1" />
진행중 ({stats.inProgress})
</Badge>
- <Badge
+ <Badge
variant={quickFilter === 'high_priority' ? 'destructive' : 'outline'}
className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground whitespace-nowrap"
onClick={() => setQuickFilter('high_priority')}
@@ -361,13 +361,13 @@ export function DocumentStagesTable({
{/* 메인 테이블 */}
<div className="space-y-4">
<div className="rounded-md border bg-white overflow-hidden">
- <ExpandableDataTable
+ <ExpandableDataTable
table={table}
expandable={true}
expandedRows={expandedRows}
setExpandedRows={setExpandedRows}
renderExpandedContent={(document) => (
- <DocumentStagesExpandedContent
+ <DocumentStagesExpandedContent
document={document}
onEditStage={(stageId) => {
setSelectedDocument(document)
@@ -379,8 +379,8 @@ export function DocumentStagesTable({
)}
expandedRowClassName="!p-0"
excludeFromClick={[
- 'actions',
- 'select'
+ 'actions',
+ 'select'
]}
>
<DataTableAdvancedToolbar
@@ -388,16 +388,11 @@ export function DocumentStagesTable({
filterFields={advancedFilterFields}
shallow={false}
>
- <div className="flex items-center gap-2">
- <Button onClick={handleNewDocument} size="sm">
- <Plus className="mr-2 h-4 w-4" />
- 문서 추가
- </Button>
- <Button onClick={handleExcelImport} variant="outline" size="sm">
- <FileSpreadsheet className="mr-2 h-4 w-4" />
- 엑셀 가져오기
- </Button>
- </div>
+ <DocumentsTableToolbarActions
+ table={table}
+ contractId={contractId}
+ projectType={projectType}
+ />
</DataTableAdvancedToolbar>
</ExpandableDataTable>
</div>
@@ -444,6 +439,16 @@ export function DocumentStagesTable({
contractId={contractId}
projectType={projectType}
/>
+
+ <DeleteDocumentsDialog
+ open={rowAction?.type === "delete"}
+ onOpenChange={() => setRowAction(null)}
+ showTrigger={false}
+ documents={rowAction?.row.original ? [rowAction?.row.original] : []} // 전체 문서 배열
+ onSuccess={() => rowAction?.row.toggleSelected(false)}
+ />
+
+
</div>
)
} \ No newline at end of file