191 lines
6.3 KiB
TypeScript
191 lines
6.3 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useMemo, useState } from "react";
|
||
|
||
import { useAlertStore } from "@/shared/lib";
|
||
import {
|
||
Button,
|
||
Dialog,
|
||
DialogContent,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@common/components";
|
||
|
||
import { useElaprDocPrgrsByDocNo } from "../api/queries";
|
||
import { getVisibleRows } from "../lib/apln-validators";
|
||
import { mapDocPrgrsToForkJoinTrackRows } from "../lib/doc-prgrs-to-forkjoin-rows";
|
||
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
|
||
|
||
import { ElaprForkJoinPickerGrid } from "./elapr-forkjoin-picker-grid";
|
||
|
||
export interface ElaprDocPrgrsForkJoinPickerConfirmPayload {
|
||
aplnRows: ElaprAplnRow[];
|
||
}
|
||
|
||
export interface ElaprDocPrgrsForkJoinPickerDialogProps {
|
||
open: boolean;
|
||
elaprDocNo: string;
|
||
onClose: () => void;
|
||
onConfirm?: (payload: ElaprDocPrgrsForkJoinPickerConfirmPayload) => void;
|
||
}
|
||
|
||
export function ElaprDocPrgrsForkJoinPickerDialog({
|
||
open,
|
||
elaprDocNo,
|
||
onClose,
|
||
onConfirm,
|
||
}: ElaprDocPrgrsForkJoinPickerDialogProps) {
|
||
const alert = useAlertStore((state) => state.setMessage);
|
||
const [aplnRows, setAplnRows] = useState<ElaprAplnRow[]>([]);
|
||
const [isEditingLocked, setIsEditingLocked] = useState(false);
|
||
|
||
// 1차 결재(elaprCycl=1)에서 지정된 6인 결재선 조회
|
||
const docPrgrsQuery = useElaprDocPrgrsByDocNo(elaprDocNo, {
|
||
elaprCycl: 1,
|
||
enabled: open && Boolean(elaprDocNo),
|
||
});
|
||
|
||
// 1차 결재선 데이터를 3개 트랙(관로, 시설, 정책)으로 매핑
|
||
const initialTrackRows = useMemo(
|
||
() => mapDocPrgrsToForkJoinTrackRows(docPrgrsQuery.data),
|
||
[docPrgrsQuery.data],
|
||
);
|
||
|
||
const handleRowsChange = useCallback((rows: ElaprAplnRow[]) => {
|
||
setAplnRows(rows);
|
||
}, []);
|
||
|
||
const handleEditingLockedChange = useCallback((locked: boolean) => {
|
||
setIsEditingLocked(locked);
|
||
}, []);
|
||
|
||
function handleOpenChange(isOpen: boolean) {
|
||
if (!isOpen) {
|
||
setAplnRows([]);
|
||
setIsEditingLocked(false);
|
||
onClose();
|
||
}
|
||
}
|
||
|
||
function handleConfirm() {
|
||
if (isEditingLocked) {
|
||
alert("편집 중인 결재선이 있습니다. 저장을 완료해 주세요.");
|
||
return;
|
||
}
|
||
|
||
const unassigned = aplnRows.find((r) => !r.aprvrUuid);
|
||
if (unassigned) {
|
||
alert("지정되지 않은 결재자가 있습니다. 모든 트랙의 결재자를 선택해 주세요.");
|
||
return;
|
||
}
|
||
|
||
const pipeRows = aplnRows
|
||
.filter((r) => r.branchSeCd === "SP")
|
||
.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||
const facRows = aplnRows
|
||
.filter((r) => r.branchSeCd === "SF")
|
||
.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||
const aggRows = aplnRows
|
||
.filter((r) => r.branchSeCd === "TOTAL_AGG")
|
||
.sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||
|
||
// 1. 부서별 인원 수 (각각 최소 2명 이상, 최대 5명)
|
||
if (pipeRows.length < 2 || pipeRows.length > 5) {
|
||
alert("하수관로 검토 결재선에 결재자를 최소 2명 이상(최대 5명) 지정해 주세요.");
|
||
return;
|
||
}
|
||
if (facRows.length < 2 || facRows.length > 5) {
|
||
alert("하수처리시설 검토 결재선에 결재자를 최소 2명 이상(최대 5명) 지정해 주세요.");
|
||
return;
|
||
}
|
||
if (aggRows.length < 2 || aggRows.length > 5) {
|
||
alert("정책부 종합병합 결재선에 결재자를 최소 2명 이상(최대 5명) 지정해 주세요.");
|
||
return;
|
||
}
|
||
|
||
// 2. 정책부 1순번 병합자(ATRZ04) 지정 검증
|
||
if (aggRows[0]?.atrzSeComCd !== "ATRZ04") {
|
||
alert("[정책부] 첫 번째 결재구분은 '병합(ATRZ04)'이어야 합니다.");
|
||
return;
|
||
}
|
||
|
||
// 3. 정책부 최종 결재구분(ATRZ99) 지정 검증
|
||
const lastAggRow = aggRows[aggRows.length - 1];
|
||
if (lastAggRow?.atrzSeComCd !== "ATRZ99") {
|
||
alert("[정책부] 최종 결재구분은 '최종승인(ATRZ99)'이어야 합니다.");
|
||
return;
|
||
}
|
||
|
||
// 4. 동일 브랜치 내 중복 결재자 검증
|
||
const checkDuplicateInBranch = (rows: typeof pipeRows, branchName: string) => {
|
||
const uuids = rows.map((r) => r.aprvrUuid).filter(Boolean);
|
||
const uniqueUuids = new Set(uuids);
|
||
if (uniqueUuids.size !== uuids.length) {
|
||
const dupRow = rows.find((r, i) => uuids.indexOf(r.aprvrUuid) !== i);
|
||
return `[${dupRow?.aprvrNm || "결재자"}] 결재자가 ${branchName} 결재선에 중복 지정되었습니다.`;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const pipeDup = checkDuplicateInBranch(pipeRows, "관로부");
|
||
if (pipeDup) {
|
||
alert(pipeDup);
|
||
return;
|
||
}
|
||
const facDup = checkDuplicateInBranch(facRows, "시설부");
|
||
if (facDup) {
|
||
alert(facDup);
|
||
return;
|
||
}
|
||
const aggDup = checkDuplicateInBranch(aggRows, "정책부");
|
||
if (aggDup) {
|
||
alert(aggDup);
|
||
return;
|
||
}
|
||
|
||
const visibleRows = getVisibleRows(aplnRows);
|
||
onConfirm?.({ aplnRows: visibleRows });
|
||
|
||
setAplnRows([]);
|
||
setIsEditingLocked(false);
|
||
onClose();
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||
<DialogContent className="max-w-[100rem]" aria-describedby={undefined}>
|
||
<DialogHeader>
|
||
<DialogTitle>2차 결재선 선택 (복합사업 처리시설+하수관로 Fork-Join 3트랙)</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
{/* 1차 결재자 자동 매핑 안내 배너 */}
|
||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-900">
|
||
<p className="font-semibold">
|
||
ℹ️ 1차 기술검토 시 지정된 결재자(6인) 정보가 자동으로 매핑되었습니다.
|
||
</p>
|
||
<p className="mt-0.5 text-xs text-emerald-700">
|
||
동일 결재선으로 진행하거나 필요 시 결재자 변경/순서 조정한 후 하단의 [확인] 버튼을 눌러
|
||
2차 결재선을 확정해 주세요.
|
||
</p>
|
||
</div>
|
||
|
||
<ElaprForkJoinPickerGrid
|
||
initialTrackRows={initialTrackRows}
|
||
onRowsChange={handleRowsChange}
|
||
onEditingLockedChange={handleEditingLockedChange}
|
||
/>
|
||
|
||
<DialogFooter>
|
||
<Button type="button" color="white" onClick={() => handleOpenChange(false)}>
|
||
닫기
|
||
</Button>
|
||
<Button type="button" disabled={isEditingLocked} onClick={handleConfirm}>
|
||
확인
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|