All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m17s
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>
93 lines
4.2 KiB
TypeScript
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 có ticket nào.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|