Enhance Cutting Plan and Work Instruction Functionality
- Integrated `SmartSelect` component for improved material selection in the cutting plan page, providing a better user experience when no materials are available. - Updated the work instruction modal to include routing options directly selectable by users, enhancing the flexibility of work instruction management. - Adjusted table layouts across various components to accommodate new data fields and improve overall UI consistency. (TASK: ERP-XXX)
This commit is contained in:
@@ -85,11 +85,12 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
NULLIF(si.delivery_address, ''),
|
||||
''
|
||||
) AS delivery_destination_name,
|
||||
-- 거래처명 (TASK:ERP-098 항목20): 출고 행 customer_name 우선,
|
||||
-- 없으면 customer_mng JOIN, 매핑 깨지면 코드 fallback
|
||||
-- 거래처명 (TASK:ERP-098 항목20): customer_mng JOIN 라벨을 최우선.
|
||||
-- 출고 생성 시 om.customer_name에 거래처 코드가 저장되는 경우가 있어
|
||||
-- JOIN 라벨 → 저장값 → 코드 순으로 fallback
|
||||
COALESCE(
|
||||
NULLIF(om.customer_name, ''),
|
||||
NULLIF(c.customer_name, ''),
|
||||
NULLIF(om.customer_name, ''),
|
||||
NULLIF(om.customer_code, ''),
|
||||
''
|
||||
) AS customer_name,
|
||||
|
||||
@@ -28,9 +28,12 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
|
||||
import {
|
||||
previewWorkInstructionNo, saveWorkInstruction,
|
||||
getEquipmentList, getEmployeeList, getRoutingVersions,
|
||||
RoutingVersionData,
|
||||
} from "@/lib/api/workInstruction";
|
||||
|
||||
// ─── 공용 다중선택 Popover (설비/작업조/작업자) ────────────────────
|
||||
@@ -128,6 +131,9 @@ export interface WorkInstructionApplyItem {
|
||||
equipmentIds?: string[];
|
||||
workTeams?: string[];
|
||||
workers?: string[];
|
||||
// 품목별 라우팅 (작업지시 등록창과 동일하게 직접 선택)
|
||||
routing?: string;
|
||||
routingOptions?: RoutingVersionData[];
|
||||
}
|
||||
|
||||
export interface WorkInstructionApplyModalProps {
|
||||
@@ -173,14 +179,26 @@ export default function WorkInstructionApplyModal({
|
||||
getEquipmentList().then((r) => { if (r.success) setEquipmentOptions(r.data || []); }).catch(() => {});
|
||||
getEmployeeList().then((r) => { if (r.success) setWorkerOptions(r.data || []); }).catch(() => {});
|
||||
|
||||
// 품목별 라우팅 등록 여부 사전 조회 (엣지 1 — 라우팅 미등록 안내).
|
||||
// 라우팅 세팅 자체는 백엔드 save 가 자동 처리하므로 여기서는 안내 표시용으로만 사용.
|
||||
// 품목별 라우팅 버전 사전 조회 — 작업지시 등록창과 동일 패턴(getRoutingVersions("__new__", itemCode)).
|
||||
// 조회 결과를 각 행의 routingOptions 에 채우고 is_default 버전을 초기 선택값으로 세팅.
|
||||
// routingStatus 는 라우팅 미등록 안내 배너용(엣지 1).
|
||||
const uniqueCodes = [...new Set(initialItems.map((x) => x.itemCode).filter(Boolean))];
|
||||
for (const code of uniqueCodes) {
|
||||
getRoutingVersions("__new__", code)
|
||||
.then((r) => {
|
||||
const has = !!(r.success && r.data && r.data.length > 0);
|
||||
setRoutingStatus((prev) => ({ ...prev, [code]: has }));
|
||||
if (r.success && r.data) {
|
||||
// 작업지시 등록창과 동일: is_default 버전을 기본 선택, 없으면 빈값
|
||||
const defaultRv = r.data.find((rv) => rv.is_default);
|
||||
setItems((prev) =>
|
||||
prev.map((it) =>
|
||||
it.itemCode === code
|
||||
? { ...it, routingOptions: r.data, routing: defaultRv?.id || "" }
|
||||
: it,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 조회 실패 시 안내를 띄우지 않음(미등록으로 단정하지 않음)
|
||||
@@ -218,7 +236,8 @@ export default function WorkInstructionApplyModal({
|
||||
qty: String(i.qty), remark: i.remark || "",
|
||||
sourceTable: i.sourceTable || "cutting_plan",
|
||||
sourceId: i.sourceId || (cuttingPlanId != null ? String(cuttingPlanId) : ""),
|
||||
routing: null,
|
||||
// 사용자가 선택한 라우팅 버전. 빈값이면 백엔드 save 가 기본 버전을 자동 resolve(104).
|
||||
routing: i.routing || null,
|
||||
startDate: i.startDate || "",
|
||||
endDate: i.endDate || "",
|
||||
equipmentIds: (i.equipmentIds || []).join(","),
|
||||
@@ -295,7 +314,7 @@ export default function WorkInstructionApplyModal({
|
||||
<div className="border rounded-lg p-5">
|
||||
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground mb-3">품목 목록</h4>
|
||||
<div className="overflow-auto">
|
||||
<Table className="min-w-[1700px]">
|
||||
<Table className="min-w-[1900px]">
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">순번</TableHead>
|
||||
@@ -304,6 +323,7 @@ export default function WorkInstructionApplyModal({
|
||||
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
|
||||
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">라우팅</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료예정일</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>
|
||||
@@ -328,6 +348,24 @@ export default function WorkInstructionApplyModal({
|
||||
value={item.qty}
|
||||
onChange={(e) => setItems((prev) => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{/* 작업지시 등록창과 동일하게 품목별 라우팅 버전 직접 선택. 옵션 0건이면 비활성. */}
|
||||
{(item.routingOptions || []).length > 0 ? (
|
||||
<SmartSelect
|
||||
value={item.routing || ""}
|
||||
onValueChange={(v) =>
|
||||
setItems((prev) => prev.map((it, i) => i === idx ? { ...it, routing: v } : it))}
|
||||
options={(item.routingOptions || []).map((rv) => ({
|
||||
code: rv.id,
|
||||
label: `${rv.version_name || "라우팅"}${rv.is_default ? " (기본)" : ""} - ${rv.processes.length}공정`,
|
||||
}))}
|
||||
placeholder="라우팅 선택"
|
||||
className="h-7 text-[13px]"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[12px] text-muted-foreground">라우팅 미등록</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="date" className="h-7 text-[13px]"
|
||||
value={item.startDate || ""}
|
||||
@@ -381,7 +419,7 @@ export default function WorkInstructionApplyModal({
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={13} className="text-center text-muted-foreground text-[12px] py-6">
|
||||
<TableCell colSpan={14} className="text-center text-muted-foreground text-[12px] py-6">
|
||||
품목이 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { SmartSelect } from "@/components/common/SmartSelect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -1454,17 +1455,19 @@ export default function CuttingPlanPage() {
|
||||
{/* 원자재 선택 */}
|
||||
<div className="flex flex-wrap items-center gap-2 border-b px-3 py-2">
|
||||
<span className="inline-flex items-center justify-center h-6 w-6 rounded bg-primary/10 text-primary font-bold text-xs">①</span>
|
||||
<Select value={mat1Id} onValueChange={setMat1Id}>
|
||||
<SelectTrigger className="h-7 w-[220px] text-xs flex-none">
|
||||
<SelectValue placeholder="-- 원자재 선택 --" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{materials.map((m) => (
|
||||
<SelectItem key={String(m.id)} value={String(m.id)}>{m.name}</SelectItem>
|
||||
))}
|
||||
{materials.length === 0 && <div className="px-2 py-4 text-center text-xs text-muted-foreground">등록된 원자재가 없습니다</div>}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{materials.length === 0 ? (
|
||||
<div className="flex h-7 w-[220px] flex-none items-center rounded border bg-muted/30 px-2 text-xs text-muted-foreground">
|
||||
등록된 원자재가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
<SmartSelect
|
||||
value={mat1Id}
|
||||
onValueChange={setMat1Id}
|
||||
options={materials.map((m) => ({ code: String(m.id), label: m.name }))}
|
||||
placeholder="-- 원자재 선택 --"
|
||||
className="h-7 w-[220px] text-xs flex-none"
|
||||
/>
|
||||
)}
|
||||
{mat1 && (
|
||||
<div className="flex items-center gap-1.5 rounded bg-primary/5 border border-primary/20 px-1.5 py-0.5 text-[11px]">
|
||||
<span className="font-semibold text-foreground">
|
||||
@@ -1480,16 +1483,19 @@ export default function CuttingPlanPage() {
|
||||
{showMat2 ? (
|
||||
<>
|
||||
<span className="inline-flex items-center justify-center h-6 w-6 rounded bg-violet-100 text-violet-600 font-bold text-xs">②</span>
|
||||
<Select value={mat2Id} onValueChange={setMat2Id}>
|
||||
<SelectTrigger className="h-7 w-[180px] text-[11px] flex-none border-violet-300">
|
||||
<SelectValue placeholder="-- 선택 --" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{materials.map((m) => (
|
||||
<SelectItem key={String(m.id)} value={String(m.id)}>{m.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{materials.length === 0 ? (
|
||||
<div className="flex h-7 w-[180px] flex-none items-center rounded border bg-muted/30 px-2 text-[11px] text-muted-foreground">
|
||||
등록된 원자재가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
<SmartSelect
|
||||
value={mat2Id}
|
||||
onValueChange={setMat2Id}
|
||||
options={materials.map((m) => ({ code: String(m.id), label: m.name }))}
|
||||
placeholder="-- 선택 --"
|
||||
className="h-7 w-[180px] text-[11px] flex-none border-violet-300"
|
||||
/>
|
||||
)}
|
||||
{mat2 && (
|
||||
<div className="flex items-center gap-1 rounded bg-violet-50 border border-violet-200 px-1.5 py-0.5 text-[11px]">
|
||||
<span className="font-semibold text-foreground">
|
||||
|
||||
@@ -800,21 +800,21 @@ export default function WorkInstructionPage() {
|
||||
<div className="border rounded-lg p-5">
|
||||
<h4 className="text-[13px] font-bold pb-2 border-b text-foreground mb-3">품목 목록</h4>
|
||||
<div className="overflow-auto">
|
||||
<Table className="min-w-[1500px]">
|
||||
<Table className="min-w-[2050px]">
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">순번</TableHead>
|
||||
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">라우팅</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료예정일</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>
|
||||
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업조</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업자</TableHead>
|
||||
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>
|
||||
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
|
||||
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">라우팅</TableHead>
|
||||
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
|
||||
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료예정일</TableHead>
|
||||
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>
|
||||
<TableHead className="w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업조</TableHead>
|
||||
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업자</TableHead>
|
||||
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -823,9 +823,9 @@ export default function WorkInstructionPage() {
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="text-[13px] text-center">{idx + 1}</TableCell>
|
||||
<TableCell className="text-[13px] font-medium">{item.itemCode}</TableCell>
|
||||
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName || item.itemCode}>{item.itemName || item.itemCode}</TableCell>
|
||||
<TableCell className="text-sm truncate max-w-[180px]" title={item.itemName || item.itemCode}>{item.itemName || item.itemCode}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.spec || "-"}</TableCell>
|
||||
<TableCell><Input type="number" className="h-7 text-[13px] w-20" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
|
||||
<TableCell><Input type="number" className="h-9 text-sm w-full min-w-[100px]" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={nv(item.routing || "")}
|
||||
@@ -834,7 +834,7 @@ export default function WorkInstructionPage() {
|
||||
setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, routing: val } : it));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="라우팅" /></SelectTrigger>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="라우팅" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안 함</SelectItem>
|
||||
{(item.routingOptions || []).map((rv: RoutingVersionData) => (
|
||||
@@ -846,10 +846,10 @@ export default function WorkInstructionPage() {
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="date" className="h-7 text-[13px]" value={item.startDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
|
||||
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.startDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="date" className="h-7 text-[13px]" value={item.endDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
|
||||
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.endDate || ""} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MultiSelectPopover
|
||||
@@ -879,7 +879,7 @@ export default function WorkInstructionPage() {
|
||||
emptyMessage="사원을 찾을 수 없어요"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell><Input className="h-7 text-[13px]" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
|
||||
<TableCell><Input className="h-9 text-sm w-full min-w-[160px]" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
|
||||
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setConfirmItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -924,25 +924,25 @@ export default function WorkInstructionPage() {
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/8 border border-primary/15 px-2 py-0.5 rounded-full font-mono">{editItems.length}건</span>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<Table className="min-w-[1500px]">
|
||||
<Table className="min-w-[2200px]">
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">순번</TableHead>
|
||||
{editOrder?.batch_no ? (
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">배치번호</TableHead>
|
||||
) : null}
|
||||
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
<TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>
|
||||
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">라우팅</TableHead>
|
||||
<TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공정작업기준</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료예정일</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>
|
||||
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업조</TableHead>
|
||||
<TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업자</TableHead>
|
||||
<TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>
|
||||
<TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>
|
||||
<TableHead className="w-[120px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>
|
||||
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">라우팅</TableHead>
|
||||
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공정작업기준</TableHead>
|
||||
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>
|
||||
<TableHead className="w-[220px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료예정일</TableHead>
|
||||
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>
|
||||
<TableHead className="w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업조</TableHead>
|
||||
<TableHead className="w-[200px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업자</TableHead>
|
||||
<TableHead className="w-[180px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>
|
||||
<TableHead className="w-[40px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -956,9 +956,9 @@ export default function WorkInstructionPage() {
|
||||
<TableCell className="text-[13px] font-mono text-primary">{editOrder.batch_no}</TableCell>
|
||||
) : null}
|
||||
<TableCell className="text-[13px] font-medium">{item.itemCode}</TableCell>
|
||||
<TableCell className="text-sm truncate max-w-[140px]" title={item.itemName}>{item.itemName || "-"}</TableCell>
|
||||
<TableCell className="text-sm truncate max-w-[180px]" title={item.itemName}>{item.itemName || "-"}</TableCell>
|
||||
<TableCell className="text-[13px] truncate" title={item.spec}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="text-right"><Input type="number" className="h-7 text-[13px] w-20 ml-auto" value={item.qty} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
|
||||
<TableCell className="text-right"><Input type="number" className="h-9 text-sm w-full min-w-[100px] ml-auto" value={item.qty} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={nv(item.routing || "")}
|
||||
@@ -967,7 +967,7 @@ export default function WorkInstructionPage() {
|
||||
setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, routing: val } : it));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs"><SelectValue placeholder="라우팅" /></SelectTrigger>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="라우팅" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안 함</SelectItem>
|
||||
{(item.routingOptions || []).map((rv: RoutingVersionData) => (
|
||||
@@ -982,7 +982,7 @@ export default function WorkInstructionPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
className="h-9 text-sm"
|
||||
disabled={!item.routing}
|
||||
onClick={() => {
|
||||
if (!editOrder || !item.routing) return;
|
||||
@@ -996,14 +996,14 @@ export default function WorkInstructionPage() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ClipboardCheck className="w-3 h-3 mr-1" /> 수정
|
||||
<ClipboardCheck className="w-3.5 h-3.5 mr-1" /> 수정
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="date" className="h-7 text-[13px]" value={item.startDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
|
||||
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.startDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="date" className="h-7 text-[13px]" value={item.endDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
|
||||
<Input type="date" className="h-9 text-sm w-full min-w-[200px]" value={item.endDate || ""} onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MultiSelectPopover
|
||||
@@ -1033,7 +1033,7 @@ export default function WorkInstructionPage() {
|
||||
emptyMessage="사원을 찾을 수 없어요"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell><Input className="h-7 text-[13px]" value={item.remark} placeholder="비고" onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
|
||||
<TableCell><Input className="h-9 text-sm w-full min-w-[160px]" value={item.remark} placeholder="비고" onChange={e => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
|
||||
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -429,7 +429,9 @@ export default function JeilGlassOrderPage() {
|
||||
if (!previewOrderNo) {
|
||||
previewOrderNo = `ORD-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${String(Date.now()).slice(-4)}`;
|
||||
}
|
||||
setMasterForm({ order_no: previewOrderNo, manager_id: user?.userId || "" });
|
||||
// status는 화면 Select 기본 표시값("수주")과 실제 저장값을 일치시킨다.
|
||||
// 비우면 백엔드가 'WAITING'으로 보정 → 출하계획 일괄 등록 가드에 차단됨.
|
||||
setMasterForm({ order_no: previewOrderNo, manager_id: user?.userId || "", status: "수주" });
|
||||
setModalDetailRows([]);
|
||||
setSelectedDestId("");
|
||||
setDestinations([]);
|
||||
@@ -1231,22 +1233,22 @@ export default function JeilGlassOrderPage() {
|
||||
<TableRow>
|
||||
<TableHead className="w-[30px]"></TableHead>
|
||||
<TableHead className="w-[36px]"></TableHead>
|
||||
<TableHead className="w-[40px]">No</TableHead>
|
||||
<TableHead className="min-w-[90px]">구분</TableHead>
|
||||
<TableHead className="min-w-[150px]">품명</TableHead>
|
||||
<TableHead className="min-w-[100px]">규격</TableHead>
|
||||
<TableHead className="min-w-[80px]">가로</TableHead>
|
||||
<TableHead className="min-w-[80px]">세로</TableHead>
|
||||
<TableHead className="min-w-[80px]">두께</TableHead>
|
||||
<TableHead className="min-w-[80px]">면적(㎡)</TableHead>
|
||||
<TableHead className="min-w-[60px]">단위</TableHead>
|
||||
<TableHead className="min-w-[120px]">포장재</TableHead>
|
||||
<TableHead className="min-w-[80px]">수량</TableHead>
|
||||
<TableHead className="min-w-[90px]">포장수량</TableHead>
|
||||
<TableHead className="min-w-[90px]">단가</TableHead>
|
||||
<TableHead className="min-w-[90px]">금액</TableHead>
|
||||
<TableHead className="min-w-[140px]">납기일</TableHead>
|
||||
<TableHead className="min-w-[160px]">납품장소</TableHead>
|
||||
<TableHead className="w-[44px]">No</TableHead>
|
||||
<TableHead className="min-w-[100px]">구분</TableHead>
|
||||
<TableHead className="min-w-[240px]">품명</TableHead>
|
||||
<TableHead className="min-w-[180px]">규격</TableHead>
|
||||
<TableHead className="min-w-[110px]">가로</TableHead>
|
||||
<TableHead className="min-w-[110px]">세로</TableHead>
|
||||
<TableHead className="min-w-[110px]">두께</TableHead>
|
||||
<TableHead className="min-w-[100px]">면적(㎡)</TableHead>
|
||||
<TableHead className="min-w-[90px]">단위</TableHead>
|
||||
<TableHead className="min-w-[170px]">포장재</TableHead>
|
||||
<TableHead className="min-w-[120px]">수량</TableHead>
|
||||
<TableHead className="min-w-[120px]">포장수량</TableHead>
|
||||
<TableHead className="min-w-[130px]">단가</TableHead>
|
||||
<TableHead className="min-w-[120px]">금액</TableHead>
|
||||
<TableHead className="min-w-[160px]">납기일</TableHead>
|
||||
<TableHead className="min-w-[220px]">납품장소</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -1293,7 +1295,7 @@ export default function JeilGlassOrderPage() {
|
||||
<span className="text-sm px-2">{row.part_name || "-"}</span>
|
||||
) : (
|
||||
<Input value={row.part_name || ""} onChange={(e) => updateDetailRow(idx, "part_name", e.target.value)}
|
||||
className="h-8 text-sm" placeholder="품명" />
|
||||
className="h-9 text-sm w-full" placeholder="품명" />
|
||||
)}
|
||||
</TableCell>
|
||||
{/* 규격: 품목검색 → 읽기전용, 행추가 → 입력 */}
|
||||
@@ -1302,20 +1304,20 @@ export default function JeilGlassOrderPage() {
|
||||
<span className="text-sm px-2">{row.spec || "-"}</span>
|
||||
) : (
|
||||
<Input value={row.spec || ""} onChange={(e) => updateDetailRow(idx, "spec", e.target.value)}
|
||||
className="h-8 text-sm" placeholder="규격" />
|
||||
className="h-9 text-sm w-full" placeholder="규격" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input value={formatNumber(row.width || "")} onChange={(e) => updateDetailRow(idx, "width", parseNumber(e.target.value))}
|
||||
className="h-8 text-sm text-right" placeholder="mm" />
|
||||
className="h-9 text-sm text-right w-full" placeholder="mm" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input value={formatNumber(row.height || "")} onChange={(e) => updateDetailRow(idx, "height", parseNumber(e.target.value))}
|
||||
className="h-8 text-sm text-right" placeholder="mm" />
|
||||
className="h-9 text-sm text-right w-full" placeholder="mm" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input value={row.thickness || ""} onChange={(e) => updateDetailRow(idx, "thickness", e.target.value)}
|
||||
className="h-8 text-sm text-right" placeholder="mm" />
|
||||
className="h-9 text-sm text-right w-full" placeholder="mm" />
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-right text-muted-foreground">
|
||||
{row.area || "-"}
|
||||
@@ -1326,14 +1328,14 @@ export default function JeilGlassOrderPage() {
|
||||
<span className="text-sm px-2">{row.unit || "-"}</span>
|
||||
) : (
|
||||
<Input value={row.unit || ""} onChange={(e) => updateDetailRow(idx, "unit", e.target.value)}
|
||||
className="h-8 text-sm" placeholder="㎡" />
|
||||
className="h-9 text-sm w-full" placeholder="㎡" />
|
||||
)}
|
||||
</TableCell>
|
||||
{/* 포장재: 등록된 옵션이 있으면 셀렉트, 없으면 안내 */}
|
||||
<TableCell>
|
||||
{(row.pkg_options && row.pkg_options.length > 0) ? (
|
||||
<Select value={row.pkg_code || ""} onValueChange={(v) => updateDetailRow(idx, "pkg_code", v)}>
|
||||
<SelectTrigger className="h-8 text-sm w-full"><SelectValue placeholder="포장재" /></SelectTrigger>
|
||||
<SelectTrigger className="h-9 text-sm w-full"><SelectValue placeholder="포장재" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{row.pkg_options.map((o: any) => (
|
||||
<SelectItem key={o.pkg_code} value={o.pkg_code}>
|
||||
@@ -1348,15 +1350,15 @@ export default function JeilGlassOrderPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input value={formatNumber(row.qty || "")} onChange={(e) => updateDetailRow(idx, "qty", parseNumber(e.target.value))}
|
||||
className="h-8 text-sm text-right" />
|
||||
className="h-9 text-sm text-right w-full" placeholder="수량" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input type="number" min="0" value={row.pack_count || "0"} onChange={(e) => updateDetailRow(idx, "pack_count", e.target.value)}
|
||||
className="h-8 text-sm text-right font-mono" disabled={!row.pkg_code} />
|
||||
className="h-9 text-sm text-right font-mono w-full" disabled={!row.pkg_code} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input value={formatNumber(row.unit_price || "")} onChange={(e) => updateDetailRow(idx, "unit_price", parseNumber(e.target.value))}
|
||||
className="h-8 text-sm text-right" />
|
||||
className="h-9 text-sm text-right w-full" placeholder="단가" />
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-right font-medium">
|
||||
{row.amount ? Number(row.amount).toLocaleString() : ""}
|
||||
@@ -1366,7 +1368,7 @@ export default function JeilGlassOrderPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input value={row.delivery_location || ""} onChange={(e) => updateDetailRow(idx, "delivery_location", e.target.value)}
|
||||
className="h-8 text-sm" placeholder="납품장소" />
|
||||
className="h-9 text-sm w-full" placeholder="납품장소" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -147,6 +147,8 @@ export function DetailFormModal({
|
||||
const [equipInspChecked, setEquipInspChecked] = useState<Set<string>>(new Set());
|
||||
// 공정에 지정된 설비 건수 — 0건(설비 미지정) vs 점검항목 0건 메시지 구분용 — TASK:ERP-102
|
||||
const [equipProcEquipCount, setEquipProcEquipCount] = useState<number>(0);
|
||||
// 설비 점검항목의 점검방법 카테고리 옵션 (코드→라벨 변환용) — TASK:ERP-102 후속
|
||||
const [equipMethodOptions, setEquipMethodOptions] = useState<CatOption[]>([]);
|
||||
|
||||
// 공정 설비 목록 (자재투입 자재별 설비 연결용) — TASK:ERP-022
|
||||
const [processEquipments, setProcessEquipments] = useState<any[]>([]);
|
||||
@@ -435,6 +437,34 @@ export function DetailFormModal({
|
||||
})();
|
||||
}, [open, formData.detail_type, formData.equip_inspection_apply, selectedProcessCode]);
|
||||
|
||||
// 설비 점검항목의 점검방법 카테고리 옵션 로드 — TASK:ERP-102 후속
|
||||
// equipment_inspection_item.inspection_method 가 카테고리 코드(CAT_...)로 저장됨 → 라벨 변환용
|
||||
useEffect(() => {
|
||||
if (!open || formData.detail_type !== "equip_inspection") return;
|
||||
if (equipMethodOptions.length) return;
|
||||
const flatten = (arr: any[]): CatOption[] => {
|
||||
const out: CatOption[] = [];
|
||||
const walk = (list: any[]) => {
|
||||
for (const v of list || []) {
|
||||
out.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) walk(v.children);
|
||||
}
|
||||
};
|
||||
walk(arr || []);
|
||||
return out;
|
||||
};
|
||||
(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(
|
||||
"/table-categories/equipment_inspection_item/inspection_method/values"
|
||||
);
|
||||
if (res.data?.data?.length) setEquipMethodOptions(flatten(res.data.data));
|
||||
} catch {
|
||||
/* 카테고리 미정의 시 빈 옵션 — 코드 fallback 으로 표시 */
|
||||
}
|
||||
})();
|
||||
}, [open, formData.detail_type, equipMethodOptions.length]);
|
||||
|
||||
// 검사항목 미적용(수동입력) 경로 셀렉트용 카테고리 옵션 로드 — TASK:ERP-061
|
||||
// 품목검사정보 화면(quality/item-inspection)과 동일 출처(inspection_standard) 사용
|
||||
useEffect(() => {
|
||||
@@ -1262,7 +1292,8 @@ export function DetailFormModal({
|
||||
</td>
|
||||
<td className="py-1">{item.equipment_name || item.equipment_code || "-"}</td>
|
||||
<td className="py-1">{item.inspection_item || "-"}</td>
|
||||
<td className="py-1">{item.inspection_method || "-"}</td>
|
||||
{/* 점검방법은 카테고리 코드(CAT_...)로 저장 → 라벨 변환, 매핑 없으면 코드 fallback — TASK:ERP-102 후속 */}
|
||||
<td className="py-1">{resolveCat(equipMethodOptions, item.inspection_method) || "-"}</td>
|
||||
<td className="py-1 font-mono">{item.lower_limit || item.upper_limit ? `${item.lower_limit || ""} ~ ${item.upper_limit || ""}${item.unit ? ` ${item.unit}` : ""}` : "-"}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user