fix: correct access dashboard status counts
This commit is contained in:
@@ -6,9 +6,26 @@ import {
|
|||||||
searchApprovedForCheckIn,
|
searchApprovedForCheckIn,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { AccessRecord, VisitRequestView } from '../types';
|
import { AccessRecord, VisitRequestView } from '../types';
|
||||||
import { formatShort } from '../status';
|
import { STATUS_CLASS, STATUS_LABEL, formatShort } from '../status';
|
||||||
import { VisitRequestDetailDialog } from '../components/VisitRequestDetailDialog';
|
import { VisitRequestDetailDialog } from '../components/VisitRequestDetailDialog';
|
||||||
|
|
||||||
|
const hasEnteredToday = (r: AccessRecord) => Boolean(r.checkInAt);
|
||||||
|
const hasExitedToday = (r: AccessRecord) => Boolean(r.checkOutAt);
|
||||||
|
|
||||||
|
function accessBadge(record: AccessRecord): { className: string; label: string } {
|
||||||
|
if (record.inside) {
|
||||||
|
return { className: 'green', label: '재실중' };
|
||||||
|
}
|
||||||
|
if (hasExitedToday(record)) {
|
||||||
|
return { className: 'gray', label: '퇴실' };
|
||||||
|
}
|
||||||
|
const status = record.status ?? 'APPROVED';
|
||||||
|
if (status === 'APPROVED') {
|
||||||
|
return { className: 'amber', label: '입장대기' };
|
||||||
|
}
|
||||||
|
return { className: STATUS_CLASS[status], label: STATUS_LABEL[status] };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Staff access console (login: 담당자/보안/관리자). Name search → force check-in/out,
|
* Staff access console (login: 담당자/보안/관리자). Name search → force check-in/out,
|
||||||
* plus today's full access log (신청 입/퇴장 + 실제 입/퇴장, 퇴실자 포함).
|
* plus today's full access log (신청 입/퇴장 + 실제 입/퇴장, 퇴실자 포함).
|
||||||
@@ -36,7 +53,7 @@ export const AccessConsolePage: React.FC = () => {
|
|||||||
|
|
||||||
const insideIds = new Set(records.filter((r) => r.inside).map((r) => r.visitRequestId));
|
const insideIds = new Set(records.filter((r) => r.inside).map((r) => r.visitRequestId));
|
||||||
// Entered and already left today → no re-entry allowed.
|
// Entered and already left today → no re-entry allowed.
|
||||||
const exitedIds = new Set(records.filter((r) => !r.inside).map((r) => r.visitRequestId));
|
const exitedIds = new Set(records.filter(hasExitedToday).map((r) => r.visitRequestId));
|
||||||
|
|
||||||
const wrap = async (fn: () => Promise<void>) => {
|
const wrap = async (fn: () => Promise<void>) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -184,12 +201,13 @@ export const AccessConsolePage: React.FC = () => {
|
|||||||
<td>{formatShort(r.checkInAt)}</td>
|
<td>{formatShort(r.checkInAt)}</td>
|
||||||
<td>{formatShort(r.checkOutAt)}</td>
|
<td>{formatShort(r.checkOutAt)}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`badge badge-${r.inside ? 'green' : 'gray'}`}>
|
{(() => {
|
||||||
{r.inside ? '재실중' : '퇴실'}
|
const badge = accessBadge(r);
|
||||||
</span>
|
return <span className={`badge badge-${badge.className}`}>{badge.label}</span>;
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{r.inside && (
|
{r.inside && hasEnteredToday(r) && (
|
||||||
<button
|
<button
|
||||||
className="btn-danger"
|
className="btn-danger"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ export interface AccessRecord {
|
|||||||
zoneName?: string;
|
zoneName?: string;
|
||||||
visitFrom: string;
|
visitFrom: string;
|
||||||
visitTo: string;
|
visitTo: string;
|
||||||
|
status?: VisitStatus;
|
||||||
checkInAt?: string;
|
checkInAt?: string;
|
||||||
checkOutAt?: string;
|
checkOutAt?: string;
|
||||||
inside: boolean;
|
inside: boolean;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ interface AccessRow {
|
|||||||
host_name: string;
|
host_name: string;
|
||||||
visit_from: string | Date;
|
visit_from: string | Date;
|
||||||
visit_to: string | Date;
|
visit_to: string | Date;
|
||||||
|
status: string;
|
||||||
check_in_at: string | Date | null;
|
check_in_at: string | Date | null;
|
||||||
check_out_at: string | Date | null;
|
check_out_at: string | Date | null;
|
||||||
}
|
}
|
||||||
@@ -745,10 +746,13 @@ export function createBusinessRouter(deps: BusinessRouterDeps = { query }): Rout
|
|||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
vr.id AS visit_request_id, v.name AS visitor_name, v.company, vr.zone_name, h.full_name AS host_name,
|
vr.id AS visit_request_id, v.name AS visitor_name, v.company, vr.zone_name, h.full_name AS host_name,
|
||||||
vr.visit_from, vr.visit_to,
|
vr.visit_from, vr.visit_to, vr.status,
|
||||||
min(ae.event_at) FILTER (WHERE ae.direction = 'IN') AS check_in_at,
|
min(ae.event_at) FILTER (WHERE ae.direction = 'IN') AS check_in_at,
|
||||||
max(ae.event_at) FILTER (WHERE ae.direction = 'OUT') AS check_out_at,
|
max(ae.event_at) FILTER (WHERE ae.direction = 'OUT') AS check_out_at,
|
||||||
(max(ae.event_at) FILTER (WHERE ae.direction = 'IN')) > COALESCE(max(ae.event_at) FILTER (WHERE ae.direction = 'OUT'), 'epoch') AS inside
|
COALESCE(
|
||||||
|
(max(ae.event_at) FILTER (WHERE ae.direction = 'IN')) > COALESCE(max(ae.event_at) FILTER (WHERE ae.direction = 'OUT'), 'epoch'),
|
||||||
|
false
|
||||||
|
) AS inside
|
||||||
FROM visit_requests vr
|
FROM visit_requests vr
|
||||||
JOIN visitors v ON v.id = vr.visitor_id
|
JOIN visitors v ON v.id = vr.visitor_id
|
||||||
JOIN users h ON h.id = vr.host_id
|
JOIN users h ON h.id = vr.host_id
|
||||||
@@ -765,9 +769,10 @@ export function createBusinessRouter(deps: BusinessRouterDeps = { query }): Rout
|
|||||||
zoneName: row.zone_name ?? undefined,
|
zoneName: row.zone_name ?? undefined,
|
||||||
visitFrom: toIso(row.visit_from)!,
|
visitFrom: toIso(row.visit_from)!,
|
||||||
visitTo: toIso(row.visit_to)!,
|
visitTo: toIso(row.visit_to)!,
|
||||||
|
status: row.status,
|
||||||
checkInAt: toIso(row.check_in_at),
|
checkInAt: toIso(row.check_in_at),
|
||||||
checkOutAt: toIso(row.check_out_at),
|
checkOutAt: toIso(row.check_out_at),
|
||||||
inside: row.inside,
|
inside: Boolean(row.inside),
|
||||||
})));
|
})));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -776,8 +781,14 @@ export function createBusinessRouter(deps: BusinessRouterDeps = { query }): Rout
|
|||||||
const result = await dbQuery<{ today_visits: string; pending: string; approved: string; total: string }>(
|
const result = await dbQuery<{ today_visits: string; pending: string; approved: string; total: string }>(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
count(*) FILTER (WHERE visit_from::date = now()::date) AS today_visits,
|
count(*) FILTER (
|
||||||
count(*) FILTER (WHERE status = 'PENDING') AS pending,
|
WHERE visit_from::date = now()::date
|
||||||
|
AND status IN ('PENDING', 'APPROVED')
|
||||||
|
) AS today_visits,
|
||||||
|
count(*) FILTER (
|
||||||
|
WHERE status = 'PENDING'
|
||||||
|
AND visit_to >= now()
|
||||||
|
) AS pending,
|
||||||
count(*) FILTER (WHERE status = 'APPROVED') AS approved,
|
count(*) FILTER (WHERE status = 'APPROVED') AS approved,
|
||||||
count(*) AS total
|
count(*) AS total
|
||||||
FROM visit_requests
|
FROM visit_requests
|
||||||
|
|||||||
Reference in New Issue
Block a user