feat(ticket): add global ticket assignment page

This commit is contained in:
2026-07-27 18:31:12 +05:30
parent 224361f144
commit 20f611f35a
13 changed files with 540 additions and 126 deletions

View File

@@ -4,12 +4,18 @@ import { ArrowLeft } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { PERMISSIONS } from '@/constants/permissions';
import { PermissionGuard } from '@/guards';
import type { TicketOverviewDetail } from '@/types';
import { AssignTicketAction } from './actions/AssignTicketAction';
export function TicketDetailHeader({
ticket,
canAssign = false,
}: {
ticket: TicketOverviewDetail;
canAssign?: boolean;
}) {
const router = useRouter();
const ticketLabel = ticket.ticket_name || ticket.id;
@@ -22,10 +28,12 @@ export function TicketDetailHeader({
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="secondary"
onClick={() => router.push('/ticket')}
>
{canAssign ? (
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
<AssignTicketAction ticketId={ticket.id} />
</PermissionGuard>
) : null}
<Button variant="secondary" onClick={() => router.push('/ticket')}>
<ArrowLeft className="size-4" />
Back to List
</Button>

View File

@@ -5,7 +5,6 @@ import { PermissionGuard } from '@/guards';
import type { TicketDetail } from '@/types';
import { AssignedContractorAction } from './actions/AssignedContractorAction';
import { AssignTicketAction } from './actions/AssignTicketAction';
import { ClosedTicketAction } from './actions/ClosedTicketAction';
import { NoTicketAction } from './actions/NoTicketAction';
import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction';
@@ -18,14 +17,7 @@ interface TicketStatusActionsProps {
function getTicketAction(ticket: TicketDetail) {
if (ticket.assignment_status === 'unassigned') {
return (
<PermissionGuard
permissions={PERMISSIONS.TICKET.ASSIGN}
fallback={<NoTicketAction />}
>
<AssignTicketAction ticket={ticket} />
</PermissionGuard>
);
return null;
}
if (ticket.assignment_status === 'assigned') {
@@ -71,5 +63,7 @@ function getTicketAction(ticket: TicketDetail) {
}
export function TicketStatusActions({ ticket }: TicketStatusActionsProps) {
return <div className="space-y-3">{getTicketAction(ticket)}</div>;
const action = getTicketAction(ticket);
return action ? <div className="space-y-3">{action}</div> : null;
}

View File

@@ -1,111 +1,21 @@
'use client';
import { useMemo, useState } from 'react';
import { Users, Mail, Phone, Send, ShieldCheck } from 'lucide-react';
import { Users } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { DatePickerSimple } from '@/components/form/DatePickerSimple';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { AssignableWorkerSelect } from '@/components/lookups/AssignableWorkerSelect';
import type { AssignableTicketUser, TicketDetail } from '@/types';
import { ROUTES } from '@/utils/routes';
import { useAssignTicketMutation } from '../../../hooks/useTicketQueries';
import { TicketActionCard } from './TicketActionCard';
export function AssignTicketAction({ ticket }: { ticket: TicketDetail }) {
const [selectedUserId, setSelectedUserId] = useState('');
const [selectedContractor, setSelectedContractor] =
useState<AssignableTicketUser | null>(null);
const [dueDate, setDueDate] = useState<Date>();
const [assignNote, setAssignNote] = useState('');
const assignMutation = useAssignTicketMutation(ticket.id);
const today = useMemo(() => {
const date = new Date();
date.setHours(0, 0, 0, 0);
return date;
}, []);
const maxDueMonth = useMemo(
() => new Date(today.getFullYear() + 20, 11),
[today],
);
export function AssignTicketAction({ ticketId }: { ticketId: string }) {
const router = useRouter();
return (
<TicketActionCard icon={Users} title="Assign to Contractor">
<div className="space-y-4">
<div className="space-y-2">
<Label>Contractor *</Label>
<AssignableWorkerSelect
value={selectedUserId}
onValueChange={(userId, user) => {
setSelectedUserId(userId);
setSelectedContractor(user);
}}
disabled={assignMutation.isPending}
/>
</div>
{selectedContractor ? (
<div className="space-y-2 rounded-lg border bg-muted/30 p-3">
<p className="text-sm font-medium">Contractor Details</p>
{selectedContractor.phone_number ? (
<p className="flex items-center gap-2 text-sm text-muted-foreground">
<Phone className="size-4 shrink-0" />
{selectedContractor.phone_number}
</p>
) : null}
<p className="flex items-center gap-2 text-sm text-muted-foreground">
<Mail className="size-4 shrink-0" />
<span className="truncate">{selectedContractor.email}</span>
</p>
<p className="flex items-center gap-2 text-sm text-muted-foreground">
<ShieldCheck className="size-4 shrink-0" />
{selectedContractor.role.name}
</p>
</div>
) : null}
<div className="space-y-2">
<Label htmlFor="assign-due-date">
Due Date <span className="text-destructive">*</span>
</Label>
<DatePickerSimple
id="assign-due-date"
value={dueDate}
onChange={setDueDate}
showLabel={false}
placeholder="Select due date"
disabled={assignMutation.isPending}
className="w-full"
startMonth={today}
endMonth={maxDueMonth}
disabledDates={{ before: today }}
/>
</div>
<div className="space-y-2">
<Label>Assignment Note (Optional)</Label>
<Textarea
value={assignNote}
onChange={(event) => setAssignNote(event.target.value)}
placeholder="Add any instructions or notes for the contractor..."
/>
</div>
<Button
disabled={!selectedUserId || !dueDate || assignMutation.isPending}
onClick={() => {
if (!selectedUserId || !dueDate) return;
assignMutation.mutate({
assigned_to_user_id: Number(selectedUserId),
defect_class: ticket.defect_class,
due_at: dueDate.toISOString(),
note: assignNote || undefined,
});
}}
className="w-full"
>
<Send />
Assign Ticket
</Button>
</div>
</TicketActionCard>
<Button
type="button"
onClick={() => router.push(ROUTES.TICKET_ASSIGN(ticketId))}
>
<Users />
Assign Ticket
</Button>
);
}