summaryrefslogtreecommitdiff
path: root/lib/pq/pq-review-table-new
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pq/pq-review-table-new')
-rw-r--r--lib/pq/pq-review-table-new/edit-investigation-dialog.tsx123
-rw-r--r--lib/pq/pq-review-table-new/request-investigation-dialog.tsx4
-rw-r--r--lib/pq/pq-review-table-new/site-visit-dialog.tsx179
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-columns.tsx20
-rw-r--r--lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx55
5 files changed, 283 insertions, 98 deletions
diff --git a/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx b/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx
index 4df7a7ec..7fd1c3f8 100644
--- a/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx
+++ b/lib/pq/pq-review-table-new/edit-investigation-dialog.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
-import { CalendarIcon, Loader } from "lucide-react"
+import { CalendarIcon, Loader, Upload, X, FileText } from "lucide-react"
import { format } from "date-fns"
import { toast } from "sonner"
@@ -49,6 +49,7 @@ const editInvestigationSchema = z.object({
]).optional(),
evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "REJECTED"]).optional(),
investigationNotes: z.string().max(1000, "QM 의견은 1000자 이내로 입력해주세요.").optional(),
+ attachments: z.array(z.instanceof(File)).optional(),
})
type EditInvestigationSchema = z.infer<typeof editInvestigationSchema>
@@ -72,6 +73,8 @@ export function EditInvestigationDialog({
onSubmit,
}: EditInvestigationDialogProps) {
const [isPending, startTransition] = React.useTransition()
+ const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
+ const fileInputRef = React.useRef<HTMLInputElement>(null)
const form = useForm<EditInvestigationSchema>({
resolver: zodResolver(editInvestigationSchema),
@@ -79,6 +82,7 @@ export function EditInvestigationDialog({
confirmedAt: investigation?.confirmedAt || undefined,
evaluationResult: investigation?.evaluationResult as "APPROVED" | "SUPPLEMENT" | "REJECTED" | undefined,
investigationNotes: investigation?.investigationNotes || "",
+ attachments: [],
},
})
@@ -89,14 +93,47 @@ export function EditInvestigationDialog({
confirmedAt: investigation.confirmedAt || undefined,
evaluationResult: investigation.evaluationResult as "APPROVED" | "SUPPLEMENT" | "REJECTED" | undefined,
investigationNotes: investigation.investigationNotes || "",
+ attachments: [],
})
+ setSelectedFiles([])
}
}, [investigation, form])
+ // 파일 선택 핸들러
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const files = Array.from(event.target.files || [])
+ if (files.length > 0) {
+ const newFiles = [...selectedFiles, ...files]
+ setSelectedFiles(newFiles)
+ form.setValue('attachments', newFiles, { shouldValidate: true })
+ }
+ }
+
+ // 파일 제거 핸들러
+ const removeFile = (index: number) => {
+ const updatedFiles = selectedFiles.filter((_, i) => i !== index)
+ setSelectedFiles(updatedFiles)
+ form.setValue('attachments', updatedFiles, { shouldValidate: true })
+ }
+
+ // 파일 크기 포맷팅
+ const formatFileSize = (bytes: number) => {
+ if (bytes === 0) return '0 Bytes'
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+ }
+
const handleSubmit = async (values: EditInvestigationSchema) => {
startTransition(async () => {
try {
- await onSubmit(values)
+ // 선택된 파일들을 values에 포함
+ const submitData = {
+ ...values,
+ attachments: selectedFiles,
+ }
+ await onSubmit(submitData)
toast.success("실사 정보가 업데이트되었습니다!")
onClose()
} catch (error) {
@@ -181,16 +218,16 @@ export function EditInvestigationDialog({
)}
/>
- {/* QM 의견 */}
+ {/* 구매 의견 */}
<FormField
control={form.control}
name="investigationNotes"
render={({ field }) => (
<FormItem>
- <FormLabel>QM 의견</FormLabel>
+ <FormLabel>구매 의견</FormLabel>
<FormControl>
<Textarea
- placeholder="실사에 대한 QM 의견을 입력하세요..."
+ placeholder="실사에 대한 구매 의견을 입력하세요..."
{...field}
className="min-h-[80px]"
/>
@@ -200,6 +237,82 @@ export function EditInvestigationDialog({
)}
/>
+ {/* 첨부파일 */}
+ <FormField
+ control={form.control}
+ name="attachments"
+ render={() => (
+ <FormItem>
+ <FormLabel>첨부파일</FormLabel>
+ <FormControl>
+ <div className="space-y-4">
+ {/* 파일 선택 영역 */}
+ <div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
+ <input
+ ref={fileInputRef}
+ type="file"
+ multiple
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.gif"
+ onChange={handleFileSelect}
+ className="hidden"
+ />
+ <Upload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
+ <div className="text-sm text-gray-600 mb-2">
+ 파일을 드래그하거나 클릭하여 선택하세요
+ </div>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => fileInputRef.current?.click()}
+ disabled={isPending}
+ >
+ 파일 선택
+ </Button>
+ <div className="text-xs text-gray-500 mt-2">
+ 지원 형식: PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, JPEG, GIF (최대 10MB)
+ </div>
+ </div>
+
+ {/* 선택된 파일 목록 */}
+ {selectedFiles.length > 0 && (
+ <div className="space-y-2">
+ <div className="text-sm font-medium">선택된 파일:</div>
+ {selectedFiles.map((file, index) => (
+ <div
+ key={index}
+ className="flex items-center justify-between p-2 bg-gray-50 rounded border"
+ >
+ <div className="flex items-center space-x-2">
+ <FileText className="h-4 w-4 text-gray-500" />
+ <div className="flex-1 min-w-0">
+ <div className="text-sm font-medium truncate">
+ {file.name}
+ </div>
+ <div className="text-xs text-gray-500">
+ {formatFileSize(file.size)}
+ </div>
+ </div>
+ </div>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => removeFile(index)}
+ disabled={isPending}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose} disabled={isPending}>
취소
diff --git a/lib/pq/pq-review-table-new/request-investigation-dialog.tsx b/lib/pq/pq-review-table-new/request-investigation-dialog.tsx
index b9648e74..6941adbb 100644
--- a/lib/pq/pq-review-table-new/request-investigation-dialog.tsx
+++ b/lib/pq/pq-review-table-new/request-investigation-dialog.tsx
@@ -241,7 +241,7 @@ export function RequestInvestigationDialog({
)}
/>
- <FormField
+ {/* <FormField
control={form.control}
name="investigationMethod"
render={({ field }) => (
@@ -257,7 +257,7 @@ export function RequestInvestigationDialog({
<FormMessage />
</FormItem>
)}
- />
+ /> */}
<FormField
control={form.control}
diff --git a/lib/pq/pq-review-table-new/site-visit-dialog.tsx b/lib/pq/pq-review-table-new/site-visit-dialog.tsx
index b6bd3624..172aed98 100644
--- a/lib/pq/pq-review-table-new/site-visit-dialog.tsx
+++ b/lib/pq/pq-review-table-new/site-visit-dialog.tsx
@@ -36,6 +36,7 @@ import {
} from "@/components/ui/popover"
import { Checkbox } from "@/components/ui/checkbox"
import { Badge } from "@/components/ui/badge"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { toast } from "sonner"
import { getSiteVisitRequestAction } from "@/lib/site-visit/service"
import {
@@ -444,82 +445,96 @@ export function SiteVisitDialog({
삼성중공업에 어떤 부문의 담당자가 몇 명 실사 참석 예정인지에 대한 정보를 입력하세요.
</div>
- <div className="space-y-4">
- {[
- { key: "technicalSales", label: "기술영업" },
- { key: "design", label: "설계" },
- { key: "procurement", label: "구매" },
- { key: "quality", label: "품질" },
- { key: "production", label: "생산" },
- { key: "commissioning", label: "시운전" },
- { key: "other", label: "기타" },
- ].map((item) => (
- <div key={item.key} className="border rounded-lg p-4 space-y-3">
- <div className="flex items-center space-x-3">
- <FormField
- control={form.control}
- name={`shiAttendees.${item.key}.checked` as `shiAttendees.${typeof item.key}.checked`}
- render={({ field }) => (
- <FormItem className="flex flex-row items-center space-x-2 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={isPending}
- />
- </FormControl>
- <FormLabel className="text-sm font-medium">{item.label}</FormLabel>
- </FormItem>
- )}
- />
- </div>
-
- <div className="grid grid-cols-2 gap-4">
- <FormField
- control={form.control}
- name={`shiAttendees.${item.key}.count` as `shiAttendees.${typeof item.key}.count`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="text-sm">참석 인원</FormLabel>
- <div className="flex items-center space-x-2">
- <FormControl>
- <Input
- type="number"
- min="0"
- placeholder="0"
- {...field}
- onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
- disabled={isPending}
- className="w-20"
- />
- </FormControl>
- <span className="text-sm text-muted-foreground">명</span>
- </div>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name={`shiAttendees.${item.key}.details` as `shiAttendees.${typeof item.key}.details`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="text-sm">참석자 정보</FormLabel>
- <FormControl>
- <Input
- placeholder="부서 및 이름 등"
- {...field}
- disabled={isPending}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- </div>
- ))}
+ <div className="border rounded-lg overflow-hidden">
+ <Table>
+ <TableHeader>
+ <TableRow className="bg-muted/50">
+ <TableHead className="w-[100px]">참석여부</TableHead>
+ <TableHead className="w-[120px]">부문</TableHead>
+ <TableHead className="w-[100px]">참석인원</TableHead>
+ <TableHead>참석자 정보</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {[
+ { key: "technicalSales", label: "기술영업" },
+ { key: "design", label: "설계" },
+ { key: "procurement", label: "구매" },
+ { key: "quality", label: "품질" },
+ { key: "production", label: "생산" },
+ { key: "commissioning", label: "시운전" },
+ { key: "other", label: "기타" },
+ ].map((item) => (
+ <TableRow key={item.key}>
+ <TableCell>
+ <FormField
+ control={form.control}
+ name={`shiAttendees.${item.key}.checked` as any}
+ render={({ field }) => (
+ <FormItem className="flex items-center space-x-2 space-y-0">
+ <FormControl>
+ <Checkbox
+ checked={field.value as boolean}
+ onCheckedChange={field.onChange}
+ disabled={isPending}
+ />
+ </FormControl>
+ </FormItem>
+ )}
+ />
+ </TableCell>
+ <TableCell>
+ <span className="font-medium">{item.label}</span>
+ </TableCell>
+ <TableCell>
+ <FormField
+ control={form.control}
+ name={`shiAttendees.${item.key}.count` as any}
+ render={({ field }) => (
+ <FormItem className="space-y-0">
+ <div className="flex items-center space-x-2">
+ <FormControl>
+ <Input
+ type="number"
+ min="0"
+ placeholder="0"
+ value={field.value as number}
+ onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
+ disabled={isPending}
+ className="w-16 h-8"
+ />
+ </FormControl>
+ <span className="text-xs text-muted-foreground">명</span>
+ </div>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </TableCell>
+ <TableCell>
+ <FormField
+ control={form.control}
+ name={`shiAttendees.${item.key}.details` as any}
+ render={({ field }) => (
+ <FormItem className="space-y-0">
+ <FormControl>
+ <Input
+ placeholder="부서 및 이름 등"
+ value={field.value as string}
+ onChange={field.onChange}
+ disabled={isPending}
+ className="h-8"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
</div>
{/* 전체 참석자 상세정보 */}
@@ -544,10 +559,10 @@ export function SiteVisitDialog({
</div>
{/* 협력업체 요청정보 및 자료 */}
- <div>
+ {/* <div>
<FormLabel className="text-sm font-medium">협력업체 요청정보 및 자료</FormLabel>
<div className="text-sm text-muted-foreground mb-2">
- 협력업체에게 요청할 정보를 선택하세요. 선택된 항목들은 협력업체 정보 입력 폼에 포함됩니다.
+ 협력업체에게 요청할 정보를 선택하세요.
</div>
<div className="mt-2 space-y-2">
{[
@@ -564,7 +579,7 @@ export function SiteVisitDialog({
<FormField
key={item.key}
control={form.control}
- name={`vendorRequests.${item.key}` as `vendorRequests.${typeof item.key}`}
+ name={`vendorRequests.${item.key}` as any}
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -580,7 +595,7 @@ export function SiteVisitDialog({
/>
))}
</div>
- <FormField
+ {/* <FormField
control={form.control}
name="otherVendorRequests"
render={({ field }) => (
@@ -597,8 +612,8 @@ export function SiteVisitDialog({
<FormMessage />
</FormItem>
)}
- />
- </div>
+ />
+ </div> */}
{/* 추가 요청사항 */}
<FormField
diff --git a/lib/pq/pq-review-table-new/vendors-table-columns.tsx b/lib/pq/pq-review-table-new/vendors-table-columns.tsx
index d3fada0d..449b69be 100644
--- a/lib/pq/pq-review-table-new/vendors-table-columns.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table-columns.tsx
@@ -55,7 +55,7 @@ export interface PQSubmission {
pqTypeLabel: string
// PQ 대상품목
- pqItems: string | null
+ pqItems: string | null | Array<{itemCode: string, itemName: string}>
// 방문실사 요청 정보
siteVisitRequestId: number | null // 방문실사 요청 ID
@@ -457,11 +457,19 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef
return <span className="text-muted-foreground">-</span>;
}
- return (
- <div className="flex items-center gap-2">
- <span className="text-sm">{pqItems}</span>
- </div>
- )
+ // JSON 파싱하여 첫 번째 아이템 표시
+ const items = typeof pqItems === 'string' ? JSON.parse(pqItems) : pqItems;
+ if (Array.isArray(items) && items.length > 0) {
+ const firstItem = items[0];
+ return (
+ <div className="flex items-center gap-2">
+ <span className="text-sm">{firstItem.itemCode} - {firstItem.itemName}</span>
+ {items.length > 1 && (
+ <span className="text-xs text-muted-foreground">외 {items.length - 1}건</span>
+ )}
+ </div>
+ );
+ }
},
}
diff --git a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
index f731a922..8398c2e7 100644
--- a/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table-toolbar-actions.tsx
@@ -309,20 +309,69 @@ const handleOpenRequestDialog = async () => {
const investigation = row.original.investigation!
const pqSubmission = row.original
+ // pqItems를 상세하게 포맷팅 (itemCode-itemName 형태로 모든 항목 표시)
+ const formatAuditItem = (pqItems: any): string => {
+ if (!pqItems) return pqSubmission.projectName || "N/A";
+
+ try {
+ // 이미 파싱된 객체 배열인 경우
+ if (Array.isArray(pqItems)) {
+ return pqItems.map(item => {
+ if (typeof item === 'string') return item;
+ if (typeof item === 'object') {
+ const code = item.itemCode || item.code || "";
+ const name = item.itemName || item.name || "";
+ if (code && name) return `${code}-${name}`;
+ return name || code || String(item);
+ }
+ return String(item);
+ }).join(', ');
+ }
+
+ // JSON 문자열인 경우
+ if (typeof pqItems === 'string') {
+ try {
+ const parsed = JSON.parse(pqItems);
+ if (Array.isArray(parsed)) {
+ return parsed.map(item => {
+ if (typeof item === 'string') return item;
+ if (typeof item === 'object') {
+ const code = item.itemCode || item.code || "";
+ const name = item.itemName || item.name || "";
+ if (code && name) return `${code}-${name}`;
+ return name || code || String(item);
+ }
+ return String(item);
+ }).join(', ');
+ }
+ return String(parsed);
+ } catch {
+ return String(pqItems);
+ }
+ }
+
+ // 기타 경우
+ return String(pqItems);
+ } catch {
+ return pqSubmission.projectName || "N/A";
+ }
+ };
+
return {
id: investigation.id,
vendorCode: row.original.vendorCode || "N/A",
vendorName: row.original.vendorName || "N/A",
vendorEmail: row.original.email || "N/A",
+ vendorContactPerson: (row.original as any).representativeName || row.original.vendorName || "N/A",
pqNumber: pqSubmission.pqNumber || "N/A",
- auditItem: pqSubmission.pqItems || pqSubmission.projectName || "N/A",
+ auditItem: formatAuditItem(pqSubmission.pqItems),
auditFactoryAddress: investigation.investigationAddress || "N/A",
auditMethod: getInvestigationMethodLabel(investigation.investigationMethod || ""),
auditResult: investigation.evaluationResult === "APPROVED" ? "Pass(승인)" :
investigation.evaluationResult === "SUPPLEMENT" ? "Pass(조건부승인)" :
investigation.evaluationResult === "REJECTED" ? "Fail(미승인)" : "N/A",
- additionalNotes: investigation.investigationNotes,
- investigationNotes: investigation.investigationNotes,
+ additionalNotes: investigation.investigationNotes || undefined,
+ investigationNotes: investigation.investigationNotes || undefined,
}
})