Files
frontend/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-forkjoin-picker-dialog.tsx
T

191 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}