feat(ticket): add due date picker and contractor details to assign flow

This commit is contained in:
2026-07-03 16:07:08 +05:30
parent b6a0f04f1e
commit 26e15360f7
12 changed files with 723 additions and 64 deletions

View File

@@ -1,56 +1,93 @@
'use client';
import { useState } from 'react';
import { Send, UserPlus } from 'lucide-react';
import { Users, Mail, Phone, Send, ShieldCheck } from 'lucide-react';
import { DatePicker } from '@/components/form/DatePicker';
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 { TicketDetail } from '@/types';
import type { AssignableTicketUser, TicketDetail } from '@/types';
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);
return (
<TicketActionCard icon={UserPlus} title="Assign Ticket">
<TicketActionCard icon={Users} title="Assign to Contractor">
<div className="space-y-4">
<div className="space-y-2">
<Label>Contractor</Label>
<Label>Contractor *</Label>
<AssignableWorkerSelect
value={selectedUserId}
onValueChange={setSelectedUserId}
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>Note</Label>
<Label>Due Date *</Label>
<DatePicker
value={dueDate}
onChange={setDueDate}
showTimeZone={false}
disabled={assignMutation.isPending}
className="w-full"
/>
</div>
<div className="space-y-2">
<Label>Assignment Note (Optional)</Label>
<Textarea
value={assignNote}
onChange={(event) => setAssignNote(event.target.value)}
placeholder="Please inspect and repair"
placeholder="Add any instructions or notes for the contractor..."
/>
</div>
<Button
disabled={!selectedUserId || assignMutation.isPending}
disabled={!selectedUserId || !dueDate || assignMutation.isPending}
onClick={() => {
if (!selectedUserId) return;
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
Assign Ticket
</Button>
</div>
</TicketActionCard>

View File

@@ -14,11 +14,9 @@ import {
ComboboxTrigger,
ComboboxValue,
} from '@/components/ui/combobox';
import { cn } from '@/lib/utils';
import type {
AsyncSelectOption,
AsyncSelectProps,
} from './AsyncSelect.types';
import type { AsyncSelectOption, AsyncSelectProps } from './AsyncSelect.types';
import { VirtualSelectList } from './VirtualSelectList';
type VirtualizerHandle = Virtualizer<HTMLDivElement, Element>;
@@ -30,6 +28,8 @@ export function AsyncSelect({
searchPlaceholder = 'Search...',
emptyMessage = 'No items found.',
disabled = false,
triggerClassName,
renderSelectedOption,
portalContainer,
}: AsyncSelectProps) {
const [open, setOpen] = useState(false);
@@ -125,7 +125,10 @@ export function AsyncSelect({
<Button
type="button"
variant="outline"
className="w-full justify-between font-normal"
className={cn(
'w-full justify-between font-normal',
triggerClassName,
)}
disabled={isDisabled}
>
{isLoading && !selectedOption ? (
@@ -134,14 +137,20 @@ export function AsyncSelect({
Loading...
</span>
) : (
<ComboboxValue placeholder={triggerPlaceholder} />
<>
{selectedOption && renderSelectedOption ? (
renderSelectedOption(selectedOption)
) : (
<ComboboxValue placeholder={triggerPlaceholder} />
)}
</>
)}
</Button>
}
/>
<ComboboxContent
container={portalContainer}
className="min-w-[var(--anchor-width)]"
className="min-w-(--anchor-width)"
>
<div className="border-b p-2">
<ComboboxInput

View File

@@ -1,8 +1,9 @@
import type { RefObject } from 'react';
import type { ReactNode, RefObject } from 'react';
export type AsyncSelectOption = {
value: string;
label: string;
data?: unknown;
};
export type PaginatedResult<T> = {
@@ -47,5 +48,7 @@ export interface AsyncSelectProps {
searchPlaceholder?: string;
emptyMessage?: string;
disabled?: boolean;
triggerClassName?: string;
renderSelectedOption?: (option: AsyncSelectOption) => ReactNode;
portalContainer?: RefObject<HTMLDivElement | null>;
}

View File

@@ -0,0 +1,171 @@
'use client';
import * as React from 'react';
import { CalendarDays, ChevronDownIcon, Globe2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
const DEFAULT_TIME_ZONES = [
'UTC',
'Asia/Kolkata',
'Asia/Dubai',
'Asia/Singapore',
'Asia/Tokyo',
'Europe/London',
'America/New_York',
'America/Los_Angeles',
'Australia/Sydney',
] as const;
export interface DatePickerProps {
value?: Date;
defaultValue?: Date;
onChange?: (date: Date | undefined) => void;
timeZone?: string;
defaultTimeZone?: string;
onTimeZoneChange?: (timeZone: string) => void;
timeZones?: readonly string[];
showTimeZone?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
}
function getBrowserTimeZone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
}
function formatDate(date: Date, timeZone: string) {
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'long',
timeZone,
}).format(date);
}
function getTimeZoneLabel(timeZone: string) {
return timeZone.replaceAll('_', ' ');
}
export function DatePicker(props: DatePickerProps) {
const {
value,
defaultValue,
onChange,
timeZone,
defaultTimeZone,
onTimeZoneChange,
timeZones = DEFAULT_TIME_ZONES,
showTimeZone = true,
placeholder = 'Pick a date',
disabled = false,
className,
} = props;
const isDateControlled = Object.hasOwn(props, 'value');
const isTimeZoneControlled = Object.hasOwn(props, 'timeZone');
const [internalDate, setInternalDate] = React.useState(defaultValue);
const [internalTimeZone, setInternalTimeZone] = React.useState(
defaultTimeZone ?? 'UTC',
);
React.useEffect(() => {
if (!isTimeZoneControlled && !defaultTimeZone) {
setInternalTimeZone(getBrowserTimeZone());
}
}, [defaultTimeZone, isTimeZoneControlled]);
const selectedDate = isDateControlled ? value : internalDate;
const selectedTimeZone = timeZone ?? internalTimeZone;
const availableTimeZones = React.useMemo(
() => Array.from(new Set([selectedTimeZone, ...timeZones])).filter(Boolean),
[selectedTimeZone, timeZones],
);
const handleDateChange = (nextDate: Date | undefined) => {
if (!isDateControlled) setInternalDate(nextDate);
onChange?.(nextDate);
};
const handleTimeZoneChange = (nextTimeZone: string | null) => {
if (!nextTimeZone) return;
if (!isTimeZoneControlled) setInternalTimeZone(nextTimeZone);
onTimeZoneChange?.(nextTimeZone);
};
return (
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
disabled={disabled}
data-empty={!selectedDate}
className={cn(
'w-64 justify-between text-left font-normal data-[empty=true]:text-muted-foreground',
className,
)}
>
<span className="flex min-w-0 items-center gap-2">
<CalendarDays className="size-4 shrink-0" />
<span className="truncate">
{selectedDate
? formatDate(selectedDate, selectedTimeZone)
: placeholder}
</span>
</span>
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleDateChange}
defaultMonth={selectedDate}
timeZone={selectedTimeZone}
/>
{showTimeZone ? (
<div className="flex items-center gap-2 border-t p-3">
<Globe2 className="size-4 shrink-0 text-muted-foreground" />
<Select
value={selectedTimeZone}
onValueChange={handleTimeZoneChange}
>
<SelectTrigger
size="sm"
className="min-w-0 flex-1"
aria-label="Time zone"
>
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
{availableTimeZones.map((zone) => (
<SelectItem key={zone} value={zone}>
{getTimeZoneLabel(zone)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
</PopoverContent>
</Popover>
);
}
export { DEFAULT_TIME_ZONES };

View File

@@ -1,5 +1,7 @@
'use client';
import { UserRound } from 'lucide-react';
import { AsyncSelect, usePaginatedSelect } from '@/components/async-select';
import type { AsyncSelectOption } from '@/components/async-select';
import { ticketKeys } from '@/app/(modules)/ticket/queries/ticketKeys';
@@ -14,6 +16,7 @@ function mapWorkerOption(user: AssignableTicketUser): AsyncSelectOption {
return {
value: String(user.id),
label: user.name || user.email || user.username,
data: user,
};
}
@@ -58,14 +61,40 @@ export function AssignableWorkerSelect({
[...ticketKeys.assignableUsers(), 'selected', workerId] as const,
});
const handleValueChange = (workerId: string) => {
const option = lookup.options.find((item) => item.value === workerId);
const user = option?.data as AssignableTicketUser | undefined;
if (user) onValueChange(workerId, user);
};
const selectedUser = lookup.selectedOption?.data as
| AssignableTicketUser
| undefined;
return (
<AsyncSelect
lookup={lookup}
onValueChange={onValueChange}
onValueChange={handleValueChange}
disabled={disabled}
placeholder="Select contractor"
searchPlaceholder="Search contractors"
emptyMessage="No assignable contractors found."
triggerClassName="h-auto min-h-14 py-2"
renderSelectedOption={(option) => (
<span className="flex min-w-0 items-center gap-2.5">
<span className="flex size-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
<UserRound className="size-5" aria-hidden />
</span>
<span className="min-w-0 text-left">
<span className="block truncate font-medium text-foreground">
{option.label}
</span>
<span className="block truncate text-xs text-muted-foreground">
{selectedUser?.role.name}
</span>
</span>
</span>
)}
/>
);
}

View File

@@ -1,5 +1,7 @@
import type { AssignableTicketUser } from '@/types';
export interface AssignableWorkerSelectProps {
value: string;
onValueChange: (value: string) => void;
onValueChange: (value: string, user: AssignableTicketUser) => void;
disabled?: boolean;
}

View File

@@ -1,54 +1,54 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from 'radix-ui';
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex cursor-pointer shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
'bg-destructive text-white hover:bg-destructive/90 dark:bg-destructive/60',
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground focus-visible:border-ring dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-8',
'icon-lg': 'size-10',
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
},
);
}
)
function Button({
className,
variant = 'default',
size = 'default',
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<'button'> &
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : 'button';
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
@@ -58,7 +58,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
)
}
export { Button, buttonVariants };
export { Button, buttonVariants }

View File

@@ -0,0 +1,220 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
DayPicker,
getDefaultClassNames,
type DayButton,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute inset-0 bg-popover opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"font-medium select-none",
captionLayout === "label"
? "text-sm"
: "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
defaultClassNames.caption_label
),
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-(--cell-size) select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] text-muted-foreground select-none",
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View File

@@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content fixed z-50 flex h-auto flex-col bg-background",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("font-semibold text-foreground", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@@ -5,6 +5,12 @@ export interface AssignableTicketUser {
name: string;
email: string;
username: string;
phone_number: string | null;
role: {
id: number;
name: string;
};
}
export interface AssignableTicketUsersParams extends PaginationParams {
@@ -19,6 +25,7 @@ export interface AssignableTicketUsersResponse {
export interface AssignTicketPayload {
assigned_to_user_id: number;
defect_class: string;
due_at: string;
note?: string;
}