feat: 품질관리·생산·출하 개선 — 검사관리·생산지시·배차·문서스냅샷
- 검사관리 수주처 선택 UI + client_id 연동 + 타입 에러 수정 - 제품검사 요청서/성적서 동적 렌더링 + Lazy Snapshot - 품질관리 Mock→API 전환 + 수주선택 모달 발주처 연동 - 생산지시 Create/Detail/Edit 제품코드 표시 추가 - 배차 상세/수정 그리드 레이아웃 개선 - 자재/수주 상세 뷰 보강
This commit is contained in:
@@ -229,7 +229,7 @@ export default function QualityInspectionPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-3 sm:p-4 md:p-5 lg:p-6 bg-slate-100 flex flex-col lg:overflow-hidden">
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-3 sm:p-4 md:p-5 lg:p-6 bg-slate-100 flex flex-col lg:overflow-auto">
|
||||
{/* 헤더 (설정 버튼 포함) */}
|
||||
<Header
|
||||
rightContent={<SettingsButton onClick={() => setSettingsOpen(true)} />}
|
||||
@@ -283,9 +283,9 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{activeDay === 1 ? (
|
||||
// ===== 기준/매뉴얼 심사 심사 =====
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:min-h-0">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:min-h-[500px]">
|
||||
{/* 좌측: 점검표 항목 */}
|
||||
<div className={`col-span-12 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[250px] sm:min-h-[300px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection && displaySettings.showDocumentViewer
|
||||
? 'lg:col-span-3'
|
||||
: displaySettings.showDocumentSection || displaySettings.showDocumentViewer
|
||||
@@ -303,7 +303,7 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{/* 중앙: 기준 문서화 */}
|
||||
{displaySettings.showDocumentSection && (
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentViewer ? 'lg:col-span-4' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentSection
|
||||
@@ -318,7 +318,7 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{/* 우측: 문서 뷰어 */}
|
||||
{displaySettings.showDocumentViewer && (
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection ? 'lg:col-span-5' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentViewer document={selectedStandardDoc} />
|
||||
@@ -327,8 +327,8 @@ export default function QualityInspectionPage() {
|
||||
</div>
|
||||
) : (
|
||||
// ===== 로트 추적 심사 심사 =====
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:gap-6 lg:min-h-0">
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:gap-6 lg:min-h-[500px]">
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[250px] sm:min-h-[300px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<ReportList
|
||||
reports={filteredReports}
|
||||
selectedId={selectedReport?.id || null}
|
||||
@@ -336,7 +336,7 @@ export default function QualityInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<RouteList
|
||||
routes={currentRoutes}
|
||||
selectedId={selectedRoute?.id || null}
|
||||
@@ -346,7 +346,7 @@ export default function QualityInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<DocumentList
|
||||
documents={currentDocuments}
|
||||
routeCode={selectedRoute?.code || null}
|
||||
|
||||
@@ -27,7 +27,7 @@ interface SupplierItem {
|
||||
interface SupplierSearchModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelectSupplier: (supplier: { name: string; code?: string }) => void;
|
||||
onSelectSupplier: (supplier: { id: number | string; name: string; code?: string }) => void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -115,7 +115,7 @@ export function SupplierSearchModal({
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback((supplier: SupplierItem) => {
|
||||
onSelectSupplier({ name: supplier.name, code: supplier.clientCode });
|
||||
onSelectSupplier({ id: supplier.id, name: supplier.name, code: supplier.clientCode });
|
||||
}, [onSelectSupplier]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -133,6 +134,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
|
||||
const result = await updateStock(id, formData);
|
||||
|
||||
if (result.success) {
|
||||
invalidateDashboard('stock');
|
||||
toast.success('재고 정보가 저장되었습니다.');
|
||||
// 상세 데이터 업데이트
|
||||
setDetail((prev) =>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useDaumPostcode } from "@/hooks/useDaumPostcode";
|
||||
import { useClientList } from "@/hooks/useClientList";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -197,7 +198,7 @@ export function OrderRegistration({
|
||||
|
||||
// 컴포넌트 마운트 시 거래처 목록 불러오기
|
||||
useEffect(() => {
|
||||
fetchClients({ onlyActive: true, size: 100 });
|
||||
fetchClients({ onlyActive: true, size: 1000 });
|
||||
}, [fetchClients]);
|
||||
|
||||
// Daum 우편번호 서비스
|
||||
@@ -504,6 +505,7 @@ export function OrderRegistration({
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave(form);
|
||||
invalidateDashboard('order');
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : '저장 중 오류가 발생했습니다.';
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
type OrderStatus,
|
||||
} from "@/components/orders";
|
||||
import { ServerErrorPage } from "@/components/common/ServerErrorPage";
|
||||
import { invalidateDashboard } from "@/lib/dashboard-invalidation";
|
||||
|
||||
|
||||
// 상태 뱃지 헬퍼
|
||||
@@ -293,6 +294,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
try {
|
||||
const result = await updateOrderStatus(order.id, "cancelled");
|
||||
if (result.success) {
|
||||
invalidateDashboard('sales');
|
||||
setOrder({ ...order, status: "cancelled" });
|
||||
toast.success("수주가 취소되었습니다.");
|
||||
setIsCancelDialogOpen(false);
|
||||
@@ -321,6 +323,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
|
||||
try {
|
||||
const result = await updateOrderStatus(order.id, "order_confirmed");
|
||||
if (result.success && result.data) {
|
||||
invalidateDashboard('sales');
|
||||
setOrder(result.data);
|
||||
toast.success("수주가 확정되었습니다.");
|
||||
setIsConfirmDialogOpen(false);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Plus, Trash2, ChevronDown, Search } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -305,6 +306,7 @@ export function ShipmentEdit({ id }: ShipmentEditProps) {
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || '출고 수정에 실패했습니다.' };
|
||||
}
|
||||
invalidateDashboard('shipment');
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -291,6 +292,7 @@ export function WorkOrderCreate() {
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || '작업지시 등록에 실패했습니다.' };
|
||||
}
|
||||
invalidateDashboard('production');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, Play, CheckCircle2, Loader2, Undo2, ClipboardCheck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -272,6 +273,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
try {
|
||||
const result = await updateWorkOrderStatus(orderId, newStatus);
|
||||
if (result.success && result.data) {
|
||||
invalidateDashboard('production');
|
||||
setOrder(result.data);
|
||||
const statusLabels = {
|
||||
waiting: '작업대기',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { SquarePen, Trash2 } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -239,6 +240,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
invalidateDashboard('production');
|
||||
toast.success('작업지시가 수정되었습니다.');
|
||||
router.push(`/production/work-orders/${orderId}?mode=view`);
|
||||
return { success: true };
|
||||
|
||||
@@ -48,6 +48,9 @@ const OrderSelectModal = dynamic(
|
||||
const ProductInspectionInputModal = dynamic(
|
||||
() => import('./ProductInspectionInputModal').then(mod => ({ default: mod.ProductInspectionInputModal })),
|
||||
);
|
||||
const SupplierSearchModal = dynamic(
|
||||
() => import('@/components/material/ReceivingManagement/SupplierSearchModal').then(mod => ({ default: mod.SupplierSearchModal })),
|
||||
);
|
||||
import type { InspectionFormData, OrderSettingItem, OrderSelectItem, OrderGroup, ProductInspectionData } from './types';
|
||||
import {
|
||||
emptyConstructionSite,
|
||||
@@ -77,6 +80,7 @@ export function InspectionCreate() {
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [orderModalOpen, setOrderModalOpen] = useState(false);
|
||||
const [clientModalOpen, setClientModalOpen] = useState(false);
|
||||
|
||||
// 제품검사 입력 모달
|
||||
const [inspectionInputOpen, setInspectionInputOpen] = useState(false);
|
||||
@@ -123,10 +127,15 @@ export function InspectionCreate() {
|
||||
changeReason: '',
|
||||
}]
|
||||
);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
orderItems: [...prev.orderItems, ...newOrderItems],
|
||||
}));
|
||||
setFormData((prev) => {
|
||||
const updated = { ...prev, orderItems: [...prev.orderItems, ...newOrderItems] };
|
||||
// 수주처 미선택 상태에서 수주 선택 시 → 수주처 자동 채움
|
||||
if (!prev.clientId && items.length > 0 && items[0].clientId) {
|
||||
updated.clientId = items[0].clientId ?? undefined;
|
||||
updated.client = items[0].clientName || '';
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ===== 수주 항목 삭제 =====
|
||||
@@ -238,9 +247,9 @@ export function InspectionCreate() {
|
||||
toast.error('현장명은 필수 입력 항목입니다.');
|
||||
return { success: false, error: '현장명을 입력해주세요.' };
|
||||
}
|
||||
if (!formData.client.trim()) {
|
||||
toast.error('수주처는 필수 입력 항목입니다.');
|
||||
return { success: false, error: '수주처를 입력해주세요.' };
|
||||
if (!formData.clientId) {
|
||||
toast.error('수주처는 필수 선택 항목입니다.');
|
||||
return { success: false, error: '수주처를 선택해주세요.' };
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
@@ -400,11 +409,29 @@ export function InspectionCreate() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수주처 <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
value={formData.client}
|
||||
onChange={(e) => updateField('client', e.target.value)}
|
||||
placeholder="수주처 입력"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={formData.client}
|
||||
readOnly
|
||||
placeholder="거래처를 선택하세요"
|
||||
className="cursor-pointer bg-muted/30"
|
||||
onClick={() => setClientModalOpen(true)}
|
||||
/>
|
||||
{formData.clientId && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => {
|
||||
updateField('client', '');
|
||||
updateField('clientId', undefined);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>담당자</Label>
|
||||
@@ -691,16 +718,28 @@ export function InspectionCreate() {
|
||||
[formData.orderItems]
|
||||
);
|
||||
|
||||
// 이미 선택된 수주가 있으면 같은 거래처+모델만 필터
|
||||
// 수주 선택 필터: 기본정보 수주처 또는 이미 선택된 수주 기준
|
||||
const orderFilter = useMemo(() => {
|
||||
if (formData.orderItems.length === 0) return { clientId: undefined, itemId: undefined, label: undefined };
|
||||
const first = formData.orderItems[0];
|
||||
return {
|
||||
clientId: first.clientId ?? undefined,
|
||||
itemId: first.itemId ?? undefined,
|
||||
label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined,
|
||||
};
|
||||
}, [formData.orderItems]);
|
||||
// 기본정보에서 수주처가 선택된 경우 → 해당 거래처 필터
|
||||
if (formData.clientId) {
|
||||
const firstItem = formData.orderItems[0];
|
||||
return {
|
||||
clientId: formData.clientId,
|
||||
itemId: firstItem?.itemId ?? undefined,
|
||||
label: [formData.client, firstItem?.itemName].filter(Boolean).join(' / ') || undefined,
|
||||
};
|
||||
}
|
||||
// 수주가 선택된 경우 → 첫 수주 기준 필터
|
||||
if (formData.orderItems.length > 0) {
|
||||
const first = formData.orderItems[0];
|
||||
return {
|
||||
clientId: first.clientId ?? undefined,
|
||||
itemId: first.itemId ?? undefined,
|
||||
label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined,
|
||||
};
|
||||
}
|
||||
return { clientId: undefined, itemId: undefined, label: undefined };
|
||||
}, [formData.clientId, formData.client, formData.orderItems]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -725,6 +764,17 @@ export function InspectionCreate() {
|
||||
filterLabel={orderFilter.label}
|
||||
/>
|
||||
|
||||
{/* 거래처(수주처) 검색 모달 */}
|
||||
<SupplierSearchModal
|
||||
open={clientModalOpen}
|
||||
onOpenChange={setClientModalOpen}
|
||||
onSelectSupplier={(supplier) => {
|
||||
updateField('clientId', Number(supplier.id));
|
||||
updateField('client', supplier.name);
|
||||
setClientModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 제품검사 입력 모달 */}
|
||||
<ProductInspectionInputModal
|
||||
open={inspectionInputOpen}
|
||||
|
||||
@@ -82,6 +82,9 @@ const ProductInspectionInputModal = dynamic(
|
||||
const OrderSelectModal = dynamic(
|
||||
() => import('./OrderSelectModal').then(mod => ({ default: mod.OrderSelectModal })),
|
||||
);
|
||||
const SupplierSearchModal = dynamic(
|
||||
() => import('@/components/material/ReceivingManagement/SupplierSearchModal').then(mod => ({ default: mod.SupplierSearchModal })),
|
||||
);
|
||||
import type {
|
||||
ProductInspection,
|
||||
InspectionFormData,
|
||||
@@ -137,6 +140,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
|
||||
|
||||
// 수주 선택 모달
|
||||
const [orderModalOpen, setOrderModalOpen] = useState(false);
|
||||
const [clientModalOpen, setClientModalOpen] = useState(false);
|
||||
|
||||
// 문서 모달
|
||||
const [requestDocOpen, setRequestDocOpen] = useState(false);
|
||||
@@ -213,6 +217,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
|
||||
qualityDocNumber: result.data.qualityDocNumber,
|
||||
siteName: result.data.siteName,
|
||||
client: result.data.client,
|
||||
clientId: result.data.clientId,
|
||||
manager: result.data.manager,
|
||||
managerContact: result.data.managerContact,
|
||||
constructionSite: { ...result.data.constructionSite },
|
||||
@@ -374,10 +379,15 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
|
||||
changeReason: '',
|
||||
}]
|
||||
);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
orderItems: [...prev.orderItems, ...newOrderItems],
|
||||
}));
|
||||
setFormData((prev) => {
|
||||
const updated = { ...prev, orderItems: [...prev.orderItems, ...newOrderItems] };
|
||||
// 수주처 미선택 상태에서 수주 선택 시 → 수주처 자동 채움
|
||||
if (!prev.clientId && items.length > 0 && items[0].clientId) {
|
||||
updated.clientId = items[0].clientId ?? undefined;
|
||||
updated.client = items[0].clientName || '';
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRemoveOrderItem = useCallback((itemId: string) => {
|
||||
@@ -393,17 +403,29 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
|
||||
[formData.orderItems]
|
||||
);
|
||||
|
||||
// 이미 선택된 수주가 있으면 같은 거래처+모델만 필터
|
||||
// 수주 선택 필터: 기본정보 수주처 또는 이미 선택된 수주 기준
|
||||
const orderFilter = useMemo(() => {
|
||||
const items = isEditMode ? formData.orderItems : (inspection?.orderItems || []);
|
||||
if (items.length === 0) return { clientId: undefined, itemId: undefined, label: undefined };
|
||||
const first = items[0];
|
||||
return {
|
||||
clientId: first.clientId ?? undefined,
|
||||
itemId: first.itemId ?? undefined,
|
||||
label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined,
|
||||
};
|
||||
}, [isEditMode, formData.orderItems, inspection?.orderItems]);
|
||||
// 기본정보에서 수주처가 선택된 경우 → 해당 거래처 필터
|
||||
if (formData.clientId) {
|
||||
const firstItem = items[0];
|
||||
return {
|
||||
clientId: formData.clientId,
|
||||
itemId: firstItem?.itemId ?? undefined,
|
||||
label: [formData.client, firstItem?.itemName].filter(Boolean).join(' / ') || undefined,
|
||||
};
|
||||
}
|
||||
// 수주가 선택된 경우 → 첫 수주 기준 필터
|
||||
if (items.length > 0) {
|
||||
const first = items[0];
|
||||
return {
|
||||
clientId: first.clientId ?? undefined,
|
||||
itemId: first.itemId ?? undefined,
|
||||
label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined,
|
||||
};
|
||||
}
|
||||
return { clientId: undefined, itemId: undefined, label: undefined };
|
||||
}, [isEditMode, formData.clientId, formData.client, formData.orderItems, inspection?.orderItems]);
|
||||
|
||||
// ===== 수주 설정 요약 =====
|
||||
const orderSummary = useMemo(() => {
|
||||
@@ -976,10 +998,28 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수주처</Label>
|
||||
<Input
|
||||
value={formData.client}
|
||||
onChange={(e) => updateField('client', e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={formData.client}
|
||||
readOnly
|
||||
placeholder="거래처를 선택하세요"
|
||||
className="cursor-pointer bg-muted/30"
|
||||
onClick={() => setClientModalOpen(true)}
|
||||
/>
|
||||
{formData.clientId && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => {
|
||||
updateField('client', '');
|
||||
updateField('clientId', undefined);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-muted-foreground">접수일</Label>
|
||||
@@ -1334,6 +1374,17 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
|
||||
filterLabel={orderFilter.label}
|
||||
/>
|
||||
|
||||
{/* 거래처(수주처) 검색 모달 */}
|
||||
<SupplierSearchModal
|
||||
open={clientModalOpen}
|
||||
onOpenChange={setClientModalOpen}
|
||||
onSelectSupplier={(supplier) => {
|
||||
updateField('clientId', Number(supplier.id));
|
||||
updateField('client', supplier.name);
|
||||
setClientModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 제품검사요청서 모달 */}
|
||||
<InspectionRequestModal
|
||||
open={requestDocOpen}
|
||||
|
||||
@@ -43,6 +43,7 @@ interface ProductInspectionApi {
|
||||
quality_doc_number: string;
|
||||
site_name: string;
|
||||
client: string;
|
||||
client_id?: number | null;
|
||||
location_count: number;
|
||||
required_info: string;
|
||||
inspection_period: string;
|
||||
@@ -196,6 +197,7 @@ function transformApiToFrontend(api: ProductInspectionApi): ProductInspection {
|
||||
qualityDocNumber: api.quality_doc_number,
|
||||
siteName: api.site_name,
|
||||
client: api.client,
|
||||
clientId: api.client_id ?? undefined,
|
||||
locationCount: api.location_count,
|
||||
requiredInfo: api.required_info,
|
||||
inspectionPeriod: api.inspection_period,
|
||||
@@ -592,13 +594,16 @@ export async function updateInspection(
|
||||
});
|
||||
|
||||
// 개소별 데이터 (시공규격, 변경사유, 검사데이터)
|
||||
apiData.locations = data.orderItems.map((item) => ({
|
||||
id: Number(item.id),
|
||||
post_width: item.constructionWidth || null,
|
||||
post_height: item.constructionHeight || null,
|
||||
change_reason: item.changeReason || null,
|
||||
inspection_data: item.inspectionData || null,
|
||||
}));
|
||||
// 새로 추가된 항목(id가 "orderId-nodeId" 형태)은 order_ids 동기화로 생성되므로 제외
|
||||
apiData.locations = data.orderItems
|
||||
.filter((item) => !String(item.id).includes('-') && !isNaN(Number(item.id)))
|
||||
.map((item) => ({
|
||||
id: Number(item.id),
|
||||
post_width: item.constructionWidth || null,
|
||||
post_height: item.constructionHeight || null,
|
||||
change_reason: item.changeReason || null,
|
||||
inspection_data: item.inspectionData || null,
|
||||
}));
|
||||
}
|
||||
|
||||
const result = await executeServerAction<ProductInspectionApi>({
|
||||
|
||||
@@ -152,6 +152,7 @@ export interface ProductInspection {
|
||||
qualityDocNumber: string; // 품질관리서 번호
|
||||
siteName: string; // 현장명
|
||||
client: string; // 수주처
|
||||
clientId?: number; // 수주처 ID
|
||||
locationCount: number; // 개소
|
||||
requiredInfo: string; // 필수정보 (완료 / N건 누락)
|
||||
inspectionPeriod: string; // 검사기간 (2026-01-01 또는 2026-01-01~2026-01-02)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* - 왼쪽: 목록으로/취소 (뒤로가기 성격)
|
||||
* - 오른쪽: [추가액션] 삭제 | 수정/저장/등록 (액션 성격)
|
||||
*
|
||||
* View 모드: 목록으로 | [추가액션] 삭제 | 수정
|
||||
* View 모드: 목록으로 | [추가액션] 수정
|
||||
* Edit 모드: 취소 | [추가액션] 삭제 | 저장
|
||||
* Create 모드: 취소 | [추가액션] 등록
|
||||
*/
|
||||
@@ -156,8 +156,8 @@ export function DetailActions({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 삭제 버튼: view, edit 모드에서 표시 (create는 삭제할 대상 없음) */}
|
||||
{!isCreateMode && canDelete && showDelete && onDelete && (
|
||||
{/* 삭제 버튼: edit 모드에서만 표시 (view는 읽기 전용, create는 삭제 대상 없음) */}
|
||||
{isEditMode && canDelete && showDelete && onDelete && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onDelete}
|
||||
|
||||
Reference in New Issue
Block a user