Files
forge/client/src/pages/Closed/index.tsx
T
Dmytro Tkachenko 28d817ebe9 Init
2026-08-29 11:59:28 +03:00

83 lines
3.3 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import PageHeader from '../../components/PageHeader';
import TicketTable from '../../components/TicketTable';
import TicketDetailModal from '../../components/TicketDetailModal';
import { getTickets } from '../../services/ticket.service';
import { formatCost, formatMinutes } from '../../utils/format.utils';
import type { Ticket } from '../../types/ticket.types';
import styles from './Closed.module.scss';
export default function Closed({ search, refreshKey }: { search: string; refreshKey: number }) {
const [tickets, setTickets] = useState<Ticket[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [selected, setSelected] = useState<Ticket | null>(null);
const [brandFilter, setBrandFilter] = useState('');
useEffect(() => {
let alive = true;
setLoading(true);
setError(false);
getTickets({ status: 'closed', q: search || undefined })
.then(t => { if (alive) setTickets(t); })
.catch(() => { if (alive) setError(true); })
.finally(() => { if (alive) setLoading(false); });
return () => { alive = false; };
}, [search, refreshKey]);
const brands = useMemo(
() => [...new Set(tickets.map(t => t.brand).filter((b): b is string => !!b))].sort(),
[tickets],
);
const filtered = useMemo(
() => tickets.filter(t => !brandFilter || t.brand === brandFilter),
[tickets, brandFilter],
);
const totalCost = useMemo(
() => filtered.reduce((sum, t) => sum + (t.finalCost ?? 0), 0),
[filtered],
);
const avgTtfr = useMemo(() => {
const vals = filtered.map(t => t.ttfrMinutes).filter((v): v is number => v != null);
return vals.length ? Math.round(vals.reduce((a, b) => a + b, 0) / vals.length) : null;
}, [filtered]);
return (
<div className={styles.page}>
<PageHeader title="Closed tickets" subtitle={`${filtered.length} fulfilled requests`}>
<select className={styles.select} value={brandFilter} onChange={e => setBrandFilter(e.target.value)}>
<option value="">All brands</option>
{brands.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</PageHeader>
{!loading && !error && filtered.length > 0 && (
<div className={styles.summary}>
<Stat label="Tickets" value={String(filtered.length)} />
<Stat label="Total cost" value={formatCost(totalCost)} />
<Stat label="Avg. time to first reply" value={formatMinutes(avgTtfr)} />
</div>
)}
{loading && <div className={styles.state}>Loading tickets</div>}
{error && <div className={styles.stateError}>Failed to load tickets. Is the server running?</div>}
{!loading && !error && filtered.length === 0 && <div className={styles.state}>No closed tickets found.</div>}
{!loading && !error && filtered.length > 0 && (
<TicketTable tickets={filtered} variant="closed" onSelect={setSelected} />
)}
{selected && <TicketDetailModal ticket={selected} onClose={() => setSelected(null)} />}
</div>
);
}
function Stat({ label, value }: { label: string; value: string }) {
return (
<div className={styles.stat}>
<div className={styles.statValue}>{value}</div>
<div className={styles.statLabel}>{label}</div>
</div>
);
}