Files
acs/frontend/src/pages/ReportPage.tsx
2026-07-27 15:08:36 +09:00

157 lines
5.4 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { listReportVisits, listTodayAccess, reportDownloadUrl } from '../api';
import { DatePickerField } from '../components/DatePickerField';
import { AccessRecord, ReportVisitView } from '../types';
import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status';
const pad = (n: number) => String(n).padStart(2, '0');
/** Today in YYYY-MM-DD (local). */
function todayISO(): string {
const d = new Date();
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** First day of the current month in YYYY-MM-DD (local). */
function firstOfMonthISO(): string {
const d = new Date();
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-01`;
}
export const ReportPage: React.FC = () => {
const [from, setFrom] = useState(firstOfMonthISO());
const [to, setTo] = useState(todayISO());
const [items, setItems] = useState<ReportVisitView[]>([]);
const [accessRecords, setAccessRecords] = useState<AccessRecord[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = () => {
setLoading(true);
setError(null);
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: ReportVisitView) => {
const access = accessById.get(r.id);
if (r.reportStatusLabel) {
return {
className: r.reportStatusLabel === '재실중' ? 'green' : r.reportStatusLabel === '퇴실' ? 'gray' : STATUS_CLASS[r.status],
label: r.reportStatusLabel,
};
}
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.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onDownload = () => {
// Trigger a download in-place via a temporary anchor. Using window.open left
// a blank tab behind (the .xlsx response has no HTML to render). The anchor's
// download attribute makes the browser save the file without navigating away.
// Same-origin, so the session cookie is sent automatically (dev proxy / nginx).
const a = document.createElement('a');
a.href = reportDownloadUrl(from, to);
a.download = `visits_${from}_${to}.xlsx`;
document.body.appendChild(a);
a.click();
a.remove();
};
return (
<div>
<div className="page-head"><h2> </h2></div>
<div className="card">
<div className="report-row">
<label className="field">
<span></span>
<DatePickerField value={from} onChange={setFrom} />
</label>
<label className="field">
<span></span>
<DatePickerField value={to} onChange={setTo} />
</label>
<button className="btn-ghost" onClick={load} disabled={loading}></button>
<button className="btn-primary" onClick={onDownload}> </button>
</div>
</div>
{error && <div className="alert alert-error">{error}</div>}
<div className="card">
<div className="page-head">
<h3> ({items.length})</h3>
</div>
{loading ? (
<p className="muted"> ...</p>
) : items.length === 0 ? (
<p className="muted"> .</p>
) : (
<table className="table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{items.map((r) => (
<tr key={r.id}>
<td>{r.visitorName}</td>
<td>{r.company || '-'}</td>
<td>{r.contact || '-'}</td>
<td>{r.zoneName || '-'}</td>
<td>{r.hostName}</td>
<td>{r.purpose || '-'}</td>
<td>{r.workName || '-'}</td>
<td>{formatDateTime(r.visitFrom)}</td>
<td>{formatDateTime(r.visitTo)}</td>
<td>
{(() => {
const badge = statusBadge(r);
return <span className={`badge badge-${badge.className}`}>{badge.label}</span>;
})()}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
};