223 lines
7.1 KiB
TypeScript
223 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
|
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import {
|
|
Button,
|
|
Skeleton,
|
|
Table,
|
|
TableBody,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@common/components";
|
|
|
|
import { useElaprAplnPage } from "../api/queries";
|
|
import { validateAplnRow, validateUniqueAtrzAprvr } from "../lib/apln-validators";
|
|
import { useAplnRows } from "../lib/use-apln-rows";
|
|
import { useDndSensors } from "../lib/use-dnd-sensors";
|
|
import type { ElaprApln } from "../model/elapr-apln-interface";
|
|
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
|
|
import type { ReviewerOption } from "../model/reviewer-option";
|
|
|
|
import { AprvrPickerDialog } from "./aprvr-picker-dialog";
|
|
import { SortableAplnPickerRow } from "./sortable-apln-picker-row";
|
|
|
|
interface ElaprAplnPickerGridProps {
|
|
elaprTmpltId?: string | null;
|
|
externalServerRows?: ElaprApln[];
|
|
isExternalLoading?: boolean;
|
|
onRowsChange?: (rows: ElaprAplnRow[]) => void;
|
|
onEditingLockedChange?: (locked: boolean) => void;
|
|
}
|
|
|
|
function ElaprAplnPickerGrid({
|
|
elaprTmpltId = null,
|
|
externalServerRows,
|
|
isExternalLoading = false,
|
|
onRowsChange,
|
|
onEditingLockedChange,
|
|
}: ElaprAplnPickerGridProps) {
|
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
|
|
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const isExternalMode = externalServerRows != null;
|
|
|
|
const aplnQuery = useElaprAplnPage({
|
|
elaprTmpltId: !isExternalMode ? (elaprTmpltId ?? undefined) : undefined,
|
|
page: 0,
|
|
size: 200,
|
|
sort: "atrzSeqNo,asc",
|
|
});
|
|
|
|
const seedKey = isExternalMode ? "external-doc-prgrs" : elaprTmpltId;
|
|
const serverRows = isExternalMode ? externalServerRows : aplnQuery.data?.content;
|
|
|
|
const {
|
|
rows,
|
|
visibleRows,
|
|
editingRowKey,
|
|
isEditingLocked,
|
|
addRow,
|
|
startEdit,
|
|
updateRow,
|
|
cancelEdit,
|
|
commitEdit,
|
|
markDeleted,
|
|
reorderRows,
|
|
} = useAplnRows({
|
|
seedKey,
|
|
serverRows,
|
|
elaprTmpltId: elaprTmpltId ?? "",
|
|
});
|
|
|
|
useEffect(() => {
|
|
onRowsChange?.(rows);
|
|
}, [rows, onRowsChange]);
|
|
|
|
useEffect(() => {
|
|
onEditingLockedChange?.(isEditingLocked);
|
|
}, [isEditingLocked, onEditingLockedChange]);
|
|
|
|
const sensors = useDndSensors();
|
|
const isReady = isExternalMode || Boolean(elaprTmpltId);
|
|
const isLoading = isExternalMode
|
|
? isExternalLoading
|
|
: aplnQuery.isPending && Boolean(elaprTmpltId);
|
|
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
|
|
const excludedVisibleRows = visibleRows.filter((r) => r.atrzSeComCd !== "ATRZ01");
|
|
|
|
function handleDragEnd(event: DragEndEvent) {
|
|
const { active, over } = event;
|
|
if (!over || active.id === over.id) return;
|
|
|
|
reorderRows(String(active.id), String(over.id));
|
|
}
|
|
|
|
function handleRowChange(row: ElaprAplnRow, patch: Partial<ElaprAplnRow>) {
|
|
updateRow(row.rowKey, patch);
|
|
}
|
|
|
|
function handleRowSave(row: ElaprAplnRow) {
|
|
const fieldError = validateAplnRow(row);
|
|
if (fieldError) {
|
|
alert(fieldError);
|
|
return;
|
|
}
|
|
|
|
const duplicateError = validateUniqueAtrzAprvr(rows);
|
|
if (duplicateError) {
|
|
alert(duplicateError);
|
|
return;
|
|
}
|
|
|
|
commitEdit(row.rowKey);
|
|
}
|
|
|
|
function handleRowDelete(row: ElaprAplnRow) {
|
|
markDeleted(row.rowKey);
|
|
}
|
|
|
|
function handleReviewerSelect(reviewer: ReviewerOption) {
|
|
if (!pickingRowKey) return;
|
|
updateRow(pickingRowKey, {
|
|
aprvrUuid: reviewer.uuid,
|
|
aprvrNm: reviewer.name,
|
|
});
|
|
setPickingRowKey(null);
|
|
}
|
|
|
|
return (
|
|
<section className="card card-border mx-0 min-h-[32rem] flex-1 pt-0">
|
|
<div className="card-header flex flex-wrap items-center justify-between gap-3">
|
|
<h3 className="text-2xl font-semibold">결재선 관리</h3>
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<Button
|
|
size="sm"
|
|
type="button"
|
|
color="green"
|
|
disabled={!isReady || isEditingLocked}
|
|
onClick={addRow}
|
|
>
|
|
결재선 추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex flex-col gap-2">
|
|
<Skeleton className="h-[4rem]" />
|
|
<Skeleton className="h-[16rem]" />
|
|
</div>
|
|
) : !isReady ? (
|
|
<p className="py-8 text-center text-lg text-muted-foreground">결재서식을 선택하세요.</p>
|
|
) : excludedVisibleRows.length === 0 ? (
|
|
<p className="py-16 text-center text-lg text-muted-foreground">
|
|
등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.
|
|
</p>
|
|
) : (
|
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
<Table caption="결재선 관리 목록으로 결재순서, 결재구분, 결재자, 관리 정보 제공" className="w-full">
|
|
<colgroup>
|
|
<col width="10%" />
|
|
<col width="10%" />
|
|
<col width="10%" />
|
|
<col width="50%" />
|
|
<col width="20%" />
|
|
</colgroup>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-10" />
|
|
<TableHead>결재순서</TableHead>
|
|
<TableHead>결재구분</TableHead>
|
|
<TableHead>결재자</TableHead>
|
|
<TableHead>관리</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<SortableContext
|
|
items={excludedVisibleRows.map((r) => r.rowKey)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{excludedVisibleRows.map((row, index) => (
|
|
<SortableAplnPickerRow
|
|
key={row.rowKey}
|
|
row={row}
|
|
displaySeqNo={index + 1}
|
|
isEditing={editingRowKey === row.rowKey}
|
|
isLocked={isEditingLocked}
|
|
dragDisabled={isEditingLocked || !row.atrzSeComCd}
|
|
onStartEdit={() => startEdit(row.rowKey)}
|
|
onSave={() => handleRowSave(row)}
|
|
onCancel={cancelEdit}
|
|
onDelete={() => handleRowDelete(row)}
|
|
onChange={(patch) => handleRowChange(row, patch)}
|
|
onPickAprvr={() => {
|
|
setPickingRowKey(row.rowKey);
|
|
setPickerOpen(true);
|
|
}}
|
|
/>
|
|
))}
|
|
</SortableContext>
|
|
</TableBody>
|
|
</Table>
|
|
</DndContext>
|
|
)}
|
|
|
|
<AprvrPickerDialog
|
|
open={pickerOpen}
|
|
onOpenChange={setPickerOpen}
|
|
atrzSeComCd={pickingRow?.atrzSeComCd ?? ""}
|
|
onSelect={handleReviewerSelect}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export { ElaprAplnPickerGrid };
|