feat(clients): align client form with first and last name API fields
This commit is contained in:
@@ -19,10 +19,12 @@ export function useClientColumns(): ColumnDef<Client>[] {
|
||||
return useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
accessorKey: 'first_name',
|
||||
header: 'Client',
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium">{row.original.name}</span>
|
||||
<span className="font-medium">
|
||||
{`${row.original.first_name} ${row.original.last_name}`.trim()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,7 +40,12 @@ export function ClientSheet({
|
||||
canSubmit,
|
||||
isSaving,
|
||||
}: ClientSheetProps) {
|
||||
const nameErrorMessage = isSubmitted ? errors.name?.message : undefined;
|
||||
const firstNameErrorMessage = isSubmitted
|
||||
? errors.first_name?.message
|
||||
: undefined;
|
||||
const lastNameErrorMessage = isSubmitted
|
||||
? errors.last_name?.message
|
||||
: undefined;
|
||||
const emailErrorMessage = isSubmitted ? errors.email?.message : undefined;
|
||||
const landlineErrorMessage = isSubmitted
|
||||
? errors.landline_number?.message
|
||||
@@ -68,23 +73,43 @@ export function ClientSheet({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
|
||||
<form
|
||||
noValidate
|
||||
onSubmit={onSubmit}
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
>
|
||||
<div className="max-h-[58vh] space-y-5 overflow-y-auto px-6 py-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
id="client-name"
|
||||
label="Client Name"
|
||||
id="client-first-name"
|
||||
label="First Name"
|
||||
required
|
||||
error={nameErrorMessage}
|
||||
error={firstNameErrorMessage}
|
||||
>
|
||||
<Input
|
||||
id="client-name"
|
||||
placeholder="Enter client name"
|
||||
aria-invalid={!!nameErrorMessage}
|
||||
{...register('name')}
|
||||
id="client-first-name"
|
||||
placeholder="Enter first name"
|
||||
aria-invalid={!!firstNameErrorMessage}
|
||||
{...register('first_name')}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
id="client-last-name"
|
||||
label="Last Name"
|
||||
required
|
||||
error={lastNameErrorMessage}
|
||||
>
|
||||
<Input
|
||||
id="client-last-name"
|
||||
placeholder="Enter last name"
|
||||
aria-invalid={!!lastNameErrorMessage}
|
||||
{...register('last_name')}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
id="client-email"
|
||||
label="Company Email"
|
||||
@@ -94,14 +119,12 @@ export function ClientSheet({
|
||||
<Input
|
||||
id="client-email"
|
||||
type="email"
|
||||
placeholder="info@example.com"
|
||||
placeholder="Enter company email"
|
||||
aria-invalid={!!emailErrorMessage}
|
||||
{...register('email')}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
id="landline-number"
|
||||
label="Landline Number"
|
||||
@@ -110,42 +133,50 @@ export function ClientSheet({
|
||||
>
|
||||
<Input
|
||||
id="landline-number"
|
||||
placeholder="+91-22-12345678"
|
||||
placeholder="Enter landline number"
|
||||
aria-invalid={!!landlineErrorMessage}
|
||||
{...register('landline_number')}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
id="address"
|
||||
label="Address"
|
||||
required
|
||||
error={addressErrorMessage}
|
||||
>
|
||||
<Input
|
||||
id="address"
|
||||
placeholder="12 MG Road, Mumbai, MH 400001"
|
||||
aria-invalid={!!addressErrorMessage}
|
||||
{...register('address')}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
id="address"
|
||||
label="Address"
|
||||
required
|
||||
error={addressErrorMessage}
|
||||
>
|
||||
<Input
|
||||
id="address"
|
||||
placeholder="Enter address"
|
||||
aria-invalid={!!addressErrorMessage}
|
||||
{...register('address')}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<FormField id="gst" label="GST">
|
||||
<Input
|
||||
id="gst"
|
||||
placeholder="27ABCDE1234F1Z5"
|
||||
placeholder="Enter GST number"
|
||||
{...register('gst')}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField id="pan" label="PAN">
|
||||
<Input id="pan" placeholder="ABCDE1234F" {...register('pan')} />
|
||||
<Input
|
||||
id="pan"
|
||||
placeholder="Enter PAN number"
|
||||
{...register('pan')}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField id="tan" label="TAN">
|
||||
<Input id="tan" placeholder="MUMA12345B" {...register('tan')} />
|
||||
<Input
|
||||
id="tan"
|
||||
placeholder="Enter TAN number"
|
||||
{...register('tan')}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
@@ -172,7 +203,7 @@ export function ClientSheet({
|
||||
>
|
||||
<Input
|
||||
id="contact-phone-number"
|
||||
placeholder="+919876543210"
|
||||
placeholder="Enter contact phone"
|
||||
aria-invalid={!!contactPhoneErrorMessage}
|
||||
{...register('contact_phone_number')}
|
||||
/>
|
||||
@@ -187,7 +218,7 @@ export function ClientSheet({
|
||||
<Input
|
||||
id="contact-email"
|
||||
type="email"
|
||||
placeholder="contact@example.com"
|
||||
placeholder="Enter contact email"
|
||||
aria-invalid={!!contactEmailErrorMessage}
|
||||
{...register('contact_email')}
|
||||
/>
|
||||
|
||||
@@ -9,37 +9,32 @@ import { z } from 'zod';
|
||||
import type { Client } from '@/types';
|
||||
import { useSaveClientMutation } from './useClientMutations';
|
||||
|
||||
const phoneSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Phone number is required')
|
||||
.regex(/^\+(?:[0-9] ?|-){6,18}[0-9]$/, 'Please enter a valid phone number');
|
||||
|
||||
const requiredText = (message: string) => z.string().trim().min(1, message);
|
||||
const optionalTaxIdSchema = z.string().trim();
|
||||
|
||||
const clientFormSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
name: z.string().trim().min(1, 'Client name is required'),
|
||||
email: z.string().trim().min(1, 'Email is required').email('Invalid email'),
|
||||
landline_number: phoneSchema,
|
||||
address: z.string().trim().min(1, 'Address is required'),
|
||||
first_name: requiredText('First name is required'),
|
||||
last_name: requiredText('Last name is required'),
|
||||
email: requiredText('Email is required').email('Invalid email'),
|
||||
landline_number: requiredText('Landline number is required'),
|
||||
address: requiredText('Address is required'),
|
||||
gst: optionalTaxIdSchema,
|
||||
pan: optionalTaxIdSchema,
|
||||
tan: optionalTaxIdSchema,
|
||||
contact_name: z.string().trim().min(1, 'Contact name is required'),
|
||||
contact_phone_number: phoneSchema,
|
||||
contact_email: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Contact email is required')
|
||||
.email('Invalid contact email'),
|
||||
contact_name: requiredText('Contact name is required'),
|
||||
contact_phone_number: requiredText('Contact phone number is required'),
|
||||
contact_email: requiredText('Contact email is required').email(
|
||||
'Invalid contact email',
|
||||
),
|
||||
is_active: z.boolean(),
|
||||
});
|
||||
|
||||
export type ClientFormValues = z.infer<typeof clientFormSchema>;
|
||||
|
||||
const defaultValues: ClientFormValues = {
|
||||
name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
landline_number: '',
|
||||
address: '',
|
||||
@@ -73,7 +68,8 @@ export function useClientForm({ onSaved }: { onSaved: () => void }) {
|
||||
});
|
||||
|
||||
const clientId = useWatch({ control, name: 'id' });
|
||||
const name = useWatch({ control, name: 'name' }) || '';
|
||||
const firstName = useWatch({ control, name: 'first_name' }) || '';
|
||||
const lastName = useWatch({ control, name: 'last_name' }) || '';
|
||||
const email = useWatch({ control, name: 'email' }) || '';
|
||||
const landlineNumber = useWatch({ control, name: 'landline_number' }) || '';
|
||||
const address = useWatch({ control, name: 'address' }) || '';
|
||||
@@ -82,7 +78,8 @@ export function useClientForm({ onSaved }: { onSaved: () => void }) {
|
||||
useWatch({ control, name: 'contact_phone_number' }) || '';
|
||||
const contactEmail = useWatch({ control, name: 'contact_email' }) || '';
|
||||
const canSubmit =
|
||||
name.trim().length > 0 &&
|
||||
firstName.trim().length > 0 &&
|
||||
lastName.trim().length > 0 &&
|
||||
email.trim().length > 0 &&
|
||||
landlineNumber.trim().length > 0 &&
|
||||
address.trim().length > 0 &&
|
||||
@@ -111,7 +108,8 @@ export function useClientForm({ onSaved }: { onSaved: () => void }) {
|
||||
(client: Client) => {
|
||||
reset({
|
||||
id: client.id,
|
||||
name: client.name || '',
|
||||
first_name: client.first_name || '',
|
||||
last_name: client.last_name || '',
|
||||
email: client.email || '',
|
||||
landline_number: client.landline_number || '',
|
||||
address: client.address || '',
|
||||
|
||||
@@ -20,7 +20,8 @@ export function useSaveClientMutation({ onSaved }: { onSaved: () => void }) {
|
||||
mutationFn: (values: ClientFormValues) =>
|
||||
clientService.saveClient({
|
||||
id: values.id,
|
||||
name: values.name.trim(),
|
||||
first_name: values.first_name.trim(),
|
||||
last_name: values.last_name.trim(),
|
||||
email: values.email.trim(),
|
||||
landline_number: values.landline_number.trim(),
|
||||
address: values.address.trim(),
|
||||
|
||||
@@ -83,7 +83,11 @@ export function SegmentDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
|
||||
<form
|
||||
noValidate
|
||||
onSubmit={onSubmit}
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
>
|
||||
<div className="max-h-[58vh] space-y-6 overflow-y-auto px-6 py-4">
|
||||
<section className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -141,17 +141,7 @@ export function useSegmentForm({ onSaved }: { onSaved: () => void }) {
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
(values) => saveMutation.mutate(values),
|
||||
(formErrors) => {
|
||||
const firstMessage = Object.values(formErrors).find(
|
||||
(error) => error?.message,
|
||||
)?.message;
|
||||
if (firstMessage) {
|
||||
toast.error(String(firstMessage));
|
||||
}
|
||||
},
|
||||
);
|
||||
const onSubmit = handleSubmit((values) => saveMutation.mutate(values));
|
||||
|
||||
const openCreate = useCallback(() => {
|
||||
reset(defaultValues);
|
||||
|
||||
@@ -128,7 +128,11 @@ export function TenantSheet({
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField label="Client" required error={fieldError('client_id')}>
|
||||
<FormField
|
||||
label="Client"
|
||||
required
|
||||
error={fieldError('client_id')}
|
||||
>
|
||||
<Select
|
||||
value={clientId}
|
||||
onValueChange={onClientChange}
|
||||
@@ -146,7 +150,9 @@ export function TenantSheet({
|
||||
<SelectContent>
|
||||
{clients.map((client) => (
|
||||
<SelectItem key={client.id} value={String(client.id)}>
|
||||
{client.name}
|
||||
{[client.first_name, client.last_name]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowLeft, BarChart3 } from 'lucide-react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SparkleButton } from '@/components/ui/sparkle-button';
|
||||
import { PERMISSIONS } from '@/constants/permissions';
|
||||
import { PermissionGuard } from '@/guards';
|
||||
import type { TicketDetail } from '@/types';
|
||||
@@ -28,14 +29,12 @@ export function TicketDetailHeader({ ticket }: { ticket: TicketDetail }) {
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{ticket.video_id ? (
|
||||
<PermissionGuard permissions={PERMISSIONS.TICKET.ASSIGN}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
<SparkleButton
|
||||
className="h-10 min-w-36 px-5"
|
||||
onClick={() => router.push(`/results/${ticket.video_id}`)}
|
||||
>
|
||||
<BarChart3 className="size-4" />
|
||||
Analysis
|
||||
</Button>
|
||||
View Analysis
|
||||
</SparkleButton>
|
||||
</PermissionGuard>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import { RepairVideoComparison } from './repair-report/RepairVideoComparison';
|
||||
export function TicketRepairReportCard({ ticket }: { ticket: TicketDetail }) {
|
||||
const repair = ticket.repair;
|
||||
|
||||
if (!repair) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-5">
|
||||
<RepairReportPanel ticket={ticket} />
|
||||
|
||||
34
src/components/ui/sparkle-button.tsx
Normal file
34
src/components/ui/sparkle-button.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type SparkleButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export function SparkleButton({
|
||||
className,
|
||||
children,
|
||||
type = 'button',
|
||||
...props
|
||||
}: SparkleButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={cn(
|
||||
'group inline-flex h-12 min-w-40 shrink-0 cursor-pointer items-center justify-center gap-3 rounded-full border-0 bg-[#1C1A1C] px-6 text-sm font-semibold text-[#AAAAAA] outline-none transition-all duration-500 ease-in-out hover:-translate-y-0.5 hover:bg-[linear-gradient(0deg,#A47CF3,#683FEA)] hover:text-white hover:[box-shadow:inset_0_1px_0_0_rgba(255,255,255,0.4),inset_0_-4px_0_0_rgba(0,0,0,0.2),0_0_0_4px_rgba(255,255,255,0.2),0_0_180px_0_#9917FF] focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
className="size-5 fill-[#AAAAAA] transition-all duration-700 ease-in-out group-hover:scale-125 group-hover:fill-white"
|
||||
>
|
||||
<path d="M10 21.236 6.755 14.745.264 11.5l6.491-3.245L10 1.764l3.245 6.491 6.491 3.245-6.491 3.245L10 21.236ZM18 21l1.5 3 1.5-3 3-1.5-3-1.5-1.5-3-1.5 3-3 1.5 3 1.5ZM19.333 4.667 20.5 7l1.167-2.333L24 3.5l-2.333-1.167L20.5 0l-1.167 2.333L17 3.5l2.333 1.167Z" />
|
||||
</svg>
|
||||
<span>{children}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default function VideoPlayerSection({
|
||||
isLoading: isVideoLoading,
|
||||
isError: isVideoError,
|
||||
refetch: refetchVideo,
|
||||
} = useAnnotatedVideoQuery(data.annotated_video_url);
|
||||
} = useAnnotatedVideoQuery(data.processed_video_url || '');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -9,7 +9,8 @@ import type {
|
||||
|
||||
function toClientPayload(payload: ClientRequest) {
|
||||
return {
|
||||
name: payload.name,
|
||||
first_name: payload.first_name,
|
||||
last_name: payload.last_name,
|
||||
email: payload.email,
|
||||
landline_number: payload.landline_number,
|
||||
address: payload.address,
|
||||
@@ -34,7 +35,8 @@ export const clientService = {
|
||||
skip: params?.skip ?? 0,
|
||||
limit: params?.limit ?? 10,
|
||||
search_term: params?.search_term,
|
||||
name: params?.name,
|
||||
first_name: params?.first_name,
|
||||
last_name: params?.last_name,
|
||||
email: params?.email,
|
||||
contact_name: params?.contact_name,
|
||||
is_active: params?.is_active,
|
||||
|
||||
@@ -3,7 +3,9 @@ import { DetectionResultLog } from './detection';
|
||||
export type CompletedVideoResult = {
|
||||
video_id: string;
|
||||
status: 'completed' | string;
|
||||
annotated_video_url: string;
|
||||
// annotated_video_url: string;
|
||||
processed_video_url: string | null;
|
||||
raw_video_url: string | null;
|
||||
fps: number;
|
||||
duration_seconds: number;
|
||||
detection_mode?: string;
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { PaginationParams } from './common';
|
||||
|
||||
export interface Client {
|
||||
id: number;
|
||||
name: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
landline_number: string;
|
||||
address: string;
|
||||
@@ -19,7 +20,8 @@ export interface Client {
|
||||
|
||||
export interface ClientRequest {
|
||||
id?: number;
|
||||
name: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
landline_number: string;
|
||||
address: string;
|
||||
@@ -34,7 +36,8 @@ export interface ClientRequest {
|
||||
|
||||
export interface ClientListParams extends PaginationParams {
|
||||
search_term?: string;
|
||||
name?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
contact_name?: string;
|
||||
is_active?: boolean;
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface TicketVideo {
|
||||
id: string;
|
||||
name: string | null;
|
||||
url: string | null;
|
||||
processed_video_url: string | null;
|
||||
thumbnail: string | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user