feat: allow admin delete from dashboard

This commit is contained in:
unknown
2026-07-20 19:16:12 +09:00
parent 3ba95a0c02
commit 5ba8c5db5e

View File

@@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getStatsSummary, listInside, listTodayAccess, listVisitRequests } from '../api'; import { deleteVisitRequest, getStatsSummary, listInside, listTodayAccess, listVisitRequests } from '../api';
import { StatsSummary, VisitRequestView } from '../types'; import { StatsSummary, VisitRequestView } from '../types';
import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status'; import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status';
import { VisitRequestDetailDialog } from '../components/VisitRequestDetailDialog'; import { VisitRequestDetailDialog } from '../components/VisitRequestDetailDialog';
import { Dialog } from '../components/Dialog';
import { useAuth } from '../auth/AuthContext';
export const DashboardPage: React.FC = () => { export const DashboardPage: React.FC = () => {
const [items, setItems] = useState<VisitRequestView[]>([]); const [items, setItems] = useState<VisitRequestView[]>([]);
@@ -12,12 +14,14 @@ export const DashboardPage: React.FC = () => {
const [exitedIds, setExitedIds] = useState<Set<number>>(new Set()); const [exitedIds, setExitedIds] = useState<Set<number>>(new Set());
const [checkOutById, setCheckOutById] = useState<Map<number, string>>(new Map()); const [checkOutById, setCheckOutById] = useState<Map<number, string>>(new Map());
const [detailId, setDetailId] = useState<number | null>(null); const [detailId, setDetailId] = useState<number | null>(null);
const [deleting, setDeleting] = useState<VisitRequestView | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { hasRole } = useAuth();
useEffect(() => { const loadDashboard = () => {
getStatsSummary().then(setStats).catch(() => setStats(null)); getStatsSummary().then(setStats).catch(() => setStats(null));
// currently-inside visits → show "재실중" instead of the plain approved status
listInside() listInside()
.then((rows) => setInsideIds(new Set(rows.map((r) => r.visitRequestId)))) .then((rows) => setInsideIds(new Set(rows.map((r) => r.visitRequestId))))
.catch(() => setInsideIds(new Set())); .catch(() => setInsideIds(new Set()));
@@ -38,8 +42,32 @@ export const DashboardPage: React.FC = () => {
.then(setItems) .then(setItems)
.catch((e) => setError(e instanceof Error ? e.message : '조회 실패')) .catch((e) => setError(e instanceof Error ? e.message : '조회 실패'))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
};
useEffect(() => {
loadDashboard();
}, []); }, []);
const confirmDelete = async (text?: string) => {
const target = deleting;
if (!target) return;
if (text !== '삭제') {
setDeleting(null);
setError('삭제하려면 확인 입력란에 "삭제"를 입력하세요.');
return;
}
setDeleting(null);
setError(null);
setNotice(null);
try {
await deleteVisitRequest(target.id);
setNotice(`${target.visitorName} 신청을 삭제했습니다.`);
loadDashboard();
} catch (e) {
setError(e instanceof Error ? e.message : '삭제 실패');
}
};
return ( return (
<div> <div>
<div className="page-head"> <div className="page-head">
@@ -48,6 +76,7 @@ export const DashboardPage: React.FC = () => {
</div> </div>
{error && <div className="alert alert-error">{error}</div>} {error && <div className="alert alert-error">{error}</div>}
{notice && <div className="alert alert-info">{notice}</div>}
<div className="stat-grid"> <div className="stat-grid">
<StatCard label="오늘 출입 예정" value={stats?.todayVisits ?? 0} accent="blue" /> <StatCard label="오늘 출입 예정" value={stats?.todayVisits ?? 0} accent="blue" />
@@ -67,7 +96,7 @@ export const DashboardPage: React.FC = () => {
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
<th></th><th></th><th></th><th> </th><th> </th><th></th> <th></th><th></th><th></th><th> </th><th> </th><th></th><th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -98,6 +127,19 @@ export const DashboardPage: React.FC = () => {
<span className={`badge badge-${STATUS_CLASS[r.status]}`}>{STATUS_LABEL[r.status]}</span> <span className={`badge badge-${STATUS_CLASS[r.status]}`}>{STATUS_LABEL[r.status]}</span>
)} )}
</td> </td>
<td className="row-actions">
{hasRole('ADMIN') && (
<button
className="btn-link-danger"
onClick={(e) => {
e.stopPropagation();
setDeleting(r);
}}
>
</button>
)}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -108,6 +150,19 @@ export const DashboardPage: React.FC = () => {
{detailId != null && ( {detailId != null && (
<VisitRequestDetailDialog requestId={detailId} onClose={() => setDetailId(null)} /> <VisitRequestDetailDialog requestId={detailId} onClose={() => setDetailId(null)} />
)} )}
{deleting != null && (
<Dialog
title="출입신청 삭제"
message={`${deleting.visitorName} / ${deleting.zoneName || '-'} 신청을 삭제합니다. 실제 입/퇴장 기록이 있는 신청은 삭제되지 않습니다. 계속하려면 "삭제"를 입력하세요.`}
withInput
inputPlaceholder="삭제"
confirmLabel="삭제"
danger
onConfirm={confirmDelete}
onCancel={() => setDeleting(null)}
/>
)}
</div> </div>
); );
}; };