fix: restrict deletion to cancelled requests
This commit is contained in:
@@ -4,7 +4,6 @@ import { deleteVisitRequest, getStatsSummary, listInside, listTodayAccess, listV
|
|||||||
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';
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
|
||||||
export const DashboardPage: React.FC = () => {
|
export const DashboardPage: React.FC = () => {
|
||||||
@@ -14,7 +13,6 @@ 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 [notice, setNotice] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -48,15 +46,8 @@ export const DashboardPage: React.FC = () => {
|
|||||||
loadDashboard();
|
loadDashboard();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const confirmDelete = async (text?: string) => {
|
const deleteCancelled = async (target: VisitRequestView) => {
|
||||||
const target = deleting;
|
if (target.status !== 'CANCELLED') return;
|
||||||
if (!target) return;
|
|
||||||
if (text !== '삭제') {
|
|
||||||
setDeleting(null);
|
|
||||||
setError('삭제하려면 확인 입력란에 "삭제"를 입력하세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDeleting(null);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setNotice(null);
|
setNotice(null);
|
||||||
try {
|
try {
|
||||||
@@ -128,12 +119,12 @@ export const DashboardPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="row-actions">
|
<td className="row-actions">
|
||||||
{hasRole('ADMIN') && (
|
{hasRole('ADMIN') && r.status === 'CANCELLED' && (
|
||||||
<button
|
<button
|
||||||
className="btn-link-danger"
|
className="btn-link-danger"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDeleting(r);
|
deleteCancelled(r);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
삭제
|
삭제
|
||||||
@@ -150,19 +141,6 @@ 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const VisitRequestListPage: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
const [cancelingId, setCancelingId] = useState<number | null>(null);
|
const [cancelingId, setCancelingId] = useState<number | null>(null);
|
||||||
const [deleting, setDeleting] = useState<VisitRequestView | null>(null);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { hasRole } = useAuth();
|
const { hasRole } = useAuth();
|
||||||
|
|
||||||
@@ -39,15 +38,8 @@ export const VisitRequestListPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = async (text?: string) => {
|
const deleteCancelled = async (target: VisitRequestView) => {
|
||||||
const target = deleting;
|
if (target.status !== 'CANCELLED') return;
|
||||||
if (!target) return;
|
|
||||||
if (text !== '삭제') {
|
|
||||||
setDeleting(null);
|
|
||||||
setError('삭제하려면 확인 입력란에 "삭제"를 입력하세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDeleting(null);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setNotice(null);
|
setNotice(null);
|
||||||
try {
|
try {
|
||||||
@@ -121,8 +113,8 @@ export const VisitRequestListPage: React.FC = () => {
|
|||||||
{(r.status === 'PENDING' || r.status === 'APPROVED') && (
|
{(r.status === 'PENDING' || r.status === 'APPROVED') && (
|
||||||
<button className="btn-link-danger" onClick={() => setCancelingId(r.id)}>취소</button>
|
<button className="btn-link-danger" onClick={() => setCancelingId(r.id)}>취소</button>
|
||||||
)}
|
)}
|
||||||
{hasRole('ADMIN') && (
|
{hasRole('ADMIN') && r.status === 'CANCELLED' && (
|
||||||
<button className="btn-link-danger" onClick={() => setDeleting(r)}>삭제</button>
|
<button className="btn-link-danger" onClick={() => deleteCancelled(r)}>삭제</button>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -143,18 +135,6 @@ export const VisitRequestListPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{deleting != null && (
|
|
||||||
<Dialog
|
|
||||||
title="출입신청 삭제"
|
|
||||||
message={`${deleting.visitorName} / ${deleting.zoneName || '-'} 신청과 관련 테스트 기록을 함께 삭제합니다. 계속하려면 "삭제"를 입력하세요.`}
|
|
||||||
withInput
|
|
||||||
inputPlaceholder="삭제"
|
|
||||||
confirmLabel="삭제"
|
|
||||||
danger
|
|
||||||
onConfirm={confirmDelete}
|
|
||||||
onCancel={() => setDeleting(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -588,6 +588,7 @@ export function createBusinessRouter(deps: BusinessRouterDeps = { query }): Rout
|
|||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
const visit = await visitById(dbQuery, id);
|
const visit = await visitById(dbQuery, id);
|
||||||
if (!visit) throw new ApiError(404, '방문 신청을 찾을 수 없습니다.');
|
if (!visit) throw new ApiError(404, '방문 신청을 찾을 수 없습니다.');
|
||||||
|
if (visit.status !== 'CANCELLED') throw new ApiError(400, '취소 상태인 신청만 삭제할 수 있습니다.');
|
||||||
|
|
||||||
await dbQuery('DELETE FROM pass_deliveries WHERE visit_request_id = $1', [id]);
|
await dbQuery('DELETE FROM pass_deliveries WHERE visit_request_id = $1', [id]);
|
||||||
await dbQuery('DELETE FROM approvals WHERE visit_request_id = $1', [id]);
|
await dbQuery('DELETE FROM approvals WHERE visit_request_id = $1', [id]);
|
||||||
|
|||||||
Reference in New Issue
Block a user