c9adfd7327
backend (M5, read-only): - devEoHistoryService: list + getByObjid - partMngHistList SQL 1:1 (NVL→COALESCE, PART_MNG.OBJID bigint cast, CODE_NAME→LEFT JOIN comm_code) - 동적 필터 10종 (Year/contract_objid/unit_code/part_no/part_name/change_option/eo_start~end/change_type/part_type/writer_id) - 신규등록 제외 가드: NOT (HIS_STATUS='DEPLOY' AND CHANGE_TYPE IS NULL AND REVISION='RE') + BOM_STATUS='deploy' - 품번변경(CHANGE_OPTION=0001790) 'A->B' 머지 CASE (part_no_disp/part_name_disp/revision_disp) frontend (M5): - change-list/page.tsx: 16셀 그리드 + 검색 8필드 + 페이징 - PartHisDetailDialog: 모든 필드 disabled, 4 섹션 (EO/프로젝트/PART/수량) - AdminPageRenderer dynamic 임포트 + 기존 menu_info URL 그대로 사용 개발관리 5개 메뉴 (M1~M5) baseline 완료. 본 PR 제외 (별 PR): writer SmartSelect, change_type/change_option comm_code 그룹 SmartSelect (그룹 ID 확정 후) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
149 lines
5.7 KiB
TypeScript
149 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
// 개발관리 > 설계변경 리스트 상세 다이얼로그 (read-only).
|
|
// wace partMngHisDetailPopUp.jsp 1:1 — 모든 필드 disabled.
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Loader2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { devEoHistoryApi, EoHistoryDetail } from "@/lib/api/devEoHistory";
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
objid: string | null;
|
|
}
|
|
|
|
export function PartHisDetailDialog({ open, onOpenChange, objid }: Props) {
|
|
const [row, setRow] = useState<EoHistoryDetail | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open || !objid) return;
|
|
let alive = true;
|
|
setLoading(true);
|
|
devEoHistoryApi.detail(objid)
|
|
.then((data) => { if (alive) setRow(data); })
|
|
.catch((e: any) => {
|
|
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
|
onOpenChange(false);
|
|
})
|
|
.finally(() => { if (alive) setLoading(false); });
|
|
return () => { alive = false; };
|
|
}, [open, objid, onOpenChange]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-5xl">
|
|
<DialogHeader>
|
|
<DialogTitle>설계변경 상세 정보 (PART_MNG_HISTORY)</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{loading || !row ? (
|
|
<div className="flex h-64 items-center justify-center">
|
|
<Loader2 className="h-6 w-6 animate-spin" />
|
|
</div>
|
|
) : (
|
|
<div className="max-h-[70vh] space-y-4 overflow-y-auto px-1 py-2 text-sm">
|
|
<Section title="EO 정보">
|
|
<Row>
|
|
<V label="EO_NO" value={row.eo_no} />
|
|
<V label="EO_DATE" value={row.eo_date} align="center" />
|
|
<V label="실행일" value={row.his_reg_date_title} align="center" />
|
|
</Row>
|
|
<Row>
|
|
<V label="EO구분" value={row.change_type_name} align="center" />
|
|
<V label="EO사유" value={row.change_option_name} align="center" />
|
|
<V label="담당자" value={row.writer_name} align="center" />
|
|
</Row>
|
|
</Section>
|
|
|
|
<Section title="프로젝트 / 유닛">
|
|
<Row>
|
|
<V label="프로젝트번호" value={row.project_no} />
|
|
<V label="프로젝트명" value={row.project_name} />
|
|
<V label="유닛명" value={row.unit_name} />
|
|
</Row>
|
|
</Section>
|
|
|
|
<Section title="PART 정보">
|
|
<Row>
|
|
<V label="모품번" value={row.parent_part_info} />
|
|
<V label="품번" value={row.part_no_disp ?? row.part_no} />
|
|
<V label="품명" value={row.part_name_disp ?? row.part_name} />
|
|
</Row>
|
|
<Row>
|
|
<V label="범주" value={row.part_type_name} align="center" />
|
|
<V label="Revision" value={row.revision_disp ?? row.revision} align="center" />
|
|
<V label="규격" value={row.spec} />
|
|
</Row>
|
|
<Row>
|
|
<V label="재료" value={row.material} />
|
|
<V label="중량" value={row.weight} align="right" />
|
|
<V label="메이커" value={row.maker} />
|
|
</Row>
|
|
<Row>
|
|
<V label="열처리경도" value={row.heat_treatment_hardness} />
|
|
<V label="열처리방법" value={row.heat_treatment_method} />
|
|
<V label="표면처리" value={row.surface_treatment} />
|
|
</Row>
|
|
<Row>
|
|
<V label="비고" value={row.remark} />
|
|
<V label="" value="" />
|
|
<V label="" value="" />
|
|
</Row>
|
|
</Section>
|
|
|
|
<Section title="수량 / BOM 상태">
|
|
<Row>
|
|
<V label="수량" value={row.qty} align="right" />
|
|
<V label="변경수량" value={row.qty_temp} align="right" />
|
|
<V label="BOM 상태" value={row.bom_qty_status} align="center" />
|
|
</Row>
|
|
<Row>
|
|
<V label="BOM 배포일" value={row.bom_deploy_date_title} align="center" />
|
|
<V label="이력 상태" value={row.his_status} align="center" />
|
|
<V label="" value="" />
|
|
</Row>
|
|
</Section>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>닫기</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
return (
|
|
<div className="rounded-md border bg-card p-3">
|
|
<div className="mb-2 text-sm font-semibold text-foreground">{title}</div>
|
|
<div className="space-y-2">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Row({ children }: { children: React.ReactNode }) {
|
|
return <div className="grid grid-cols-3 gap-3">{children}</div>;
|
|
}
|
|
|
|
function V({ label, value, align }: { label: string; value: any; align?: "left" | "center" | "right" }) {
|
|
const cls = align === "right" ? "text-right" : align === "center" ? "text-center" : "";
|
|
return (
|
|
<div>
|
|
{label && <Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>}
|
|
<div className={`min-h-9 rounded-md border bg-muted/30 px-2 py-2 ${cls}`}>
|
|
{value != null && value !== "" ? value : <span className="text-muted-foreground">—</span>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|