[agent-pipeline] pipe-20260305181927-h4x5 round-4

This commit is contained in:
DDD1542 2026-03-06 03:46:39 +09:00
parent 182634f852
commit 2439951fab
1 changed files with 87 additions and 7 deletions

View File

@ -254,22 +254,36 @@ function ReceivedTab() {
const [processOpen, setProcessOpen] = useState(false); const [processOpen, setProcessOpen] = useState(false);
const [selectedLine, setSelectedLine] = useState<ApprovalLine | null>(null); const [selectedLine, setSelectedLine] = useState<ApprovalLine | null>(null);
const [selectedRequest, setSelectedRequest] = useState<ApprovalRequest | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const fetchPending = useCallback(async () => { const fetchPending = useCallback(async () => {
setLoading(true); setLoading(true);
const res = await getMyPendingApprovals(); try {
if (res.success && res.data) setPendingLines(res.data); const res = await getMyPendingApprovals();
if (res.success && res.data) setPendingLines(res.data);
} catch {
setPendingLines([]);
}
setLoading(false); setLoading(false);
}, []); }, []);
useEffect(() => { fetchPending(); }, [fetchPending]); useEffect(() => { fetchPending(); }, [fetchPending]);
const openProcess = (line: ApprovalLine) => { const openProcess = async (line: ApprovalLine) => {
setSelectedLine(line); setSelectedLine(line);
setSelectedRequest(null);
setComment(""); setComment("");
setProcessOpen(true); setProcessOpen(true);
// 결재 상세 내용(결재선 포함) 비동기 로드
if (line.request_id) {
setDetailLoading(true);
const res = await getApprovalRequest(line.request_id);
if (res.success && res.data) setSelectedRequest(res.data);
setDetailLoading(false);
}
}; };
const handleProcess = async (action: "approved" | "rejected") => { const handleProcess = async (action: "approved" | "rejected") => {
@ -342,14 +356,14 @@ function ReceivedTab() {
{/* 결재 처리 모달 */} {/* 결재 처리 모달 */}
<Dialog open={processOpen} onOpenChange={setProcessOpen}> <Dialog open={processOpen} onOpenChange={setProcessOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[450px]"> <DialogContent className="max-w-[95vw] sm:max-w-[520px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle> <DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm"> <DialogDescription className="text-xs sm:text-sm">
{selectedLine?.title} {selectedLine?.title}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3"> <div className="max-h-[55vh] space-y-4 overflow-y-auto">
<div className="grid grid-cols-2 gap-3 text-sm"> <div className="grid grid-cols-2 gap-3 text-sm">
<div> <div>
<span className="text-muted-foreground text-xs"></span> <span className="text-muted-foreground text-xs"></span>
@ -359,18 +373,84 @@ function ReceivedTab() {
<span className="text-muted-foreground text-xs"> </span> <span className="text-muted-foreground text-xs"> </span>
<p className="mt-1 font-medium">{selectedLine?.step_order} </p> <p className="mt-1 font-medium">{selectedLine?.step_order} </p>
</div> </div>
{selectedLine?.target_table && (
<div>
<span className="text-muted-foreground text-xs"> </span>
<p className="mt-1 font-medium">{selectedLine.target_table}</p>
</div>
)}
<div>
<span className="text-muted-foreground text-xs"></span>
<p className="mt-1">{formatDate(selectedLine?.request_created_at || selectedLine?.created_at)}</p>
</div>
</div> </div>
{/* 결재선 상세 */}
{detailLoading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
<span className="text-muted-foreground ml-2 text-xs"> ...</span>
</div>
) : selectedRequest?.lines && selectedRequest.lines.length > 0 && (
<div>
<span className="text-muted-foreground text-xs"> </span>
<div className="mt-2 space-y-2">
{selectedRequest.lines
.sort((a, b) => a.step_order - b.step_order)
.map((line) => (
<div
key={line.line_id}
className={`flex items-center justify-between rounded-md border p-2 ${
line.line_id === selectedLine?.line_id ? "border-primary bg-primary/5" : "bg-muted/30"
}`}
>
<div className="flex items-center gap-2">
<Badge
variant={line.line_id === selectedLine?.line_id ? "default" : "outline"}
className="text-[10px]"
>
{line.step_order}
</Badge>
<span className="text-sm font-medium">{line.approver_name || line.approver_id}</span>
{line.approver_position && (
<span className="text-muted-foreground text-xs">({line.approver_position})</span>
)}
{line.line_id === selectedLine?.line_id && (
<Badge variant="secondary" className="text-[10px]"> </Badge>
)}
</div>
<div className="flex items-center gap-2">
<StatusBadge status={line.status} />
{line.processed_at && (
<span className="text-muted-foreground text-[10px]">{formatDate(line.processed_at)}</span>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* 의견 입력 */}
<div> <div>
<Label className="text-xs sm:text-sm"></Label> <Label className="text-xs sm:text-sm"> </Label>
<Textarea <Textarea
value={comment} value={comment}
onChange={(e) => setComment(e.target.value)} onChange={(e) => setComment(e.target.value)}
placeholder="결재 의견을 입력하세요 (선택사항)" placeholder="결재 의견을 입력하세요 (선택사항)"
className="min-h-[80px] text-xs sm:text-sm" className="mt-1 min-h-[80px] text-xs sm:text-sm"
/> />
</div> </div>
</div> </div>
<DialogFooter className="gap-2 sm:gap-0"> <DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => setProcessOpen(false)}
disabled={isProcessing}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button <Button
variant="destructive" variant="destructive"
onClick={() => handleProcess("rejected")} onClick={() => handleProcess("rejected")}