Files
acs/frontend/src/pages/ApprovalQueuePage.tsx
2026-07-22 19:04:54 +09:00

166 lines
5.5 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { approveRequest, listPendingRequests, rejectRequest } from '../api';
import { VisitRequestView } from '../types';
import { formatVisitRange } from '../status';
import { Dialog } from '../components/Dialog';
type SortKey = 'visitorName' | 'company' | 'zoneName' | 'purpose' | 'visitFrom';
type BusyAction = 'approve' | 'reject';
const COLUMNS: { key: SortKey; label: string }[] = [
{ key: 'visitorName', label: '방문자' },
{ key: 'company', label: '회사' },
{ key: 'zoneName', label: '출입구역' },
{ key: 'purpose', label: '출입목적' },
{ key: 'visitFrom', label: '출입기간' },
];
export const ApprovalQueuePage: React.FC = () => {
const [items, setItems] = useState<VisitRequestView[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [busyAction, setBusyAction] = useState<{ id: number; action: BusyAction } | null>(null);
const [sortKey, setSortKey] = useState<SortKey | null>(null);
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const [rejectingId, setRejectingId] = useState<number | null>(null);
const onSort = (key: SortKey) => {
if (sortKey === key) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
} else {
setSortKey(key);
setSortDir('asc');
}
};
const sortedItems = useMemo(() => {
if (!sortKey) return items;
const arr = [...items];
arr.sort((a, b) => {
const cmp = sortKey === 'visitFrom'
? new Date(a.visitFrom).getTime() - new Date(b.visitFrom).getTime()
: String(a[sortKey] ?? '').localeCompare(String(b[sortKey] ?? ''), 'ko');
return sortDir === 'asc' ? cmp : -cmp;
});
return arr;
}, [items, sortKey, sortDir]);
const load = () => {
setLoading(true);
listPendingRequests()
.then(setItems)
.catch((e) => setError(e instanceof Error ? e.message : '조회 실패'))
.finally(() => setLoading(false));
};
useEffect(load, []);
const approve = async (id: number) => {
setError(null);
setBusyAction({ id, action: 'approve' });
try {
await approveRequest(id);
load();
} catch (e) {
setError(e instanceof Error ? e.message : '처리 실패');
} finally {
setBusyAction(null);
}
};
const confirmReject = async (comment?: string) => {
const id = rejectingId;
setRejectingId(null);
if (id == null) return;
setError(null);
setBusyAction({ id, action: 'reject' });
try {
await rejectRequest(id, comment);
load();
} catch (e) {
setError(e instanceof Error ? e.message : '처리 실패');
} finally {
setBusyAction(null);
}
};
return (
<div>
<div className="page-head"><h2> ({items.length})</h2></div>
{error && <div className="alert alert-error">{error}</div>}
<div className="card">
{loading ? (
<p className="muted"> </p>
) : items.length === 0 ? (
<p className="muted"> .</p>
) : (
<table className="table">
<thead>
<tr>
{COLUMNS.map((c) => (
<th
key={c.key}
onClick={() => onSort(c.key)}
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
title="클릭하여 정렬"
>
{c.label}{sortKey === c.key ? (sortDir === 'asc' ? ' ▲' : ' ▼') : ''}
</th>
))}
<th></th>
</tr>
</thead>
<tbody>
{sortedItems.map((r) => (
<tr key={r.id}>
<td>{r.visitorName}</td>
<td>{r.company || '-'}</td>
<td>{r.zoneName || '-'}</td>
<td>{r.purpose}</td>
<td>{formatVisitRange(r.visitFrom, r.visitTo)}</td>
<td className="action-cell">
<button
className="btn-success"
disabled={busyAction?.id === r.id && busyAction.action === 'approve'}
onClick={() => {
if (busyAction?.id === r.id) return;
approve(r.id);
}}
>
{busyAction?.id === r.id && busyAction.action === 'approve' ? '처리 중...' : '승인'}
</button>
<button
className="btn-danger"
disabled={busyAction?.id === r.id && busyAction.action === 'reject'}
onClick={() => {
if (busyAction?.id === r.id) return;
setRejectingId(r.id);
}}
>
{busyAction?.id === r.id && busyAction.action === 'reject' ? '처리 중...' : '반려'}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{rejectingId != null && (
<Dialog
title="반려 처리"
message="반려 사유를 입력하세요 (선택)."
withInput
inputPlaceholder="반려 사유"
confirmLabel="반려"
danger
onConfirm={confirmReject}
onCancel={() => setRejectingId(null)}
/>
)}
</div>
);
};