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(() => { useEffect(() => {
loadDashboard(); loadDashboard();
const t = setInterval(loadDashboard, 3000);
return () => clearInterval(t);
}, []); }, []);
const deleteCancelled = async (target: VisitRequestView) => { const deleteCancelled = async (target: VisitRequestView) => {

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { listReportVisits, reportDownloadUrl } from '../api'; import { listReportVisits, listTodayAccess, reportDownloadUrl } from '../api';
import { DatePickerField } from '../components/DatePickerField'; import { DatePickerField } from '../components/DatePickerField';
import { VisitRequestView } from '../types'; import { AccessRecord, VisitRequestView } from '../types';
import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status'; import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status';
const pad = (n: number) => String(n).padStart(2, '0'); const pad = (n: number) => String(n).padStart(2, '0');
@@ -21,21 +21,41 @@ export const ReportPage: React.FC = () => {
const [from, setFrom] = useState(firstOfMonthISO()); const [from, setFrom] = useState(firstOfMonthISO());
const [to, setTo] = useState(todayISO()); const [to, setTo] = useState(todayISO());
const [items, setItems] = useState<VisitRequestView[]>([]); const [items, setItems] = useState<VisitRequestView[]>([]);
const [accessRecords, setAccessRecords] = useState<AccessRecord[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const load = () => { const load = () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
listReportVisits(from, to) Promise.all([
.then(setItems) listReportVisits(from, to),
listTodayAccess().catch(() => [] as AccessRecord[]),
])
.then(([visits, records]) => {
setItems(visits);
setAccessRecords(records);
})
.catch((e) => { .catch((e) => {
setItems([]); setItems([]);
setAccessRecords([]);
setError(e instanceof Error ? e.message : '보고서 조회에 실패했습니다.'); setError(e instanceof Error ? e.message : '보고서 조회에 실패했습니다.');
}) })
.finally(() => setLoading(false)); .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(() => { useEffect(() => {
load(); load();
// Load the default month range once; explicit 조회 handles later date changes. // 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>{r.workName || '-'}</td>
<td>{formatDateTime(r.visitFrom)}</td> <td>{formatDateTime(r.visitFrom)}</td>
<td>{formatDateTime(r.visitTo)}</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> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { cancelVisitRequest, deleteVisitRequest, listVisitRequests, uploadVisitRequests } from '../api'; import { cancelVisitRequest, deleteVisitRequest, listTodayAccess, listVisitRequests, uploadVisitRequests } from '../api';
import { VisitRequestView } from '../types'; import { AccessRecord, VisitRequestView } from '../types';
import { STATUS_CLASS, STATUS_LABEL, formatVisitRange } from '../status'; import { STATUS_CLASS, STATUS_LABEL, formatVisitRange } from '../status';
import { Dialog } from '../components/Dialog'; import { Dialog } from '../components/Dialog';
import { useAuth } from '../auth/AuthContext'; import { useAuth } from '../auth/AuthContext';
export const VisitRequestListPage: React.FC = () => { export const VisitRequestListPage: React.FC = () => {
const [items, setItems] = useState<VisitRequestView[]>([]); const [items, setItems] = useState<VisitRequestView[]>([]);
const [accessRecords, setAccessRecords] = useState<AccessRecord[]>([]);
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);
@@ -18,13 +19,35 @@ export const VisitRequestListPage: React.FC = () => {
const load = () => { const load = () => {
setLoading(true); setLoading(true);
listVisitRequests() Promise.all([
.then(setItems) listVisitRequests(),
listTodayAccess().catch(() => [] as AccessRecord[]),
])
.then(([requests, records]) => {
setItems(requests);
setAccessRecords(records);
})
.catch((e) => setError(e instanceof Error ? e.message : '조회 실패')) .catch((e) => setError(e instanceof Error ? e.message : '조회 실패'))
.finally(() => setLoading(false)); .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 confirmCancel = async () => {
const id = cancelingId; const id = cancelingId;
@@ -105,7 +128,12 @@ export const VisitRequestListPage: React.FC = () => {
<td>{r.purpose || '-'}</td> <td>{r.purpose || '-'}</td>
<td>{r.workName || '-'}</td> <td>{r.workName || '-'}</td>
<td>{formatVisitRange(r.visitFrom, r.visitTo)}</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"> <td className="row-actions">
{r.status === 'APPROVED' && ( {r.status === 'APPROVED' && (
<button className="btn-link" onClick={() => navigate(`/badge/${r.id}`)}></button> <button className="btn-link" onClick={() => navigate(`/badge/${r.id}`)}></button>