Files
solution-erp/fe-admin/src/pages/office/ItTicketsPage.tsx
pqhuy1987 dcf76f8a9f
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m17s
[CLAUDE] Office: P11-D ItTicket auto-assign round-robin + SLA timer (Wave 2, Mig 46)
Mig 46 AddSlaFieldsToItTicket (SlaDueAt/SlaWarnedSent/SlaBreached). CreateItTicketHandler: round-robin least-loaded assign cho IT staff (dept Code=IT, tie-break Id) + SlaDueAt theo Priority (Urgent 4h/High 8h/Medium 24h/Low 72h). ItTicketSlaJob background (breach+warning notify, KHONG auto-transition). PUT /{id}/assign admin override. DbInitializer seed dept IT + 2 sample staff (nv.cao/nv.truong). FE ItTicketsPage +MaTicket+assignee+SLA badge (2 app SHA256 mirror). +9 test (191->200). Self-review PASS (seed<->query dept-code verified; em main solo review do session-limit kill reviewer-spawn).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:23:45 +07:00

93 lines
4.2 KiB
TypeScript

// Ticket CNTT — Phase 10.3 G-O6 (S38) + P11-D auto-assign round-robin + SLA timer (S52).
// Read-only kanban list + MaTicket + người xử lý (auto-assign dept IT) + SLA badge (đỏ khi quá hạn).
// File MIRROR SHA256 identical fe-user counterpart.
import { useQuery } from '@tanstack/react-query'
import { Ticket } from 'lucide-react'
import { PageHeader } from '@/components/PageHeader'
import { api } from '@/lib/api'
import { cn } from '@/lib/cn'
import {
IT_TICKET_CATEGORY_LABELS, IT_TICKET_PRIORITY_BADGE, IT_TICKET_PRIORITY_LABELS,
IT_TICKET_STATUS_LABELS, type ItTicketDto, type PagedResult,
} from '@/types/workflowApps'
function formatSlaDue(iso: string): string {
return new Date(iso).toLocaleString('vi-VN', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
})
}
export function ItTicketsPage() {
const list = useQuery({
queryKey: ['it-tickets'],
queryFn: async () => (await api.get<PagedResult<ItTicketDto>>('/it-tickets', { params: { pageSize: 100 } })).data,
})
const items = list.data?.items ?? []
// Group by status for kanban-ish display
const grouped: Record<number, ItTicketDto[]> = { 1: [], 2: [], 3: [], 4: [], 5: [] }
items.forEach((t) => {
if (grouped[t.status]) grouped[t.status].push(t)
})
return (
<div className="space-y-4">
<PageHeader title="Ticket CNTT" description="Helpdesk — báo lỗi và yêu cầu hỗ trợ kỹ thuật" />
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3">
{[1, 2, 3, 5, 4].map((statusKey) => (
<div key={statusKey} className="rounded-lg border bg-card p-3">
<h3 className="font-medium text-sm mb-2">
{IT_TICKET_STATUS_LABELS[statusKey]} <span className="text-xs text-muted-foreground">({grouped[statusKey].length})</span>
</h3>
<div className="space-y-2">
{list.isLoading && <div className="text-xs text-muted-foreground">Đang tải...</div>}
{!list.isLoading && grouped[statusKey].length === 0 && (
<div className="text-xs text-muted-foreground italic">Trống</div>
)}
{grouped[statusKey].map((t) => (
<div key={t.id} className="rounded border p-2 text-xs space-y-1 bg-background">
<div className="flex items-center justify-between">
<span className="font-mono text-[10px] text-muted-foreground">{t.maTicket ?? '—'}</span>
<span className={cn('rounded px-1.5 py-0.5 text-[10px]', IT_TICKET_PRIORITY_BADGE[t.priority])}>
{IT_TICKET_PRIORITY_LABELS[t.priority]}
</span>
</div>
<div className="font-medium truncate">{t.title}</div>
<div className="text-muted-foreground">
{IT_TICKET_CATEGORY_LABELS[t.category]} · {t.requesterFullName}
</div>
<div className="flex items-center justify-between gap-1 pt-0.5">
<span className="text-muted-foreground truncate" title={t.assignedToFullName ?? undefined}>
👤 {t.assignedToFullName ?? <span className="italic">Chưa giao</span>}
</span>
{t.slaDueAt && (
<span
className={cn(
'rounded px-1.5 py-0.5 text-[10px] whitespace-nowrap',
t.slaBreached ? 'bg-red-100 text-red-700 font-medium' : 'bg-slate-100 text-slate-600',
)}
title={`Hạn xử lý SLA: ${formatSlaDue(t.slaDueAt)}`}
>
{t.slaBreached ? 'Quá hạn SLA' : `SLA ${formatSlaDue(t.slaDueAt)}`}
</span>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
{!list.isLoading && items.length === 0 && (
<div className="rounded-lg border bg-card p-8 text-center text-muted-foreground">
<Ticket className="mx-auto h-10 w-10 mb-3 opacity-50" />
Chưa ticket nào.
</div>
)}
</div>
)
}