f32861df8b
- Introduced multiple new pages for design management, including change management, design requests, my work, project management, and task management. - Added session files to track design sessions with relevant details such as session ID, end time, and reason. - Enhanced the overall structure and organization of the design management features, improving user experience and functionality. This commit expands the design management capabilities within the application, allowing for better tracking and handling of design-related tasks.
535 lines
24 KiB
TypeScript
535 lines
24 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 외주품목정보 — 하드코딩 페이지
|
|
*
|
|
* 좌측: 품목 목록 (subcontractor_item_mapping 기반 품목, item_info 조인)
|
|
* 우측: 선택한 품목의 외주업체 정보 (subcontractor_item_mapping → subcontractor_mng 조인)
|
|
*
|
|
* 외주업체관리와 양방향 연동 (같은 subcontractor_item_mapping 테이블)
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
|
import { Plus, Save, Loader2, FileSpreadsheet, Download, Pencil, Package, Users, Search, Settings2 } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { toast } from "sonner";
|
|
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
|
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
|
import { TableSettingsModal, TableSettings, loadTableSettings } from "@/components/common/TableSettingsModal";
|
|
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
|
import { FullscreenDialog } from "@/components/common/FullscreenDialog";
|
|
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
|
|
import { exportToExcel } from "@/lib/utils/excelExport";
|
|
|
|
const ITEM_TABLE = "item_info";
|
|
const MAPPING_TABLE = "subcontractor_item_mapping";
|
|
const SUBCONTRACTOR_TABLE = "subcontractor_mng";
|
|
|
|
// 좌측: 품목 컬럼
|
|
const LEFT_COLUMNS: DataGridColumn[] = [
|
|
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
|
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
|
{ key: "size", label: "규격", width: "w-[90px]" },
|
|
{ key: "unit", label: "단위", width: "w-[60px]" },
|
|
{ key: "standard_price", label: "기준단가", width: "w-[90px]", formatNumber: true, align: "right" },
|
|
{ key: "selling_price", label: "판매가격", width: "w-[90px]", formatNumber: true, align: "right" },
|
|
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
|
{ key: "status", label: "상태", width: "w-[60px]" },
|
|
];
|
|
|
|
// 우측: 외주업체 정보 컬럼
|
|
const RIGHT_COLUMNS: DataGridColumn[] = [
|
|
{ key: "subcontractor_code", label: "외주업체코드", width: "w-[110px]" },
|
|
{ key: "subcontractor_name", label: "외주업체명", minWidth: "min-w-[120px]" },
|
|
{ key: "subcontractor_item_code", label: "외주품번", width: "w-[100px]" },
|
|
{ key: "subcontractor_item_name", label: "외주품명", width: "w-[100px]" },
|
|
{ key: "base_price", label: "기준가", width: "w-[80px]", formatNumber: true, align: "right" },
|
|
{ key: "calculated_price", label: "단가", width: "w-[80px]", formatNumber: true, align: "right" },
|
|
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
|
];
|
|
|
|
export default function SubcontractorItemPage() {
|
|
const { user } = useAuth();
|
|
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
|
|
|
|
// 좌측: 품목
|
|
const [items, setItems] = useState<any[]>([]);
|
|
const [itemLoading, setItemLoading] = useState(false);
|
|
const [itemCount, setItemCount] = useState(0);
|
|
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
|
const [tableSettingsOpen, setTableSettingsOpen] = useState(false);
|
|
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"] | undefined>();
|
|
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
|
|
|
|
// 우측: 외주업체
|
|
const [subcontractorItems, setSubcontractorItems] = useState<any[]>([]);
|
|
const [subcontractorLoading, setSubcontractorLoading] = useState(false);
|
|
|
|
// 카테고리
|
|
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
|
|
|
// 외주업체 추가 모달
|
|
const [subSelectOpen, setSubSelectOpen] = useState(false);
|
|
const [subSearchKeyword, setSubSearchKeyword] = useState("");
|
|
const [subSearchResults, setSubSearchResults] = useState<any[]>([]);
|
|
const [subSearchLoading, setSubSearchLoading] = useState(false);
|
|
const [subCheckedIds, setSubCheckedIds] = useState<Set<string>>(new Set());
|
|
|
|
// 품목 수정 모달
|
|
const [editItemOpen, setEditItemOpen] = useState(false);
|
|
const [editItemForm, setEditItemForm] = useState<Record<string, any>>({});
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// 엑셀
|
|
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
|
|
|
|
const applyTableSettings = useCallback((settings: TableSettings) => {
|
|
setFilterConfig(settings.filters);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const saved = loadTableSettings("subcontractor-item");
|
|
if (saved) applyTableSettings(saved);
|
|
}, []);
|
|
|
|
// 카테고리 로드
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
const optMap: Record<string, { code: string; label: string }[]> = {};
|
|
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
|
const result: { code: string; label: string }[] = [];
|
|
for (const v of vals) {
|
|
result.push({ code: v.valueCode, label: v.valueLabel });
|
|
if (v.children?.length) result.push(...flatten(v.children));
|
|
}
|
|
return result;
|
|
};
|
|
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
|
try {
|
|
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
|
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
|
} catch { /* skip */ }
|
|
}
|
|
setCategoryOptions(optMap);
|
|
};
|
|
load();
|
|
}, []);
|
|
|
|
const resolve = (col: string, code: string) => {
|
|
if (!code) return "";
|
|
return categoryOptions[col]?.find((o) => o.code === code)?.label || code;
|
|
};
|
|
|
|
// 좌측: 품목 조회 (division이 "외주관리"인 품목만 필터링)
|
|
const outsourcingDivisionCode = categoryOptions["division"]?.find(
|
|
(o) => o.label === "외주관리" || o.label === "외주" || o.label.includes("외주")
|
|
)?.code;
|
|
|
|
const fetchItems = useCallback(async () => {
|
|
setItemLoading(true);
|
|
try {
|
|
const filters: any[] = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
|
|
// division = 외주관리 필터 추가
|
|
if (outsourcingDivisionCode) {
|
|
filters.push({ columnName: "division", operator: "equals", value: outsourcingDivisionCode });
|
|
}
|
|
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
|
page: 1, size: 500,
|
|
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
|
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
|
const data = raw.map((r: any) => {
|
|
const converted = { ...r };
|
|
for (const col of CATS) {
|
|
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
|
}
|
|
return converted;
|
|
});
|
|
setItems(data);
|
|
setItemCount(res.data?.data?.total || raw.length);
|
|
} catch (err) {
|
|
console.error("품목 조회 실패:", err);
|
|
toast.error("품목 목록을 불러오는데 실패했습니다.");
|
|
} finally {
|
|
setItemLoading(false);
|
|
}
|
|
}, [searchFilters, categoryOptions, outsourcingDivisionCode]);
|
|
|
|
useEffect(() => { fetchItems(); }, [fetchItems]);
|
|
|
|
// 선택된 품목
|
|
const selectedItem = items.find((i) => i.id === selectedItemId);
|
|
|
|
// 우측: 외주업체 목록 조회
|
|
useEffect(() => {
|
|
if (!selectedItem?.item_number) { setSubcontractorItems([]); return; }
|
|
const itemKey = selectedItem.item_number;
|
|
const fetchSubcontractorItems = async () => {
|
|
setSubcontractorLoading(true);
|
|
try {
|
|
// subcontractor_item_mapping에서 해당 품목의 매핑 조회
|
|
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
|
page: 1, size: 500,
|
|
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
|
|
autoFilter: true,
|
|
});
|
|
const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
|
|
|
// subcontractor_id → subcontractor_mng 조인 (외주업체명)
|
|
const subIds = [...new Set(mappings.map((m: any) => m.subcontractor_id).filter(Boolean))];
|
|
let subMap: Record<string, any> = {};
|
|
if (subIds.length > 0) {
|
|
try {
|
|
const subRes = await apiClient.post(`/table-management/tables/${SUBCONTRACTOR_TABLE}/data`, {
|
|
page: 1, size: subIds.length + 10,
|
|
dataFilter: { enabled: true, filters: [{ columnName: "subcontractor_code", operator: "in", value: subIds }] },
|
|
autoFilter: true,
|
|
});
|
|
for (const s of (subRes.data?.data?.data || subRes.data?.data?.rows || [])) {
|
|
subMap[s.subcontractor_code] = s;
|
|
}
|
|
} catch { /* skip */ }
|
|
}
|
|
|
|
setSubcontractorItems(mappings.map((m: any) => ({
|
|
...m,
|
|
subcontractor_code: m.subcontractor_id,
|
|
subcontractor_name: subMap[m.subcontractor_id]?.subcontractor_name || "",
|
|
})));
|
|
} catch (err) {
|
|
console.error("외주업체 조회 실패:", err);
|
|
} finally {
|
|
setSubcontractorLoading(false);
|
|
}
|
|
};
|
|
fetchSubcontractorItems();
|
|
}, [selectedItem?.item_number]);
|
|
|
|
// 외주업체 검색
|
|
const searchSubcontractors = async () => {
|
|
setSubSearchLoading(true);
|
|
try {
|
|
const filters: any[] = [];
|
|
if (subSearchKeyword) filters.push({ columnName: "subcontractor_name", operator: "contains", value: subSearchKeyword });
|
|
const res = await apiClient.post(`/table-management/tables/${SUBCONTRACTOR_TABLE}/data`, {
|
|
page: 1, size: 50,
|
|
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
const all = res.data?.data?.data || res.data?.data?.rows || [];
|
|
// 이미 등록된 외주업체 제외
|
|
const existing = new Set(subcontractorItems.map((s: any) => s.subcontractor_id || s.subcontractor_code));
|
|
setSubSearchResults(all.filter((s: any) => !existing.has(s.subcontractor_code)));
|
|
} catch { /* skip */ } finally { setSubSearchLoading(false); }
|
|
};
|
|
|
|
// 외주업체 추가 저장
|
|
const addSelectedSubcontractors = async () => {
|
|
const selected = subSearchResults.filter((s) => subCheckedIds.has(s.id));
|
|
if (selected.length === 0 || !selectedItem) return;
|
|
try {
|
|
for (const sub of selected) {
|
|
await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
|
|
subcontractor_id: sub.subcontractor_code,
|
|
item_id: selectedItem.item_number,
|
|
});
|
|
}
|
|
toast.success(`${selected.length}개 외주업체가 추가되었습니다.`);
|
|
setSubCheckedIds(new Set());
|
|
setSubSelectOpen(false);
|
|
// 우측 새로고침
|
|
const sid = selectedItemId;
|
|
setSelectedItemId(null);
|
|
setTimeout(() => setSelectedItemId(sid), 50);
|
|
} catch (err: any) {
|
|
toast.error(err.response?.data?.message || "외주업체 추가에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
// 품목 수정
|
|
const openEditItem = () => {
|
|
if (!selectedItem) return;
|
|
setEditItemForm({ ...selectedItem });
|
|
setEditItemOpen(true);
|
|
};
|
|
|
|
const handleEditSave = async () => {
|
|
if (!editItemForm.id) return;
|
|
setSaving(true);
|
|
try {
|
|
await apiClient.put(`/table-management/tables/${ITEM_TABLE}/edit`, {
|
|
originalData: { id: editItemForm.id },
|
|
updatedData: {
|
|
selling_price: editItemForm.selling_price || null,
|
|
standard_price: editItemForm.standard_price || null,
|
|
currency_code: editItemForm.currency_code || null,
|
|
},
|
|
});
|
|
toast.success("수정되었습니다.");
|
|
setEditItemOpen(false);
|
|
fetchItems();
|
|
} catch (err: any) {
|
|
toast.error(err.response?.data?.message || "수정에 실패했습니다.");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// 엑셀 다운로드
|
|
const handleExcelDownload = async () => {
|
|
if (items.length === 0) return;
|
|
const data = items.map((i) => ({
|
|
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
|
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
|
}));
|
|
await exportToExcel(data, "외주품목정보.xlsx", "외주품목");
|
|
toast.success("다운로드 완료");
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full flex-col gap-3 p-3">
|
|
{/* 검색 */}
|
|
<DynamicSearchFilter
|
|
tableName={ITEM_TABLE}
|
|
filterId="subcontractor-item"
|
|
onFilterChange={setSearchFilters}
|
|
dataCount={itemCount}
|
|
externalFilterConfig={filterConfig}
|
|
extraActions={
|
|
<div className="flex gap-1.5">
|
|
<Button variant="outline" size="sm" className="h-9" onClick={() => setTableSettingsOpen(true)}>
|
|
<Settings2 className="w-4 h-4 mr-1.5" /> 테이블 설정
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="h-9" onClick={() => setExcelUploadOpen(true)}>
|
|
<FileSpreadsheet className="w-3.5 h-3.5 mr-1" /> 엑셀 업로드
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="h-9" onClick={handleExcelDownload}>
|
|
<Download className="w-3.5 h-3.5 mr-1" /> 엑셀 다운로드
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* 분할 패널 */}
|
|
<div className="flex-1 overflow-hidden border rounded-lg bg-background shadow-sm">
|
|
<ResizablePanelGroup direction="horizontal">
|
|
{/* 좌측: 외주품목 목록 */}
|
|
<ResizablePanel defaultSize={55} minSize={30}>
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center justify-between p-3 border-b bg-muted/10 shrink-0">
|
|
<div className="font-semibold flex items-center gap-2 text-sm">
|
|
<Package className="w-4 h-4" /> 외주품목 목록
|
|
<Badge variant="secondary" className="font-normal">{itemCount}건</Badge>
|
|
</div>
|
|
<div className="flex gap-1.5">
|
|
<Button variant="outline" size="sm" disabled={!selectedItemId} onClick={openEditItem}>
|
|
<Pencil className="w-3.5 h-3.5 mr-1" /> 수정
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<DataGrid
|
|
gridId="subcontractor-item-left"
|
|
columns={LEFT_COLUMNS}
|
|
data={items}
|
|
loading={itemLoading}
|
|
selectedId={selectedItemId}
|
|
onSelect={setSelectedItemId}
|
|
onRowDoubleClick={() => openEditItem()}
|
|
emptyMessage="등록된 외주품목이 없습니다"
|
|
/>
|
|
</div>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle withHandle />
|
|
|
|
{/* 우측: 외주업체 정보 */}
|
|
<ResizablePanel defaultSize={45} minSize={25}>
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center justify-between p-3 border-b bg-muted/10 shrink-0">
|
|
<div className="font-semibold flex items-center gap-2 text-sm">
|
|
<Users className="w-4 h-4" /> 외주업체 정보
|
|
{selectedItem && <Badge variant="outline" className="font-normal">{selectedItem.item_name}</Badge>}
|
|
</div>
|
|
<Button variant="outline" size="sm" disabled={!selectedItemId}
|
|
onClick={() => { setSubCheckedIds(new Set()); setSubSelectOpen(true); searchSubcontractors(); }}>
|
|
<Plus className="w-3.5 h-3.5 mr-1" /> 외주업체 추가
|
|
</Button>
|
|
</div>
|
|
{!selectedItemId ? (
|
|
<div className="flex-1 flex items-center justify-center text-muted-foreground text-sm">
|
|
좌측에서 품목을 선택하세요
|
|
</div>
|
|
) : (
|
|
<DataGrid
|
|
gridId="subcontractor-item-right"
|
|
columns={RIGHT_COLUMNS}
|
|
data={subcontractorItems}
|
|
loading={subcontractorLoading}
|
|
showRowNumber={false}
|
|
emptyMessage="등록된 외주업체가 없습니다"
|
|
/>
|
|
)}
|
|
</div>
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
|
|
{/* 품목 수정 모달 */}
|
|
<FullscreenDialog
|
|
open={editItemOpen}
|
|
onOpenChange={setEditItemOpen}
|
|
title="외주품목 수정"
|
|
description={`${editItemForm.item_number || ""} — ${editItemForm.item_name || ""}`}
|
|
defaultMaxWidth="max-w-2xl"
|
|
footer={
|
|
<>
|
|
<Button variant="outline" onClick={() => setEditItemOpen(false)}>취소</Button>
|
|
<Button onClick={handleEditSave} disabled={saving}>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : <Save className="w-4 h-4 mr-1.5" />} 저장
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<div className="grid grid-cols-2 gap-4 py-4">
|
|
{[
|
|
{ key: "item_number", label: "품목코드" },
|
|
{ key: "item_name", label: "품명" },
|
|
{ key: "size", label: "규격" },
|
|
{ key: "unit", label: "단위" },
|
|
{ key: "material", label: "재질" },
|
|
{ key: "status", label: "상태" },
|
|
].map((f) => (
|
|
<div key={f.key} className="space-y-1.5">
|
|
<Label className="text-sm text-muted-foreground">{f.label}</Label>
|
|
<Input value={editItemForm[f.key] || ""} className="h-9 bg-muted/50" disabled />
|
|
</div>
|
|
))}
|
|
|
|
<div className="col-span-2 border-t my-2" />
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">판매가격</Label>
|
|
<Input value={editItemForm.selling_price || ""} onChange={(e) => setEditItemForm((p) => ({ ...p, selling_price: e.target.value }))}
|
|
placeholder="판매가격" className="h-9" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">기준단가</Label>
|
|
<Input value={editItemForm.standard_price || ""} onChange={(e) => setEditItemForm((p) => ({ ...p, standard_price: e.target.value }))}
|
|
placeholder="기준단가" className="h-9" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">통화</Label>
|
|
<Select value={editItemForm.currency_code || ""} onValueChange={(v) => setEditItemForm((p) => ({ ...p, currency_code: v }))}>
|
|
<SelectTrigger className="h-9"><SelectValue placeholder="통화" /></SelectTrigger>
|
|
<SelectContent>
|
|
{(categoryOptions["currency_code"] || []).map((o) => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</FullscreenDialog>
|
|
|
|
{/* 외주업체 추가 모달 */}
|
|
<Dialog open={subSelectOpen} onOpenChange={setSubSelectOpen}>
|
|
<DialogContent className="max-w-2xl max-h-[70vh]">
|
|
<DialogHeader>
|
|
<DialogTitle>외주업체 선택</DialogTitle>
|
|
<DialogDescription>품목에 추가할 외주업체를 선택하세요.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex gap-2 mb-3">
|
|
<Input placeholder="외주업체명 검색" value={subSearchKeyword}
|
|
onChange={(e) => setSubSearchKeyword(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && searchSubcontractors()}
|
|
className="h-9 flex-1" />
|
|
<Button size="sm" onClick={searchSubcontractors} disabled={subSearchLoading} className="h-9">
|
|
{subSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> 조회</>}
|
|
</Button>
|
|
</div>
|
|
<div className="overflow-auto max-h-[350px] border rounded-lg">
|
|
<Table>
|
|
<TableHeader className="sticky top-0 bg-background z-10">
|
|
<TableRow>
|
|
<TableHead className="w-[40px] text-center">
|
|
<input type="checkbox"
|
|
checked={subSearchResults.length > 0 && subCheckedIds.size === subSearchResults.length}
|
|
onChange={(e) => {
|
|
if (e.target.checked) setSubCheckedIds(new Set(subSearchResults.map((s) => s.id)));
|
|
else setSubCheckedIds(new Set());
|
|
}} />
|
|
</TableHead>
|
|
<TableHead className="w-[110px]">외주업체코드</TableHead>
|
|
<TableHead className="min-w-[130px]">외주업체명</TableHead>
|
|
<TableHead className="w-[80px]">거래유형</TableHead>
|
|
<TableHead className="w-[80px]">담당자</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{subSearchResults.length === 0 ? (
|
|
<TableRow><TableCell colSpan={5} className="text-center text-muted-foreground py-8">검색 결과가 없습니다</TableCell></TableRow>
|
|
) : subSearchResults.map((s) => (
|
|
<TableRow key={s.id} className={cn("cursor-pointer", subCheckedIds.has(s.id) && "bg-primary/5")}
|
|
onClick={() => setSubCheckedIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(s.id)) next.delete(s.id); else next.add(s.id);
|
|
return next;
|
|
})}>
|
|
<TableCell className="text-center"><input type="checkbox" checked={subCheckedIds.has(s.id)} readOnly /></TableCell>
|
|
<TableCell className="text-xs">{s.subcontractor_code}</TableCell>
|
|
<TableCell className="text-sm">{s.subcontractor_name}</TableCell>
|
|
<TableCell className="text-xs">{s.division}</TableCell>
|
|
<TableCell className="text-xs">{s.contact_person}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<DialogFooter>
|
|
<div className="flex items-center gap-2 w-full justify-between">
|
|
<span className="text-sm text-muted-foreground">{subCheckedIds.size}개 선택됨</span>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" onClick={() => setSubSelectOpen(false)}>취소</Button>
|
|
<Button onClick={addSelectedSubcontractors} disabled={subCheckedIds.size === 0}>
|
|
<Plus className="w-4 h-4 mr-1" /> {subCheckedIds.size}개 추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 엑셀 업로드 */}
|
|
<ExcelUploadModal
|
|
open={excelUploadOpen}
|
|
onOpenChange={setExcelUploadOpen}
|
|
tableName={ITEM_TABLE}
|
|
userId={user?.userId}
|
|
onSuccess={() => fetchItems()}
|
|
/>
|
|
|
|
<TableSettingsModal
|
|
open={tableSettingsOpen}
|
|
onOpenChange={setTableSettingsOpen}
|
|
tableName={ITEM_TABLE}
|
|
settingsId="subcontractor-item"
|
|
onSave={applyTableSettings}
|
|
/>
|
|
|
|
{ConfirmDialogComponent}
|
|
</div>
|
|
);
|
|
}
|