154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useMemo, useState } from 'react';
|
|
import { AlertCircle, CalendarClock, Check, RotateCcw, X } from 'lucide-react';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
import type { DataTableAction } from '@/components/data-table';
|
|
import { PageHeader } from '@/components/page-header';
|
|
import { Button } from '@/components/ui/button';
|
|
import { PERMISSIONS } from '@/constants/permissions';
|
|
import type { TicketExtensionRequest } from '@/types';
|
|
import { ROUTES } from '@/utils/routes';
|
|
|
|
import { useExtensionRequestColumns } from './components/ExtensionRequestColumns';
|
|
import { ExtensionRequestDecisionDialog } from './components/ExtensionRequestDecisionDialog';
|
|
import { ExtensionRequestFilters } from './components/ExtensionRequestFilters';
|
|
import { ExtensionRequestTable } from './components/ExtensionRequestTable';
|
|
import type { ExtensionRequestDecisionAction } from './hooks/useExtensionRequestDecision';
|
|
import { useExtensionRequestFilters } from './hooks/useExtensionRequestFilters';
|
|
import { useExtensionRequestsQuery } from './hooks/useExtensionRequestQueries';
|
|
|
|
export default function ExtensionRequestsPage() {
|
|
const router = useRouter();
|
|
const [decision, setDecision] = useState<{
|
|
request: TicketExtensionRequest;
|
|
action: ExtensionRequestDecisionAction;
|
|
} | null>(null);
|
|
const {
|
|
skip,
|
|
setSkip,
|
|
limit,
|
|
setLimit,
|
|
ticketName,
|
|
debouncedTicketName,
|
|
setTicketName,
|
|
statusFilter,
|
|
setStatusFilter,
|
|
clearFilters,
|
|
} = useExtensionRequestFilters();
|
|
const extensionRequestsQuery = useExtensionRequestsQuery({
|
|
skip,
|
|
limit,
|
|
ticketName: debouncedTicketName,
|
|
statusFilter,
|
|
});
|
|
const columns = useExtensionRequestColumns();
|
|
const requests = extensionRequestsQuery.data?.items ?? [];
|
|
const total = extensionRequestsQuery.data?.total ?? 0;
|
|
|
|
const actions = useMemo<DataTableAction<TicketExtensionRequest>[]>(
|
|
() => [
|
|
{
|
|
label: 'Approve extension',
|
|
icon: <Check className="text-emerald-600" />,
|
|
permission: PERMISSIONS.TICKET.ASSIGN,
|
|
hidden: (request) => request.status !== 'pending',
|
|
className:
|
|
'border-emerald-200 hover:bg-emerald-50 dark:border-emerald-900 dark:hover:bg-emerald-950/50',
|
|
onClick: (request) => setDecision({ request, action: 'approve' }),
|
|
},
|
|
{
|
|
label: 'Reject extension',
|
|
icon: <X className="text-destructive" />,
|
|
permission: PERMISSIONS.TICKET.ASSIGN,
|
|
hidden: (request) => request.status !== 'pending',
|
|
className:
|
|
'border-destructive/30 hover:bg-destructive/5 hover:text-destructive',
|
|
onClick: (request) => setDecision({ request, action: 'reject' }),
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
const openTicket = useCallback(
|
|
(request: TicketExtensionRequest) => {
|
|
router.push(ROUTES.TICKET_DETAIL(request.ticket_id));
|
|
},
|
|
[router],
|
|
);
|
|
|
|
const filterToolbar = useMemo(
|
|
() => (
|
|
<ExtensionRequestFilters
|
|
ticketName={ticketName}
|
|
statusFilter={statusFilter}
|
|
onTicketNameChange={setTicketName}
|
|
onStatusChange={setStatusFilter}
|
|
onClear={clearFilters}
|
|
/>
|
|
),
|
|
[clearFilters, setTicketName, setStatusFilter, statusFilter, ticketName],
|
|
);
|
|
|
|
return (
|
|
<main className="relative z-10 space-y-5">
|
|
<PageHeader
|
|
title="Extension Requests"
|
|
description="Track deadline extension requests across all ticket lots."
|
|
icon={CalendarClock}
|
|
/>
|
|
|
|
{extensionRequestsQuery.isError ? (
|
|
<div
|
|
role="alert"
|
|
className="flex min-h-48 flex-col items-center justify-center rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 text-center"
|
|
>
|
|
<AlertCircle className="mb-2 size-6 text-destructive" />
|
|
<p className="text-sm font-medium text-destructive">
|
|
Failed to load extension requests.
|
|
</p>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="mt-3"
|
|
disabled={extensionRequestsQuery.isFetching}
|
|
onClick={() => extensionRequestsQuery.refetch()}
|
|
>
|
|
<RotateCcw
|
|
className={
|
|
extensionRequestsQuery.isFetching ? 'animate-spin' : undefined
|
|
}
|
|
/>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<ExtensionRequestTable
|
|
columns={columns}
|
|
requests={requests}
|
|
isLoading={extensionRequestsQuery.isLoading}
|
|
toolbar={filterToolbar}
|
|
skip={skip}
|
|
limit={limit}
|
|
total={total}
|
|
onPageChange={setSkip}
|
|
onLimitChange={setLimit}
|
|
onView={openTicket}
|
|
actions={actions}
|
|
/>
|
|
)}
|
|
|
|
<ExtensionRequestDecisionDialog
|
|
request={decision?.request ?? null}
|
|
action={decision?.action ?? null}
|
|
open={Boolean(decision)}
|
|
onOpenChange={(open) => {
|
|
if (!open) setDecision(null);
|
|
}}
|
|
/>
|
|
</main>
|
|
);
|
|
}
|