feat(ticket): refine repair report and history timeline UI

This commit is contained in:
2026-06-19 11:22:41 +05:30
parent 8c3d19a429
commit a567a52e26
27 changed files with 1550 additions and 363 deletions

View File

@@ -0,0 +1,71 @@
'use client';
import { AsyncSelect, usePaginatedSelect } from '@/components/async-select';
import type { AsyncSelectOption } from '@/components/async-select';
import { ticketKeys } from '@/app/(modules)/ticket/queries/ticketKeys';
import { ticketService } from '@/services/api';
import type { AssignableTicketUser } from '@/types';
import type { AssignableWorkerSelectProps } from './AssignableWorkerSelect.types';
const WORKER_PAGE_SIZE = 20;
function mapWorkerOption(user: AssignableTicketUser): AsyncSelectOption {
return {
value: String(user.id),
label: user.name || user.email || user.username,
};
}
export function AssignableWorkerSelect({
value,
onValueChange,
disabled = false,
}: AssignableWorkerSelectProps) {
const lookup = usePaginatedSelect<AssignableTicketUser>({
selectedValue: value,
pageSize: WORKER_PAGE_SIZE,
queryKey: (searchTerm) =>
[
...ticketKeys.assignableUsers(),
'async-lookup',
searchTerm,
WORKER_PAGE_SIZE,
] as const,
queryFn: async ({ searchTerm, skip, limit }) => {
const data = await ticketService.getAssignableUsers({
search_term: searchTerm || undefined,
skip,
limit,
});
return { items: data.items, total: data.total };
},
mapOption: mapWorkerOption,
resolveSelected: async (workerId) => {
if (!workerId) return null;
const data = await ticketService.getAssignableUsers({
skip: 0,
limit: WORKER_PAGE_SIZE,
});
return (
data.items.find((worker) => String(worker.id) === workerId) ?? null
);
},
resolveSelectedQueryKey: (workerId) =>
[...ticketKeys.assignableUsers(), 'selected', workerId] as const,
});
return (
<AsyncSelect
lookup={lookup}
onValueChange={onValueChange}
disabled={disabled}
placeholder="Select worker"
searchPlaceholder="Search workers..."
emptyMessage="No assignable workers found."
/>
);
}

View File

@@ -0,0 +1,5 @@
export interface AssignableWorkerSelectProps {
value: string;
onValueChange: (value: string) => void;
disabled?: boolean;
}

View File

@@ -1,24 +1,28 @@
'use client';
"use client"
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import * as React from "react"
import { Avatar as AvatarPrimitive } from "radix-ui"
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils"
function Avatar({
className,
size = "default",
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className,
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
className
)}
{...props}
/>
);
)
}
function AvatarImage({
@@ -28,10 +32,10 @@ function AvatarImage({
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn('aspect-square size-full', className)}
className={cn("aspect-square size-full", className)}
{...props}
/>
);
)
}
function AvatarFallback({
@@ -42,12 +46,64 @@ function AvatarFallback({
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className,
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
);
)
}
export { Avatar, AvatarImage, AvatarFallback };
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarBadge,
AvatarGroup,
AvatarGroupCount,
}