feat: add Node ACS admin workflows
This commit is contained in:
162
frontend/src/components/VisitRequestDetailDialog.tsx
Normal file
162
frontend/src/components/VisitRequestDetailDialog.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getVisitRequest, listTodayAccess } from '../api';
|
||||
import { AccessRecord, VisitRequestView } from '../types';
|
||||
import { STATUS_CLASS, STATUS_LABEL, formatDateTime } from '../status';
|
||||
|
||||
interface Props {
|
||||
requestId: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const VisitRequestDetailDialog: React.FC<Props> = ({ requestId, onClose }) => {
|
||||
const [request, setRequest] = useState<VisitRequestView | null>(null);
|
||||
const [access, setAccess] = useState<AccessRecord | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
Promise.all([
|
||||
getVisitRequest(requestId),
|
||||
listTodayAccess().catch(() => [] as AccessRecord[]),
|
||||
])
|
||||
.then(([detail, records]) => {
|
||||
if (!alive) return;
|
||||
setRequest(detail);
|
||||
setAccess(records.find((r) => r.visitRequestId === requestId) ?? null);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!alive) return;
|
||||
setError(e instanceof Error ? e.message : '상세 조회에 실패했습니다.');
|
||||
})
|
||||
.finally(() => {
|
||||
if (alive) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [requestId]);
|
||||
|
||||
const status = getDisplayStatus(request, access);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-box detail-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="detail-head">
|
||||
<div>
|
||||
<h3 className="modal-title">출입 신청 상세</h3>
|
||||
{request && <p className="modal-message">{request.visitorName} / {request.zoneName || '-'}</p>}
|
||||
</div>
|
||||
<button className="btn-ghost" onClick={onClose}>닫기</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="muted">불러오는 중...</p>
|
||||
) : error ? (
|
||||
<div className="alert alert-error">{error}</div>
|
||||
) : request ? (
|
||||
<>
|
||||
<div className="detail-status">
|
||||
<span className={`badge badge-${status.className}`}>{status.label}</span>
|
||||
</div>
|
||||
<div className="form-grid visit-form detail-form">
|
||||
<fieldset className="form-group span-2">
|
||||
<legend>방문자</legend>
|
||||
<div className="group-grid">
|
||||
<ReadOnlyField label="방문자 이름" value={request.visitorName} />
|
||||
<ReadOnlyField label="회사/소속" value={request.company} />
|
||||
<ReadOnlyField label="연락처" value={request.contact} />
|
||||
<ReadOnlyField label="이메일" value={request.email} />
|
||||
|
||||
<div className="field">
|
||||
<span>출입 전산실</span>
|
||||
<div className="checkbox-row">
|
||||
{['4층전산실', '5층전산실'].map((room) => (
|
||||
<label key={room} className="checkbox-inline">
|
||||
<input type="checkbox" checked={request.zoneName?.includes(room) ?? false} readOnly />
|
||||
<span>{room}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReadOnlyField label="추가 구역" value={extraZone(request.zoneName)} />
|
||||
<ReadOnlyField label="차량번호" value={request.vehicleNo} />
|
||||
<ReadOnlyField label="출입 목적" value={request.purpose} />
|
||||
<ReadOnlyField label="작업명" value={request.workName} />
|
||||
<ReadOnlyField label="출입 일시" value={formatDateTime(request.visitFrom)} />
|
||||
<ReadOnlyField label="퇴실 예정일시" value={formatDateTime(request.visitTo)} />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="form-group span-2">
|
||||
<legend>출입통제담당자</legend>
|
||||
<div className="group-grid">
|
||||
<ReadOnlyField label="이름" value={request.controlName || request.hostName} />
|
||||
<ReadOnlyField label="담당팀" value={request.controlTeam || request.hostDepartment} />
|
||||
<ReadOnlyField label="연락처" value={request.controlContact} />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="form-group span-2">
|
||||
<legend>현장감시자</legend>
|
||||
<div className="subsection-title">현장감시자1 <em className="hint-inline">: IT센터 사무보조원</em></div>
|
||||
<div className="group-grid">
|
||||
<ReadOnlyField label="이름" value={request.watcher1Name} />
|
||||
<ReadOnlyField label="소속" value={request.watcher1Team} />
|
||||
<ReadOnlyField label="연락처" value={request.watcher1Contact} />
|
||||
</div>
|
||||
|
||||
<div className="subsection-title">현장감시자2 <em className="hint-inline">: 작업을 입회할 상주직원</em></div>
|
||||
<div className="group-grid">
|
||||
<ReadOnlyField label="이름" value={request.watcher2Name} />
|
||||
<ReadOnlyField label="소속" value={request.watcher2Team} />
|
||||
<ReadOnlyField label="연락처" value={request.watcher2Contact} />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="form-group span-2">
|
||||
<legend>출입 처리 현황</legend>
|
||||
<div className="group-grid">
|
||||
<ReadOnlyField label="상태" value={status.label} />
|
||||
<ReadOnlyField label="실제 입장" value={formatDateTime(access?.checkInAt)} />
|
||||
<ReadOnlyField label="실제 퇴장" value={formatDateTime(access?.checkOutAt)} />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReadOnlyField: React.FC<{ label: string; value?: string | null }> = ({ label, value }) => (
|
||||
<label className="field">
|
||||
<span>{label}</span>
|
||||
<input value={value || '-'} readOnly />
|
||||
</label>
|
||||
);
|
||||
|
||||
function extraZone(zoneName?: string): string {
|
||||
if (!zoneName) return '-';
|
||||
const parts = zoneName.split('/').map((part) => part.trim()).filter(Boolean);
|
||||
if (parts.length > 1) return parts.slice(1).join(' / ');
|
||||
if (zoneName.includes('전산실')) return '-';
|
||||
return zoneName;
|
||||
}
|
||||
|
||||
function getDisplayStatus(request: VisitRequestView | null, access: AccessRecord | null) {
|
||||
if (access?.inside) {
|
||||
return { label: '재실중', className: 'green' };
|
||||
}
|
||||
if (access?.checkOutAt) {
|
||||
return { label: '퇴장', className: 'gray' };
|
||||
}
|
||||
if (!request) {
|
||||
return { label: '-', className: 'gray' };
|
||||
}
|
||||
return { label: STATUS_LABEL[request.status], className: STATUS_CLASS[request.status] };
|
||||
}
|
||||
Reference in New Issue
Block a user