feat(ticket): refine repair report and history timeline UI
This commit is contained in:
71
src/components/lookups/AssignableWorkerSelect.tsx
Normal file
71
src/components/lookups/AssignableWorkerSelect.tsx
Normal 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."
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
src/components/lookups/AssignableWorkerSelect.types.ts
Normal file
5
src/components/lookups/AssignableWorkerSelect.types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface AssignableWorkerSelectProps {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user