Compare commits
2 Commits
d261404903
...
7ee621b790
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ee621b790 | |||
| 44e9dc4ec8 |
@@ -55,6 +55,7 @@ export function useExtensionRequestDecision({
|
|||||||
queryKey: ticketKeys.timeline(ticketId),
|
queryKey: ticketKeys.timeline(ticketId),
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() });
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
},
|
},
|
||||||
onError: () => toast.error('Failed to review extension request'),
|
onError: () => toast.error('Failed to review extension request'),
|
||||||
|
|||||||
@@ -2,11 +2,48 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { CalendarClock, ClipboardCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import type { TicketListItem } from '@/types';
|
import type { TicketListItem } from '@/types';
|
||||||
|
|
||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
|
|
||||||
|
function TicketAttention({ ticket }: { ticket: TicketListItem }) {
|
||||||
|
const summary = ticket.attention_summary;
|
||||||
|
const reviewCount = summary?.pending_repair_review_count ?? 0;
|
||||||
|
const extensionCount = summary?.pending_extension_request_count ?? 0;
|
||||||
|
const showReview = reviewCount > 0;
|
||||||
|
const showExtension = extensionCount > 0;
|
||||||
|
|
||||||
|
if (!showReview && !showExtension) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
{showReview ? (
|
||||||
|
<Badge
|
||||||
|
className="gap-1.5 border-0 bg-orange-100 text-orange-700 shadow-none dark:bg-orange-950/60 dark:text-orange-300"
|
||||||
|
title={`${reviewCount} pending repair ${reviewCount === 1 ? 'review' : 'reviews'}`}
|
||||||
|
>
|
||||||
|
<ClipboardCheck aria-hidden="true" />
|
||||||
|
Review
|
||||||
|
<span className="font-semibold tabular-nums">{reviewCount}</span>
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{showExtension ? (
|
||||||
|
<Badge
|
||||||
|
className="gap-1.5 border-0 bg-blue-100 text-blue-700 shadow-none dark:bg-blue-950/60 dark:text-blue-300"
|
||||||
|
title={`${extensionCount} pending extension ${extensionCount === 1 ? 'request' : 'requests'}`}
|
||||||
|
>
|
||||||
|
<CalendarClock aria-hidden="true" />
|
||||||
|
Extension
|
||||||
|
<span className="font-semibold tabular-nums">+{extensionCount}</span>
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function useTicketColumns(): ColumnDef<TicketListItem>[] {
|
export function useTicketColumns(): ColumnDef<TicketListItem>[] {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -29,6 +66,13 @@ export function useTicketColumns(): ColumnDef<TicketListItem>[] {
|
|||||||
header: 'Detections',
|
header: 'Detections',
|
||||||
size: 120,
|
size: 120,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'attention',
|
||||||
|
header: 'Attention',
|
||||||
|
size: 210,
|
||||||
|
meta: { disableTruncate: true },
|
||||||
|
cell: ({ row }) => <TicketAttention ticket={row.original} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'created_by',
|
id: 'created_by',
|
||||||
header: 'Uploaded By',
|
header: 'Uploaded By',
|
||||||
|
|||||||
138
src/app/(modules)/ticket/components/TicketSummaryCards.tsx
Normal file
138
src/app/(modules)/ticket/components/TicketSummaryCards.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
CalendarClock,
|
||||||
|
CircleCheck,
|
||||||
|
ClipboardCheck,
|
||||||
|
ListChecks,
|
||||||
|
Ticket,
|
||||||
|
TriangleAlert,
|
||||||
|
type LucideIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import type { TicketSummaryResponse } from '@/types';
|
||||||
|
|
||||||
|
interface TicketSummaryCardsProps {
|
||||||
|
summary?: TicketSummaryResponse;
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryMetric {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
icon: LucideIcon;
|
||||||
|
iconClassName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryCard({ metric }: { metric: SummaryMetric }) {
|
||||||
|
const Icon = metric.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card size="sm" className="py-4">
|
||||||
|
<CardContent className="flex items-start justify-between gap-3 px-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-2xl font-semibold tracking-tight tabular-nums">
|
||||||
|
{metric.value.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 truncate text-xs font-medium text-muted-foreground">
|
||||||
|
{metric.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`rounded-lg p-2 ${metric.iconClassName}`}>
|
||||||
|
<Icon className="size-4" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TicketSummarySkeleton() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-5"
|
||||||
|
aria-label="Loading ticket summary"
|
||||||
|
>
|
||||||
|
{Array.from({ length: 5 }, (_, index) => (
|
||||||
|
<Card key={index} size="sm" className="py-4">
|
||||||
|
<CardContent className="flex items-start justify-between gap-3 px-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-7 w-14" />
|
||||||
|
<Skeleton className="h-3 w-24" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="size-8" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TicketSummaryCards({
|
||||||
|
summary,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
}: TicketSummaryCardsProps) {
|
||||||
|
if (isLoading) return <TicketSummarySkeleton />;
|
||||||
|
|
||||||
|
if (isError || !summary) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
className="flex items-center gap-2 rounded-lg border border-dashed px-4 py-3 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
<TriangleAlert className="size-4" aria-hidden="true" />
|
||||||
|
Ticket summary is temporarily unavailable.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics: SummaryMetric[] = [
|
||||||
|
{
|
||||||
|
label: 'Total Tickets',
|
||||||
|
value: summary.total_tickets,
|
||||||
|
icon: Ticket,
|
||||||
|
iconClassName:
|
||||||
|
'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'In Progress',
|
||||||
|
value: summary.in_progress_tickets,
|
||||||
|
icon: ListChecks,
|
||||||
|
iconClassName:
|
||||||
|
'bg-indigo-100 text-indigo-700 dark:bg-indigo-950/60 dark:text-indigo-300',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Repair Reviews',
|
||||||
|
value: summary.pending_actions.repair_reviews,
|
||||||
|
icon: ClipboardCheck,
|
||||||
|
iconClassName:
|
||||||
|
'bg-orange-100 text-orange-700 dark:bg-orange-950/60 dark:text-orange-300',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Extension Requests',
|
||||||
|
value: summary.pending_actions.extension_requests,
|
||||||
|
icon: CalendarClock,
|
||||||
|
iconClassName:
|
||||||
|
'bg-blue-100 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Completed',
|
||||||
|
value: summary.completed_tickets,
|
||||||
|
icon: CircleCheck,
|
||||||
|
iconClassName:
|
||||||
|
'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-label="Ticket summary"
|
||||||
|
className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-5"
|
||||||
|
>
|
||||||
|
{metrics.map((metric) => (
|
||||||
|
<SummaryCard key={metric.label} metric={metric} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -103,6 +103,12 @@ function buildTicketListItem(
|
|||||||
created_by_email: event.created_by_email ?? null,
|
created_by_email: event.created_by_email ?? null,
|
||||||
created_at: event.created_at ?? event.updated_at,
|
created_at: event.created_at ?? event.updated_at,
|
||||||
updated_at: event.updated_at,
|
updated_at: event.updated_at,
|
||||||
|
attention_summary: event.attention_summary ?? {
|
||||||
|
has_pending_extension_request: false,
|
||||||
|
pending_extension_request_count: 0,
|
||||||
|
has_pending_repair_review: false,
|
||||||
|
pending_repair_review_count: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +128,7 @@ function mergeTicketListItem(
|
|||||||
created_by_email: event.created_by_email ?? current.created_by_email,
|
created_by_email: event.created_by_email ?? current.created_by_email,
|
||||||
created_at: event.created_at ?? current.created_at,
|
created_at: event.created_at ?? current.created_at,
|
||||||
updated_at: event.updated_at ?? current.updated_at,
|
updated_at: event.updated_at ?? current.updated_at,
|
||||||
|
attention_summary: event.attention_summary ?? current.attention_summary,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +139,8 @@ export function useTenantTicketTableEvents(enabled = true) {
|
|||||||
const events = useMemo(
|
const events = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
ticket_status: (event: TicketTableStatusEvent) => {
|
ticket_status: (event: TicketTableStatusEvent) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() });
|
||||||
|
|
||||||
queryClient
|
queryClient
|
||||||
.getQueryCache()
|
.getQueryCache()
|
||||||
.findAll({ queryKey: ticketKeys.lists() })
|
.findAll({ queryKey: ticketKeys.lists() })
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ export function useTicketsQuery(params: UseTicketsQueryParams) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTicketSummaryQuery(chainageId?: string) {
|
||||||
|
const summaryParams = useMemo(
|
||||||
|
() => ({ chainage_id: chainageId || undefined }),
|
||||||
|
[chainageId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ticketKeys.summary(summaryParams),
|
||||||
|
queryFn: () => ticketService.getTicketSummary(summaryParams),
|
||||||
|
staleTime: TICKET_TABLE_REFRESH_MS,
|
||||||
|
refetchInterval: TICKET_TABLE_REFRESH_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useTicketOverviewQuery(ticketId: string | undefined) {
|
export function useTicketOverviewQuery(ticketId: string | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ticketKeys.overview(ticketId ?? ''),
|
queryKey: ticketKeys.overview(ticketId ?? ''),
|
||||||
@@ -214,6 +228,7 @@ export function useDiscardDetectionMutation(
|
|||||||
queryKey: ticketKeys.overview(ticketId),
|
queryKey: ticketKeys.overview(ticketId),
|
||||||
}),
|
}),
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }),
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.assignments(ticketId),
|
queryKey: ticketKeys.assignments(ticketId),
|
||||||
}),
|
}),
|
||||||
@@ -260,6 +275,7 @@ export function useReviewDetectionRepairProofMutation({
|
|||||||
queryKey: ticketKeys.timeline(ticketId),
|
queryKey: ticketKeys.timeline(ticketId),
|
||||||
}),
|
}),
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
onError: () => toast.error('Failed to review repair proof'),
|
onError: () => toast.error('Failed to review repair proof'),
|
||||||
@@ -305,6 +321,7 @@ export function useAssignTicketMutation(ticketId: string) {
|
|||||||
queryKey: ticketKeys.timeline(ticketId),
|
queryKey: ticketKeys.timeline(ticketId),
|
||||||
}),
|
}),
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() }),
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ticketKeys.assignmentDetectionLists(ticketId),
|
queryKey: ticketKeys.assignmentDetectionLists(ticketId),
|
||||||
}),
|
}),
|
||||||
@@ -349,6 +366,7 @@ export function useRequestTicketExtensionMutation(
|
|||||||
queryKey: ticketKeys.timeline(ticketId),
|
queryKey: ticketKeys.timeline(ticketId),
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() });
|
||||||
},
|
},
|
||||||
onError: () => toast.error('Failed to request an extension'),
|
onError: () => toast.error('Failed to request an extension'),
|
||||||
});
|
});
|
||||||
@@ -371,6 +389,7 @@ export function useCloseTicketMutation(ticketId: string) {
|
|||||||
queryKey: ticketKeys.timeline(ticketId),
|
queryKey: ticketKeys.timeline(ticketId),
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: ticketKeys.lists() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ticketKeys.summaries() });
|
||||||
},
|
},
|
||||||
onError: () => toast.error('Failed to close ticket'),
|
onError: () => toast.error('Failed to close ticket'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ import type { TicketListItem } from '@/types';
|
|||||||
import { ROUTES } from '@/utils/routes';
|
import { ROUTES } from '@/utils/routes';
|
||||||
|
|
||||||
import { TicketFilters } from './components/TicketFilters';
|
import { TicketFilters } from './components/TicketFilters';
|
||||||
|
import { TicketSummaryCards } from './components/TicketSummaryCards';
|
||||||
import { TicketTable } from './components/TicketTable';
|
import { TicketTable } from './components/TicketTable';
|
||||||
import { useTicketColumns } from './components/TicketColumns';
|
import { useTicketColumns } from './components/TicketColumns';
|
||||||
import { useTenantTicketTableEvents } from './hooks/useTenantTicketTableEvents';
|
import { useTenantTicketTableEvents } from './hooks/useTenantTicketTableEvents';
|
||||||
import { useTicketsQuery } from './hooks/useTicketQueries';
|
import {
|
||||||
|
useTicketsQuery,
|
||||||
|
useTicketSummaryQuery,
|
||||||
|
} from './hooks/useTicketQueries';
|
||||||
|
|
||||||
export default function TicketPage() {
|
export default function TicketPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -26,6 +30,7 @@ export default function TicketPage() {
|
|||||||
limit,
|
limit,
|
||||||
chainageId: segmentId,
|
chainageId: segmentId,
|
||||||
});
|
});
|
||||||
|
const summaryQuery = useTicketSummaryQuery(segmentId);
|
||||||
|
|
||||||
const columns = useTicketColumns();
|
const columns = useTicketColumns();
|
||||||
const tickets = ticketsQuery.data?.items ?? [];
|
const tickets = ticketsQuery.data?.items ?? [];
|
||||||
@@ -62,6 +67,12 @@ export default function TicketPage() {
|
|||||||
icon={Ticket}
|
icon={Ticket}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TicketSummaryCards
|
||||||
|
summary={summaryQuery.data}
|
||||||
|
isLoading={summaryQuery.isLoading}
|
||||||
|
isError={summaryQuery.isError}
|
||||||
|
/>
|
||||||
|
|
||||||
<TicketTable
|
<TicketTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
tickets={tickets}
|
tickets={tickets}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export const ticketKeys = {
|
|||||||
all: ['tickets'] as const,
|
all: ['tickets'] as const,
|
||||||
lists: () => [...ticketKeys.all, 'list'] as const,
|
lists: () => [...ticketKeys.all, 'list'] as const,
|
||||||
list: (params: TicketListParams) => [...ticketKeys.lists(), params] as const,
|
list: (params: TicketListParams) => [...ticketKeys.lists(), params] as const,
|
||||||
|
summaries: () => [...ticketKeys.all, 'summary'] as const,
|
||||||
|
summary: (params: { chainage_id?: string }) =>
|
||||||
|
[...ticketKeys.summaries(), params] as const,
|
||||||
details: () => [...ticketKeys.all, 'detail'] as const,
|
details: () => [...ticketKeys.all, 'detail'] as const,
|
||||||
overview: (ticketId: string) =>
|
overview: (ticketId: string) =>
|
||||||
[...ticketKeys.details(), ticketId, 'overview'] as const,
|
[...ticketKeys.details(), ticketId, 'overview'] as const,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.5rem;
|
--radius: 0.1rem;
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const API_ROUTES = {
|
|||||||
},
|
},
|
||||||
TICKETS: {
|
TICKETS: {
|
||||||
BASE: '/biz/api/v1/tickets',
|
BASE: '/biz/api/v1/tickets',
|
||||||
|
SUMMARY: '/biz/api/v1/tickets/summary',
|
||||||
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
|
DETAIL: (id: string) => `/biz/api/v1/tickets/${id}`,
|
||||||
ASSIGNMENT_DETECTIONS: (id: string) =>
|
ASSIGNMENT_DETECTIONS: (id: string) =>
|
||||||
`/biz/api/v1/tickets/${id}/assignment-detections`,
|
`/biz/api/v1/tickets/${id}/assignment-detections`,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import type {
|
|||||||
TicketOverviewDetail,
|
TicketOverviewDetail,
|
||||||
TicketListParams,
|
TicketListParams,
|
||||||
TicketListResponse,
|
TicketListResponse,
|
||||||
|
TicketSummaryParams,
|
||||||
|
TicketSummaryResponse,
|
||||||
TicketTimelineResponse,
|
TicketTimelineResponse,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
|
||||||
@@ -45,6 +47,20 @@ export const ticketService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTicketSummary: async (
|
||||||
|
params?: TicketSummaryParams,
|
||||||
|
): Promise<TicketSummaryResponse> => {
|
||||||
|
const response = await axiosClient.get<TicketSummaryResponse>(
|
||||||
|
API_ROUTES.TICKETS.SUMMARY,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
chainage_id: params?.chainage_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
getTicketOverview: async (
|
getTicketOverview: async (
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
): Promise<TicketOverviewDetail> => {
|
): Promise<TicketOverviewDetail> => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { TicketStatus } from './status';
|
import type { TicketStatus } from './status';
|
||||||
|
import type { TicketAttentionSummary } from './list';
|
||||||
|
|
||||||
export interface SseTokenResponse {
|
export interface SseTokenResponse {
|
||||||
sse_token: string;
|
sse_token: string;
|
||||||
@@ -18,6 +19,7 @@ export type TicketTableStatusEvent = {
|
|||||||
created_by_email?: string | null;
|
created_by_email?: string | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
attention_summary?: TicketAttentionSummary;
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from './detail';
|
|||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './timeline';
|
export * from './timeline';
|
||||||
|
export * from './summary';
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ export interface TicketListParams extends PaginationParams {
|
|||||||
chainage_id?: string;
|
chainage_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TicketAttentionSummary {
|
||||||
|
has_pending_extension_request: boolean;
|
||||||
|
pending_extension_request_count: number;
|
||||||
|
has_pending_repair_review: boolean;
|
||||||
|
pending_repair_review_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TicketListItem {
|
export interface TicketListItem {
|
||||||
id: string;
|
id: string;
|
||||||
ticket_name: string;
|
ticket_name: string;
|
||||||
@@ -15,6 +22,7 @@ export interface TicketListItem {
|
|||||||
created_by_email: string | null;
|
created_by_email: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
attention_summary: TicketAttentionSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketListResponse {
|
export interface TicketListResponse {
|
||||||
|
|||||||
13
src/types/ticket/summary.ts
Normal file
13
src/types/ticket/summary.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface TicketSummaryParams {
|
||||||
|
chainage_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TicketSummaryResponse {
|
||||||
|
total_tickets: number;
|
||||||
|
in_progress_tickets: number;
|
||||||
|
pending_actions: {
|
||||||
|
extension_requests: number;
|
||||||
|
repair_reviews: number;
|
||||||
|
};
|
||||||
|
completed_tickets: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user