[as] feat: 전결 로직 수정 및 결재현황 섹션 구현
This commit is contained in:
@@ -27,7 +27,9 @@ export function Providers({ children }: ProvidersProps) {
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<div style={{ fontSize: "1.4rem" }}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ export {
|
||||
findCurrentUserPendingApprovalRequest,
|
||||
hasCurrentUserPendingApprovalRequest,
|
||||
} from "./lib/approval-request-utils";
|
||||
export {
|
||||
APPROVER_STATUS_MAP,
|
||||
type ApproverStatusConfig,
|
||||
getApproverStatus,
|
||||
sortByAtrzSeqNo,
|
||||
} from "./lib/approval-utils";
|
||||
export {
|
||||
buildElaprPrgrsSttsListFromApln,
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
@@ -105,9 +111,12 @@ export type {
|
||||
export type { ReviewerOption } from "./model/reviewer-option";
|
||||
export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog";
|
||||
export { ElaprAplnPickerGrid } from "./ui/elapr-apln-picker-grid";
|
||||
export { ElaprApprovalDialog } from "./ui/elapr-approval-dialog";
|
||||
export { ElaprDocPrgrsStatus } from "./ui/elapr-doc-prgrs-status";
|
||||
export {
|
||||
ElaprTmpltPickerDialog,
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
ElaprTmpltPickerDialog,
|
||||
} from "./ui/elapr-tmplt-picker-dialog";
|
||||
export { ElaprTmpltSelectTable } from "./ui/elapr-tmplt-select-table";
|
||||
export { SortableAplnPickerRow } from "./ui/sortable-apln-picker-row";
|
||||
export { SortablePrgrsSttsRow } from "./ui/sortable-prgrs-stts-row";
|
||||
|
||||
+5
-4
@@ -1,4 +1,4 @@
|
||||
import type { ElaprDocPrgrsClientResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
|
||||
|
||||
export interface ApproverStatusConfig {
|
||||
label: string;
|
||||
@@ -7,9 +7,10 @@ export interface ApproverStatusConfig {
|
||||
|
||||
export const APPROVER_STATUS_MAP: Record<string, ApproverStatusConfig> = {
|
||||
ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" },
|
||||
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-800" },
|
||||
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800" },
|
||||
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800" },
|
||||
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-700" },
|
||||
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-700" },
|
||||
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-700" },
|
||||
ATRZ_PRGRS_999: { label: "결재안함", badgeClass: "bg-blue-100 text-blue-700" },
|
||||
};
|
||||
|
||||
export function sortByAtrzSeqNo(
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from "../model/elapr-prgrs-stts-interface";
|
||||
|
||||
export const PRGRS_STTS_WAITING_CD = "ATRZ_PRGRS_001";
|
||||
export const PRGRS_STTS_APPROVED_CD = "ATRZ_PRGRS_003";
|
||||
|
||||
/** 서버 행 비교용 최소 필드 */
|
||||
export interface PrgrsSttsServerRow {
|
||||
@@ -11,6 +12,7 @@ export interface PrgrsSttsServerRow {
|
||||
aprvrUuid?: string;
|
||||
atrzSeComCd?: string;
|
||||
atrzSeqNo?: number;
|
||||
dcarbYn?: string;
|
||||
}
|
||||
|
||||
/** 결재선 그리드 로컬 행 — 진행상태 API 응답 + UI 보조 필드 */
|
||||
@@ -29,6 +31,7 @@ export interface ElaprPrgrsSttsRow {
|
||||
ognzNm?: string;
|
||||
jbpsSeNm?: string;
|
||||
jbgdSeNm?: string;
|
||||
dcarbYn?: string;
|
||||
isNew?: boolean;
|
||||
isDirty?: boolean;
|
||||
/** 소프트 삭제 — 최종 저장 시 mode:D */
|
||||
@@ -41,6 +44,10 @@ export function isPrgrsSttsRowEditable(row: ElaprPrgrsSttsRow): boolean {
|
||||
return row.isNew === true || row.atrzPrgrsSttsComCd === PRGRS_STTS_WAITING_CD;
|
||||
}
|
||||
|
||||
export function isPrgrsSttsRowDcarbable(row: ElaprPrgrsSttsRow): boolean {
|
||||
return row.atrzPrgrsSttsComCd !== PRGRS_STTS_APPROVED_CD;
|
||||
}
|
||||
|
||||
function getVisibleRows(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] {
|
||||
return rows.filter((row) => !row.isDeleted);
|
||||
}
|
||||
@@ -85,7 +92,8 @@ function rowDiffersFromServer(row: ElaprPrgrsSttsRow, serverRow: PrgrsSttsServer
|
||||
return (
|
||||
row.aprvrUuid !== serverRow.aprvrUuid ||
|
||||
row.atrzSeComCd !== serverRow.atrzSeComCd ||
|
||||
row.atrzSeqNo !== serverRow.atrzSeqNo
|
||||
row.atrzSeqNo !== serverRow.atrzSeqNo ||
|
||||
(row.dcarbYn ?? "N") !== (serverRow.dcarbYn ?? "N")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,6 +157,7 @@ export function buildPrgrsSttsSavePayload(
|
||||
atrzSeComCd: row.atrzSeComCd,
|
||||
atrzSeqNo: row.atrzSeqNo,
|
||||
atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd,
|
||||
dcarbYn: row.dcarbYn ?? "N",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -164,6 +173,7 @@ export function buildPrgrsSttsSavePayload(
|
||||
atrzSeComCd: row.atrzSeComCd,
|
||||
atrzSeqNo: row.atrzSeqNo,
|
||||
atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd,
|
||||
dcarbYn: row.dcarbYn ?? "N",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ function toRow(item: ElaprDocPrgrsClientResponseDto): ElaprPrgrsSttsRow {
|
||||
ognzNm: item.ognzNm,
|
||||
jbpsSeNm: item.jbpsSeNm,
|
||||
jbgdSeNm: item.jbgdSeNm,
|
||||
dcarbYn: item.dcarbYn ?? "N",
|
||||
isNew: false,
|
||||
isDirty: false,
|
||||
isDeleted: false,
|
||||
@@ -71,6 +72,7 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp
|
||||
aprvrUuid: "",
|
||||
aprvrNm: "",
|
||||
atrzPrgrsSttsComCd: PRGRS_STTS_WAITING_CD,
|
||||
dcarbYn: "N",
|
||||
isNew: true,
|
||||
isDirty: true,
|
||||
isDeleted: false,
|
||||
@@ -104,6 +106,33 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp
|
||||
[editingRowKey],
|
||||
);
|
||||
|
||||
const setDcarbYn = useCallback(
|
||||
(rowKey: string, checked: boolean) => {
|
||||
if (editingRowKey != null && editingRowKey !== rowKey) return;
|
||||
|
||||
setRows((prev) => {
|
||||
const target = prev.find((r) => r.rowKey === rowKey);
|
||||
if (!target || target.isDeleted) return prev;
|
||||
|
||||
const nextYn = checked ? "Y" : "N";
|
||||
|
||||
return prev.map((row) => {
|
||||
if (row.isDeleted) return row;
|
||||
|
||||
if (row.rowKey === rowKey) {
|
||||
if ((row.dcarbYn ?? "N") === nextYn) return row;
|
||||
return { ...row, dcarbYn: nextYn, isDirty: true };
|
||||
}
|
||||
|
||||
if (!checked) return row;
|
||||
if ((row.dcarbYn ?? "N") === "N") return row;
|
||||
return { ...row, dcarbYn: "N", isDirty: true };
|
||||
});
|
||||
});
|
||||
},
|
||||
[editingRowKey],
|
||||
);
|
||||
|
||||
const cancelEdit = useCallback(() => {
|
||||
if (!editingRowKey) return;
|
||||
|
||||
@@ -202,6 +231,7 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp
|
||||
addRow,
|
||||
startEdit,
|
||||
updateRow,
|
||||
setDcarbYn,
|
||||
cancelEdit,
|
||||
commitEdit,
|
||||
markDeleted,
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ElaprPrgrsSttsResponseDto {
|
||||
atrzPrgrsSttsComNm?: string;
|
||||
rjctRsn?: string;
|
||||
atrzDt?: string;
|
||||
dcarbYn?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 ElaprPrgrsSttsSaveRequestDto */
|
||||
@@ -33,6 +34,7 @@ export interface ElaprPrgrsSttsSaveRequestDto {
|
||||
atrzSeComCd?: string;
|
||||
atrzSeqNo?: number;
|
||||
atrzPrgrsSttsComCd?: string;
|
||||
dcarbYn?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 ElaprPrgrsSttsSearchDto */
|
||||
|
||||
+24
-34
@@ -9,31 +9,13 @@ import { AxiosError } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
AprvrPickerDialog,
|
||||
buildPrgrsSttsSavePayload,
|
||||
elaprPrgrsSttsService,
|
||||
findCurrentUserPendingApprovalRequest,
|
||||
hasUnsavedPrgrsSttsChanges,
|
||||
isPrgrsSttsRowEditable,
|
||||
type ReviewerOption,
|
||||
useDndSensors,
|
||||
useElaprDocPrgrsByDocNo,
|
||||
usePrgrsSttsRows,
|
||||
useSaveElaprPrgrsStts,
|
||||
validateAllPrgrsSttsRows,
|
||||
validateAtrzOrder,
|
||||
validatePrgrsSttsRow,
|
||||
} from "@/entities/elapr";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Label,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -43,6 +25,22 @@ import {
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import { elaprPrgrsSttsService } from "../api/elapr-prgrs-stts-service";
|
||||
import { useElaprDocPrgrsByDocNo, useSaveElaprPrgrsStts } from "../api/queries";
|
||||
import { validateAtrzOrder } from "../lib/apln-validators";
|
||||
import { findCurrentUserPendingApprovalRequest } from "../lib/approval-request-utils";
|
||||
import {
|
||||
buildPrgrsSttsSavePayload,
|
||||
hasUnsavedPrgrsSttsChanges,
|
||||
isPrgrsSttsRowEditable,
|
||||
validateAllPrgrsSttsRows,
|
||||
validatePrgrsSttsRow,
|
||||
} from "../lib/prgrs-stts-save-utils";
|
||||
import { useDndSensors } from "../lib/use-dnd-sensors";
|
||||
import { usePrgrsSttsRows } from "../lib/use-prgrs-stts-rows";
|
||||
import type { ReviewerOption } from "../model/reviewer-option";
|
||||
|
||||
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
|
||||
import { SortablePrgrsSttsRow } from "./sortable-prgrs-stts-row";
|
||||
|
||||
interface ElaprApprovalDialogProps {
|
||||
@@ -68,7 +66,6 @@ function ElaprApprovalDialog({
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
|
||||
const [finalApprovalYn, setFinalApprovalYn] = useState(false);
|
||||
|
||||
const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null);
|
||||
|
||||
@@ -82,6 +79,7 @@ function ElaprApprovalDialog({
|
||||
addRow,
|
||||
startEdit,
|
||||
updateRow,
|
||||
setDcarbYn,
|
||||
cancelEdit,
|
||||
commitEdit,
|
||||
markDeleted,
|
||||
@@ -199,7 +197,6 @@ function ElaprApprovalDialog({
|
||||
try {
|
||||
await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, {
|
||||
atrzPrgrsSttsComCd: "ATRZ_PRGRS_003",
|
||||
finalApprovalYn,
|
||||
});
|
||||
alert("결재가 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
@@ -312,12 +309,13 @@ function ElaprApprovalDialog({
|
||||
>
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="8%" />
|
||||
<col width="10%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="12%" />
|
||||
<col width="20%" />
|
||||
<col width="30%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="24%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -325,6 +323,7 @@ function ElaprApprovalDialog({
|
||||
<TableHead>결재순서</TableHead>
|
||||
<TableHead>결재구분</TableHead>
|
||||
<TableHead>결재자</TableHead>
|
||||
<TableHead>전결자 지정</TableHead>
|
||||
<TableHead>결재상태</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
@@ -353,6 +352,7 @@ function ElaprApprovalDialog({
|
||||
setPickingRowKey(row.rowKey);
|
||||
setPickerOpen(true);
|
||||
}}
|
||||
onToggleDcarb={(checked) => setDcarbYn(row.rowKey, checked)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
@@ -383,16 +383,6 @@ function ElaprApprovalDialog({
|
||||
</Button>
|
||||
{canApprove && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mr-2 flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="all-approve"
|
||||
checked={finalApprovalYn}
|
||||
onCheckedChange={(checked) => setFinalApprovalYn(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="all-approve" className="text-xl">
|
||||
전결
|
||||
</Label>
|
||||
</div>
|
||||
<Button onClick={handleApprove} disabled={isBusy}>
|
||||
결재
|
||||
</Button>
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { cn, Skeleton } from "@common/components";
|
||||
|
||||
import { useElaprDocPrgrsByDocNo } from "../api/queries";
|
||||
import { getApproverStatus, sortByAtrzSeqNo } from "../lib/approval-utils";
|
||||
|
||||
const REJECT_STATUS_CD = "ATRZ_PRGRS_004";
|
||||
|
||||
interface ElaprDocPrgrsStatusProps {
|
||||
elaprDocNo: string | null | undefined;
|
||||
}
|
||||
|
||||
function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
|
||||
const status = getApproverStatus(item.atrzPrgrsSttsComCd);
|
||||
const isRejected = item.atrzPrgrsSttsComCd === REJECT_STATUS_CD;
|
||||
const rjctRsn = item.rjctRsn?.trim();
|
||||
const metaParts = [item.ognzNm, item.jbpsSeNm].filter(Boolean);
|
||||
|
||||
const statusBadge = (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-sm px-2 py-1 text-lg font-medium",
|
||||
status.badgeClass,
|
||||
isRejected && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
|
||||
)}
|
||||
>
|
||||
{status.label}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-[12rem] flex-1 flex-col items-center gap-3 p-4 text-center">
|
||||
{isRejected && rjctRsn ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
|
||||
<TooltipContent sideOffset={4} className="max-w-md whitespace-pre-wrap">
|
||||
{rjctRsn}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
statusBadge
|
||||
)}
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl text-muted-foreground">[{item.atrzSeComNm || "-"}]</span>
|
||||
<span className="text-xl font-semibold">{item.aprvrNm || item.aprvrUuid || "-"}</span>
|
||||
</div>
|
||||
{metaParts.length > 0 && (
|
||||
<span className="text-base text-gray-500">{metaParts.join(" · ")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) {
|
||||
const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo);
|
||||
const steps = sortByAtrzSeqNo(data ?? []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-6">
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (steps.length === 0) {
|
||||
return <div className="py-8 text-center text-xl text-gray-500">결재 정보가 없습니다.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto">
|
||||
{steps.map((item, index) => (
|
||||
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
|
||||
<div className="flex items-center">
|
||||
{index > 0 && <ChevronRight className="size-8 text-muted-foreground" />}
|
||||
</div>
|
||||
<ElaprDocPrgrsStep item={item} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprDocPrgrsStatus };
|
||||
+19
-2
@@ -6,9 +6,9 @@ import { MoveVertical } from "lucide-react";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type ElaprPrgrsSttsRow, isPrgrsSttsRowEditable } from "@/entities/elapr";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
cn,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { getApproverStatus } from "../../lib/approval-utils";
|
||||
import { getApproverStatus } from "../lib/approval-utils";
|
||||
import {
|
||||
type ElaprPrgrsSttsRow,
|
||||
isPrgrsSttsRowDcarbable,
|
||||
isPrgrsSttsRowEditable,
|
||||
} from "../lib/prgrs-stts-save-utils";
|
||||
|
||||
interface SortablePrgrsSttsRowProps {
|
||||
row: ElaprPrgrsSttsRow;
|
||||
@@ -33,6 +38,7 @@ interface SortablePrgrsSttsRowProps {
|
||||
onDelete: () => void;
|
||||
onChange: (patch: Partial<ElaprPrgrsSttsRow>) => void;
|
||||
onPickAprvr: () => void;
|
||||
onToggleDcarb: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
function SortablePrgrsSttsRow({
|
||||
@@ -47,6 +53,7 @@ function SortablePrgrsSttsRow({
|
||||
onDelete,
|
||||
onChange,
|
||||
onPickAprvr,
|
||||
onToggleDcarb,
|
||||
}: SortablePrgrsSttsRowProps) {
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSE");
|
||||
const selectableAtrzSeCodes = atrzSeCodes?.filter((code) => code.lwpoCd !== "ATRZ01");
|
||||
@@ -63,6 +70,7 @@ function SortablePrgrsSttsRow({
|
||||
};
|
||||
|
||||
const isEditable = isPrgrsSttsRowEditable(row);
|
||||
const isDcarbable = isPrgrsSttsRowDcarbable(row);
|
||||
const rowDisabled = isLocked && !isEditing;
|
||||
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
const status = getApproverStatus(row.atrzPrgrsSttsComCd);
|
||||
@@ -139,6 +147,15 @@ function SortablePrgrsSttsRow({
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{isDcarbable && !rowDisabled && (
|
||||
<Checkbox
|
||||
checked={row.dcarbYn === "Y"}
|
||||
onCheckedChange={(checked) => onToggleDcarb(checked === true)}
|
||||
aria-label="전결자 지정"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{isRejectedRow && rjctRsn ? (
|
||||
<Tooltip>
|
||||
@@ -1,418 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
AprvrPickerDialog,
|
||||
buildPrgrsSttsSavePayload,
|
||||
elaprPrgrsSttsService,
|
||||
findCurrentUserPendingApprovalRequest,
|
||||
hasUnsavedPrgrsSttsChanges,
|
||||
isPrgrsSttsRowEditable,
|
||||
type ReviewerOption,
|
||||
useDndSensors,
|
||||
useElaprDocPrgrsByDocNo,
|
||||
usePrgrsSttsRows,
|
||||
useSaveElaprPrgrsStts,
|
||||
validateAllPrgrsSttsRows,
|
||||
validateAtrzOrder,
|
||||
validatePrgrsSttsRow,
|
||||
} from "@/entities/elapr";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Label,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import { SortablePrgrsSttsRow } from "./sortable-prgrs-stts-row";
|
||||
|
||||
interface ElaprApprovalDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
elaprDocNo: string;
|
||||
currentUserUuid: string;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const UNSAVED_PRGRS_STTS_MESSAGE = "결재선 변경 이력이 있습니다.\n결재선 저장을 해 주세요.";
|
||||
|
||||
function ElaprApprovalDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
elaprDocNo,
|
||||
currentUserUuid,
|
||||
onSuccess,
|
||||
}: ElaprApprovalDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [rjctRsn, setRjctRsn] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
|
||||
const [finalApprovalYn, setFinalApprovalYn] = useState(false);
|
||||
|
||||
const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null);
|
||||
|
||||
const elaprDmndId = elaprDocPrgrsList?.[0]?.elaprDmndId;
|
||||
|
||||
const {
|
||||
rows,
|
||||
visibleRows,
|
||||
editingRowKey,
|
||||
isEditingLocked,
|
||||
addRow,
|
||||
startEdit,
|
||||
updateRow,
|
||||
cancelEdit,
|
||||
commitEdit,
|
||||
markDeleted,
|
||||
reorderRows,
|
||||
} = usePrgrsSttsRows({
|
||||
serverRows: elaprDocPrgrsList,
|
||||
elaprDmndId,
|
||||
});
|
||||
|
||||
const savePrgrsSttsMutation = useSaveElaprPrgrsStts();
|
||||
const sensors = useDndSensors();
|
||||
|
||||
const currentUserPrgrs = findCurrentUserPendingApprovalRequest(
|
||||
elaprDocPrgrsList,
|
||||
currentUserUuid,
|
||||
);
|
||||
const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId);
|
||||
|
||||
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
|
||||
const isBusy = isProcessing || savePrgrsSttsMutation.isPending;
|
||||
const isPrgrsSttsUnsaved = hasUnsavedPrgrsSttsChanges(rows, elaprDocPrgrsList ?? []);
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
reorderRows(String(active.id), String(over.id));
|
||||
}
|
||||
|
||||
function handleRowSave(rowKey: string) {
|
||||
const row = rows.find((r) => r.rowKey === rowKey);
|
||||
if (!row) return;
|
||||
|
||||
const fieldError = validatePrgrsSttsRow(row);
|
||||
if (fieldError) {
|
||||
alert(fieldError);
|
||||
return;
|
||||
}
|
||||
|
||||
commitEdit(rowKey);
|
||||
}
|
||||
|
||||
function handleReviewerSelect(reviewer: ReviewerOption) {
|
||||
if (!pickingRowKey) return;
|
||||
updateRow(pickingRowKey, {
|
||||
aprvrUuid: reviewer.uuid,
|
||||
aprvrNm: reviewer.name,
|
||||
ognzNm: reviewer.deptNm,
|
||||
});
|
||||
setPickingRowKey(null);
|
||||
}
|
||||
|
||||
function assertPrgrsSttsSaved(): boolean {
|
||||
if (isEditingLocked) {
|
||||
alert(UNSAVED_PRGRS_STTS_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPrgrsSttsUnsaved) {
|
||||
alert(UNSAVED_PRGRS_STTS_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSavePrgrsStts() {
|
||||
if (!elaprDmndId) {
|
||||
alert("결재 요청 정보가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const validationError = validateAllPrgrsSttsRows(rows, isEditingLocked);
|
||||
if (validationError) {
|
||||
alert(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
const orderError = validateAtrzOrder(rows);
|
||||
if (orderError) {
|
||||
alert(orderError);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, elaprDocPrgrsList ?? []);
|
||||
if (payload.length === 0) {
|
||||
alert("변경된 결재선이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await savePrgrsSttsMutation.mutateAsync(payload);
|
||||
alert("결재선이 저장되었습니다.");
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
|
||||
});
|
||||
onSuccess();
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = "결재선 저장 중 오류가 발생했습니다.";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? errorMessage;
|
||||
}
|
||||
alert(errorMessage);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApprove() {
|
||||
if (!currentUserPrgrs?.elaprPrgrsId) return;
|
||||
if (!assertPrgrsSttsSaved()) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, {
|
||||
atrzPrgrsSttsComCd: "ATRZ_PRGRS_003",
|
||||
finalApprovalYn,
|
||||
});
|
||||
alert("결재가 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
|
||||
});
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = "";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? "";
|
||||
} else {
|
||||
errorMessage = "결재 처리 중 오류가 발생했습니다.";
|
||||
}
|
||||
alert(errorMessage);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
if (!currentUserPrgrs?.elaprPrgrsId) return;
|
||||
if (!rjctRsn.trim()) {
|
||||
alert("반려 사유를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!assertPrgrsSttsSaved()) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, {
|
||||
atrzPrgrsSttsComCd: "ATRZ_PRGRS_004",
|
||||
rjctRsn: rjctRsn.trim(),
|
||||
});
|
||||
alert("반려 처리가 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo],
|
||||
});
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = "";
|
||||
if (error instanceof AxiosError) {
|
||||
errorMessage = error.response?.data?.message ?? "";
|
||||
} else {
|
||||
errorMessage = "반려 처리 중 오류가 발생했습니다.";
|
||||
}
|
||||
alert(errorMessage);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setRjctRsn("");
|
||||
setPickingRowKey(null);
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[72rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>전자결재</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<h4 className="text-xl font-semibold">결재 진행 단계</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={!elaprDmndId || isEditingLocked || isBusy}
|
||||
onClick={addRow}
|
||||
>
|
||||
결재선 추가
|
||||
</Button>
|
||||
{elaprDmndId && (
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={handleSavePrgrsStts}
|
||||
disabled={!isPrgrsSttsUnsaved || isEditingLocked || isBusy}
|
||||
>
|
||||
결재선 저장
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
) : visibleRows.length === 0 ? (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
{elaprDmndId
|
||||
? "등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요."
|
||||
: "결재 정보가 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="10%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="12%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10" />
|
||||
<TableHead>결재순서</TableHead>
|
||||
<TableHead>결재구분</TableHead>
|
||||
<TableHead>결재자</TableHead>
|
||||
<TableHead>결재상태</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<SortableContext
|
||||
items={visibleRows.map((r) => r.rowKey)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{visibleRows.map((row, index) => (
|
||||
<SortablePrgrsSttsRow
|
||||
key={row.rowKey}
|
||||
row={row}
|
||||
displaySeqNo={index + 1}
|
||||
isEditing={editingRowKey === row.rowKey}
|
||||
isLocked={isEditingLocked}
|
||||
dragDisabled={
|
||||
isEditingLocked || !isPrgrsSttsRowEditable(row) || !row.atrzSeComCd
|
||||
}
|
||||
onStartEdit={() => startEdit(row.rowKey)}
|
||||
onSave={() => handleRowSave(row.rowKey)}
|
||||
onCancel={cancelEdit}
|
||||
onDelete={() => markDeleted(row.rowKey)}
|
||||
onChange={(patch) => updateRow(row.rowKey, patch)}
|
||||
onPickAprvr={() => {
|
||||
setPickingRowKey(row.rowKey);
|
||||
setPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canApprove && (
|
||||
<div>
|
||||
<h4 className="mb-3 text-xl font-semibold">반려 의견</h4>
|
||||
<Textarea
|
||||
value={rjctRsn}
|
||||
onChange={(event) => setRjctRsn(event.target.value)}
|
||||
placeholder="반려 시 사유를 입력하세요"
|
||||
rows={4}
|
||||
className="!text-xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Button type="button" color="white" onClick={handleClose} disabled={isBusy}>
|
||||
취소
|
||||
</Button>
|
||||
{canApprove && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mr-2 flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="all-approve"
|
||||
checked={finalApprovalYn}
|
||||
onCheckedChange={(checked) => setFinalApprovalYn(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="all-approve" className="text-xl">
|
||||
전결
|
||||
</Label>
|
||||
</div>
|
||||
<Button onClick={handleApprove} disabled={isBusy}>
|
||||
결재
|
||||
</Button>
|
||||
<Button color="red" onClick={handleReject} disabled={isBusy}>
|
||||
반려
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
<AprvrPickerDialog
|
||||
open={pickerOpen}
|
||||
onOpenChange={setPickerOpen}
|
||||
atrzSeComCd={pickingRow?.atrzSeComCd ?? ""}
|
||||
onSelect={handleReviewerSelect}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprApprovalDialog };
|
||||
@@ -11,6 +11,7 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
ElaprApprovalDialog,
|
||||
type ElaprDmndSaveRequestDto,
|
||||
elaprDmndService,
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
@@ -45,8 +46,6 @@ import {
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
import { ElaprApprovalDialog } from "./elapr-approval-dialog";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: FnrsCnsltnMngDetResponseDto;
|
||||
docSeComCd: string;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { MoveVertical } from "lucide-react";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type ElaprPrgrsSttsRow, isPrgrsSttsRowEditable } from "@/entities/elapr";
|
||||
import { Button, cn, TableCell, TableRow } from "@common/components";
|
||||
|
||||
import { getApproverStatus } from "../../lib/approval-utils";
|
||||
|
||||
interface SortablePrgrsSttsRowProps {
|
||||
row: ElaprPrgrsSttsRow;
|
||||
displaySeqNo: number;
|
||||
isEditing: boolean;
|
||||
isLocked: boolean;
|
||||
dragDisabled: boolean;
|
||||
onStartEdit: () => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
onChange: (patch: Partial<ElaprPrgrsSttsRow>) => void;
|
||||
onPickAprvr: () => void;
|
||||
}
|
||||
|
||||
function SortablePrgrsSttsRow({
|
||||
row,
|
||||
displaySeqNo,
|
||||
isEditing,
|
||||
isLocked,
|
||||
dragDisabled,
|
||||
onStartEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onChange,
|
||||
onPickAprvr,
|
||||
}: SortablePrgrsSttsRowProps) {
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSE");
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: row.rowKey,
|
||||
disabled: dragDisabled,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.6 : 1,
|
||||
};
|
||||
|
||||
const isEditable = isPrgrsSttsRowEditable(row);
|
||||
const rowDisabled = isLocked && !isEditing;
|
||||
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
const status = getApproverStatus(row.atrzPrgrsSttsComCd);
|
||||
const isRejectedRow = row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_004";
|
||||
const rjctRsn = row.rjctRsn?.trim();
|
||||
|
||||
const statusBadge = (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-sm px-3 py-1.5 text-lg font-semibold",
|
||||
status.badgeClass,
|
||||
isRejectedRow && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
|
||||
)}
|
||||
>
|
||||
{status.label}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
|
||||
<TableCell align="center" className="w-10">
|
||||
{!dragDisabled && (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="cursor-grab !bg-transparent text-muted-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
aria-label="순서 변경"
|
||||
>
|
||||
<MoveVertical className="!size-6" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">{displaySeqNo}</TableCell>
|
||||
<TableCell align="center">
|
||||
{isEditing ? (
|
||||
<CodeSelect
|
||||
upCd="ATRZSE"
|
||||
className="min-w-[120px]"
|
||||
value={row.atrzSeComCd ?? ""}
|
||||
onValueChange={(v) =>
|
||||
onChange({
|
||||
atrzSeComCd: v,
|
||||
atrzSeComNm: findCodeLabel(atrzSeCodes, v),
|
||||
})
|
||||
}
|
||||
placeholder="결재구분"
|
||||
/>
|
||||
) : (
|
||||
atrzSeLabel || "-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span>{row.aprvrNm || row.aprvrUuid || "-"}</span>
|
||||
{isEditing && (
|
||||
<Button type="button" size="sm" onClick={onPickAprvr}>
|
||||
선택
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!isEditing && (row.ognzNm || row.jbpsSeNm || row.jbgdSeNm) && (
|
||||
<div className="text-base text-gray-500">
|
||||
{row.ognzNm ?? ""}
|
||||
{row.jbpsSeNm ? ` · ${row.jbpsSeNm}` : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{isRejectedRow && rjctRsn ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
|
||||
<TooltipContent sideOffset={4} className="max-w-md whitespace-pre-wrap">
|
||||
{rjctRsn}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
statusBadge
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button type="button" size="sm" onClick={onSave}>
|
||||
확인
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="white" onClick={onCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</>
|
||||
) : isEditable ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
disabled={rowDisabled}
|
||||
onClick={onStartEdit}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="gray"
|
||||
disabled={rowDisabled}
|
||||
onClick={onDelete}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export { SortablePrgrsSttsRow };
|
||||
@@ -3,7 +3,9 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
@@ -76,6 +78,9 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
||||
<div className="mt-6 space-y-4">
|
||||
<BasicInfoSection det={detDto} />
|
||||
<ContactInfoSection det={detDto} />
|
||||
<SectionCollapsible title="결재현황">
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.fnrsCnsltnDocNo} />
|
||||
</SectionCollapsible>
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<BusinessCostSection bizCstList={bizCstList} />
|
||||
<FnrsSharSection fnrsSharList={fnrsSharList} />
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { ElaprDocPrgrsClientResponseDto } from "@/entities/instl-sncn";
|
||||
|
||||
export interface ApproverStatusConfig {
|
||||
label: string;
|
||||
badgeClass: string;
|
||||
}
|
||||
|
||||
export const APPROVER_STATUS_MAP: Record<string, ApproverStatusConfig> = {
|
||||
ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" },
|
||||
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-800" },
|
||||
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800" },
|
||||
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800" },
|
||||
};
|
||||
|
||||
export function sortByAtrzSeqNo(
|
||||
list: ElaprDocPrgrsClientResponseDto[],
|
||||
): ElaprDocPrgrsClientResponseDto[] {
|
||||
return list.slice().sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
}
|
||||
|
||||
export function getApproverStatus(status: string | undefined): ApproverStatusConfig {
|
||||
return (
|
||||
APPROVER_STATUS_MAP[status ?? ""] ?? {
|
||||
label: status || "-",
|
||||
badgeClass: "bg-gray-100 text-gray-700",
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
buildElaprPrgrsSttsListFromAplnRows,
|
||||
ElaprApprovalDialog,
|
||||
type ElaprDmndSaveRequestDto,
|
||||
elaprDmndService,
|
||||
type ElaprTmpltPickerConfirmPayload,
|
||||
@@ -40,8 +41,6 @@ import { buildSavePayload, buildTechReviewReplyPayload } from "../../lib/save-pa
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
import { ElaprApprovalDialog } from "./elapr-approval-dialog";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: InstlSncnMngDetResponse;
|
||||
docSeComCd: string;
|
||||
|
||||
@@ -150,6 +150,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
setIsSubmitting(true);
|
||||
await instlSncnService.remove(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({ queryKey: instlSncnKeys.list() });
|
||||
router.push("/instl-sncn");
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
@@ -74,6 +76,9 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
<div className="mt-6 space-y-4">
|
||||
<BasicInfoSection det={detDto} />
|
||||
<ContactInfoSection det={detDto} />
|
||||
<SectionCollapsible title="결재현황">
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.instlSncnDocNo} />
|
||||
</SectionCollapsible>
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<BusinessCostSection bizCstList={bizCstList} />
|
||||
<ChecklistSection
|
||||
|
||||
@@ -152,6 +152,7 @@ export interface ElaprDocPrgrsClientResponseDto {
|
||||
jbpsSeNm?: string;
|
||||
jbgdSeNm?: string;
|
||||
ognzNm?: string;
|
||||
dcarbYn?: string;
|
||||
}
|
||||
|
||||
/** 사업규모 저장 공통 필드 */
|
||||
|
||||
Reference in New Issue
Block a user