feat: add Node ACS admin workflows
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { reportDownloadUrl } from '../api';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { listReportVisits, reportDownloadUrl } from '../api';
|
||||
import { DatePickerField } from '../components/DatePickerField';
|
||||
import { VisitRequestView } from '../types';
|
||||
import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status';
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
@@ -18,6 +20,27 @@ function firstOfMonthISO(): string {
|
||||
export const ReportPage: React.FC = () => {
|
||||
const [from, setFrom] = useState(firstOfMonthISO());
|
||||
const [to, setTo] = useState(todayISO());
|
||||
const [items, setItems] = useState<VisitRequestView[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
listReportVisits(from, to)
|
||||
.then(setItems)
|
||||
.catch((e) => {
|
||||
setItems([]);
|
||||
setError(e instanceof Error ? e.message : '보고서 조회에 실패했습니다.');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
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
|
||||
@@ -37,7 +60,6 @@ export const ReportPage: React.FC = () => {
|
||||
<div className="page-head"><h2>출입관리 보고서</h2></div>
|
||||
|
||||
<div className="card">
|
||||
<p className="muted">기간을 선택하고 [엑셀 다운로드] 버튼을 클릭하면, 엑셀(.xlsx) 파일로 내려받습니다. (방문 시작일 기준)</p>
|
||||
<div className="report-row">
|
||||
<label className="field">
|
||||
<span>시작일</span>
|
||||
@@ -47,9 +69,57 @@ export const ReportPage: React.FC = () => {
|
||||
<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><span className={`badge badge-${STATUS_CLASS[r.status]}`}>{STATUS_LABEL[r.status]}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user