1. 출입증 카드 하단 안내문을 한 줄로(.badge-foot font-size 11px + nowrap). 2. 보고서 화면: 명칭 '리포트→보고서'·'방문 리포트→출입관리 보고서', 안내문 보강, 날짜 입력을 YYYY.MM.DD 표시 커스텀 피커(DatePickerField)로 교체, 기본값 시작일=이번 달 1일·종료일=오늘. 3. 보고서 엑셀 열 너비를 내용 기준(한글 2폭)으로 계산해 설정 → 셀 잘림 해소 (POI autoSizeColumn의 CJK 과소측정 문제 회피). 4. 상단바에서 사용자 이름 표시 제거(역할 태그만 유지) → '발송내역' 메뉴 잘림 해소, 발송내역 화면 [새로고침]→[조회]. 5. 대시보드 '최근 출입 신청'에서 현재 재실 중인 방문자는 상태를 '재실중'으로 표시 (기존 listInside API 재활용, 백엔드 무변경). 검증: 프론트 tsc+vite 빌드 통과, 백엔드 build+test(9건) 통과, 생성 xlsx의 열 너비가 내용에 맞게 설정됨(연락처15·출입일시18·상태10 등) 확인. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
3.2 KiB
TypeScript
87 lines
3.2 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { getStatsSummary, listInside, listVisitRequests } from '../api';
|
|
import { StatsSummary, VisitRequestView } from '../types';
|
|
import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status';
|
|
|
|
export const DashboardPage: React.FC = () => {
|
|
const [items, setItems] = useState<VisitRequestView[]>([]);
|
|
const [stats, setStats] = useState<StatsSummary | null>(null);
|
|
const [insideIds, setInsideIds] = useState<Set<number>>(new Set());
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
getStatsSummary().then(setStats).catch(() => setStats(null));
|
|
// currently-inside visits → show "재실중" instead of the plain approved status
|
|
listInside()
|
|
.then((rows) => setInsideIds(new Set(rows.map((r) => r.visitRequestId))))
|
|
.catch(() => setInsideIds(new Set()));
|
|
listVisitRequests()
|
|
.then(setItems)
|
|
.catch((e) => setError(e instanceof Error ? e.message : '조회 실패'))
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const recent = items.slice(0, 8);
|
|
|
|
return (
|
|
<div>
|
|
<div className="page-head">
|
|
<h2>대시보드</h2>
|
|
<Link className="btn-primary" to="/visit-requests/new">+ 출입 신청</Link>
|
|
</div>
|
|
|
|
{error && <div className="alert alert-error">{error}</div>}
|
|
|
|
<div className="stat-grid">
|
|
<StatCard label="오늘 출입 예정" value={stats?.todayVisits ?? 0} accent="blue" />
|
|
<StatCard label="현재 재실" value={stats?.currentlyInside ?? 0} accent="green" />
|
|
<StatCard label="승인 대기" value={stats?.pending ?? 0} accent="amber" />
|
|
<StatCard label="전체 신청" value={stats?.total ?? 0} accent="gray" />
|
|
</div>
|
|
|
|
<div className="card">
|
|
<h3>최근 출입 신청</h3>
|
|
{loading ? (
|
|
<p className="muted">불러오는 중…</p>
|
|
) : recent.length === 0 ? (
|
|
<p className="muted">신청 내역이 없습니다.</p>
|
|
) : (
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>방문자</th><th>회사</th><th>출입구역</th><th>출입 일시</th><th>상태</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{recent.map((r) => (
|
|
<tr key={r.id}>
|
|
<td>{r.visitorName}</td>
|
|
<td>{r.company || '-'}</td>
|
|
<td>{r.zoneName || '-'}</td>
|
|
<td>{formatDateTime(r.visitFrom)}</td>
|
|
<td>
|
|
{insideIds.has(r.id) ? (
|
|
<span className="badge badge-green">재실중</span>
|
|
) : (
|
|
<span className={`badge badge-${STATUS_CLASS[r.status]}`}>{STATUS_LABEL[r.status]}</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const StatCard: React.FC<{ label: string; value: number; accent: string }> = ({ label, value, accent }) => (
|
|
<div className={`stat-card accent-${accent}`}>
|
|
<div className="stat-value">{value}</div>
|
|
<div className="stat-label">{label}</div>
|
|
</div>
|
|
);
|