Compare commits

..

3 Commits

Author SHA1 Message Date
unknown
eec18e7179 fix: show access state in reports 2026-07-22 19:51:35 +09:00
unknown
dee612fc5c fix: show access state in request list 2026-07-22 19:47:48 +09:00
unknown
7cbc660679 fix: refresh dashboard access state 2026-07-22 19:45:11 +09:00
3 changed files with 66 additions and 11 deletions

View File

@@ -44,6 +44,8 @@ export const DashboardPage: React.FC = () => {
useEffect(() => {
loadDashboard();
const t = setInterval(loadDashboard, 3000);
return () => clearInterval(t);
}, []);
const deleteCancelled = async (target: VisitRequestView) => {

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { listReportVisits, reportDownloadUrl } from '../api';
import { listReportVisits, listTodayAccess, reportDownloadUrl } from '../api';
import { DatePickerField } from '../components/DatePickerField';
import { VisitRequestView } from '../types';
import { AccessRecord, VisitRequestView } from '../types';
import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status';
const pad = (n: number) => String(n).padStart(2, '0');
@@ -21,21 +21,41 @@ export const ReportPage: React.FC = () => {
const [from, setFrom] = useState(firstOfMonthISO());
const [to, setTo] = useState(todayISO());
const [items, setItems] = useState<VisitRequestView[]>([]);
const [accessRecords, setAccessRecords] = useState<AccessRecord[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = () => {
setLoading(true);
setError(null);
listReportVisits(from, to)
.then(setItems)
Promise.all([
listReportVisits(from, to),
listTodayAccess().catch(() => [] as AccessRecord[]),
])
.then(([visits, records]) => {
setItems(visits);
setAccessRecords(records);
})
.catch((e) => {
setItems([]);
setAccessRecords([]);
setError(e instanceof Error ? e.message : '보고서 조회에 실패했습니다.');
})
.finally(() => setLoading(false));
};
const accessById = new Map(accessRecords.map((r) => [r.visitRequestId, r]));
const statusBadge = (r: VisitRequestView) => {
const access = accessById.get(r.id);
if (access?.inside) {
return { className: 'green', label: '재실중' };
}
if (access?.checkOutAt) {
return { className: 'gray', label: '퇴장' };
}
return { className: STATUS_CLASS[r.status], label: STATUS_LABEL[r.status] };
};
useEffect(() => {
load();
// Load the default month range once; explicit 조회 handles later date changes.
@@ -113,7 +133,12 @@ export const ReportPage: React.FC = () => {
<td>{r.workName || '-'}</td>
<td>{formatDateTime(r.visitFrom)}</td>
<td>{formatDateTime(r.visitTo)}</td>
<td><span className={`badge badge-${STATUS_CLASS[r.status]}`}>{STATUS_LABEL[r.status]}</span></td>
<td>
{(() => {
const badge = statusBadge(r);
return <span className={`badge badge-${badge.className}`}>{badge.label}</span>;
})()}
</td>
</tr>
))}
</tbody>

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { cancelVisitRequest, deleteVisitRequest, listVisitRequests, uploadVisitRequests } from '../api';
import { VisitRequestView } from '../types';
import { cancelVisitRequest, deleteVisitRequest, listTodayAccess, listVisitRequests, uploadVisitRequests } from '../api';
import { AccessRecord, VisitRequestView } from '../types';
import { STATUS_CLASS, STATUS_LABEL, formatVisitRange } from '../status';
import { Dialog } from '../components/Dialog';
import { useAuth } from '../auth/AuthContext';
export const VisitRequestListPage: React.FC = () => {
const [items, setItems] = useState<VisitRequestView[]>([]);
const [accessRecords, setAccessRecords] = useState<AccessRecord[]>([]);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@@ -18,13 +19,35 @@ export const VisitRequestListPage: React.FC = () => {
const load = () => {
setLoading(true);
listVisitRequests()
.then(setItems)
Promise.all([
listVisitRequests(),
listTodayAccess().catch(() => [] as AccessRecord[]),
])
.then(([requests, records]) => {
setItems(requests);
setAccessRecords(records);
})
.catch((e) => setError(e instanceof Error ? e.message : '조회 실패'))
.finally(() => setLoading(false));
};
useEffect(load, []);
useEffect(() => {
load();
const t = setInterval(load, 3000);
return () => clearInterval(t);
}, []);
const accessById = new Map(accessRecords.map((r) => [r.visitRequestId, r]));
const statusBadge = (r: VisitRequestView) => {
const access = accessById.get(r.id);
if (access?.inside) {
return { className: 'green', label: '재실중' };
}
if (access?.checkOutAt) {
return { className: 'gray', label: '퇴장' };
}
return { className: STATUS_CLASS[r.status], label: STATUS_LABEL[r.status] };
};
const confirmCancel = async () => {
const id = cancelingId;
@@ -105,7 +128,12 @@ export const VisitRequestListPage: React.FC = () => {
<td>{r.purpose || '-'}</td>
<td>{r.workName || '-'}</td>
<td>{formatVisitRange(r.visitFrom, r.visitTo)}</td>
<td><span className={`badge badge-${STATUS_CLASS[r.status]}`}>{STATUS_LABEL[r.status]}</span></td>
<td>
{(() => {
const badge = statusBadge(r);
return <span className={`badge badge-${badge.className}`}>{badge.label}</span>;
})()}
</td>
<td className="row-actions">
{r.status === 'APPROVED' && (
<button className="btn-link" onClick={() => navigate(`/badge/${r.id}`)}></button>