Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useEffect } from 'react';
|
|
|
|
|
import { useForm, useFieldArray } from 'react-hook-form';
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import { Plus, Trash2, Loader2 } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
import {
|
|
|
|
|
Sheet,
|
|
|
|
|
SheetContent,
|
|
|
|
|
SheetHeader,
|
|
|
|
|
SheetTitle,
|
|
|
|
|
SheetFooter,
|
|
|
|
|
} from '@/components/ui/sheet';
|
|
|
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
|
|
|
import { Separator } from '@/components/ui/separator';
|
|
|
|
|
import { TagPicker } from '@/components/shared/tag-picker';
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
|
|
|
|
import { createClientSchema, type CreateClientInput } from '@/lib/validators/clients';
|
|
|
|
|
|
|
|
|
|
interface ClientFormProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
/** If provided, form is in edit mode */
|
|
|
|
|
client?: {
|
|
|
|
|
id: string;
|
|
|
|
|
fullName: string;
|
|
|
|
|
companyName?: string | null;
|
|
|
|
|
nationality?: string | null;
|
|
|
|
|
isProxy?: boolean;
|
|
|
|
|
proxyType?: string | null;
|
|
|
|
|
actualOwnerName?: string | null;
|
|
|
|
|
yachtName?: string | null;
|
|
|
|
|
berthSizeDesired?: string | null;
|
|
|
|
|
preferredContactMethod?: string | null;
|
|
|
|
|
preferredLanguage?: string | null;
|
|
|
|
|
timezone?: string | null;
|
|
|
|
|
source?: string | null;
|
|
|
|
|
sourceDetails?: string | null;
|
|
|
|
|
contacts?: Array<{
|
|
|
|
|
channel: string;
|
|
|
|
|
value: string;
|
|
|
|
|
label?: string | null;
|
|
|
|
|
isPrimary?: boolean;
|
|
|
|
|
}>;
|
|
|
|
|
tags?: Array<{ id: string }>;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ClientForm({ open, onOpenChange, client }: ClientFormProps) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const isEdit = !!client;
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
register,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
control,
|
|
|
|
|
watch,
|
|
|
|
|
setValue,
|
|
|
|
|
reset,
|
|
|
|
|
formState: { errors, isSubmitting },
|
|
|
|
|
} = useForm<CreateClientInput>({
|
|
|
|
|
resolver: zodResolver(createClientSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
fullName: '',
|
|
|
|
|
contacts: [{ channel: 'email', value: '', isPrimary: true }],
|
|
|
|
|
isProxy: false,
|
|
|
|
|
tagIds: [],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { fields, append, remove } = useFieldArray({ control, name: 'contacts' });
|
|
|
|
|
const isProxy = watch('isProxy');
|
|
|
|
|
const tagIds = watch('tagIds') ?? [];
|
|
|
|
|
|
|
|
|
|
// Populate form when editing
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (client && open) {
|
|
|
|
|
reset({
|
|
|
|
|
fullName: client.fullName,
|
|
|
|
|
companyName: client.companyName ?? undefined,
|
|
|
|
|
nationality: client.nationality ?? undefined,
|
|
|
|
|
isProxy: client.isProxy ?? false,
|
|
|
|
|
proxyType: client.proxyType ?? undefined,
|
|
|
|
|
actualOwnerName: client.actualOwnerName ?? undefined,
|
|
|
|
|
yachtName: client.yachtName ?? undefined,
|
|
|
|
|
berthSizeDesired: client.berthSizeDesired ?? undefined,
|
2026-03-26 12:06:18 +01:00
|
|
|
preferredContactMethod: (client.preferredContactMethod as string) ?? undefined,
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
preferredLanguage: client.preferredLanguage ?? undefined,
|
|
|
|
|
timezone: client.timezone ?? undefined,
|
2026-03-26 12:06:18 +01:00
|
|
|
source: (client.source as string) ?? undefined,
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
sourceDetails: client.sourceDetails ?? undefined,
|
|
|
|
|
contacts:
|
|
|
|
|
client.contacts && client.contacts.length > 0
|
|
|
|
|
? client.contacts.map((c) => ({
|
2026-03-26 12:06:18 +01:00
|
|
|
channel: c.channel as 'email' | 'phone' | 'whatsapp' | 'other',
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
value: c.value,
|
|
|
|
|
label: c.label ?? undefined,
|
|
|
|
|
isPrimary: c.isPrimary ?? false,
|
|
|
|
|
}))
|
|
|
|
|
: [{ channel: 'email', value: '', isPrimary: true }],
|
|
|
|
|
tagIds: client.tags?.map((t) => t.id) ?? [],
|
|
|
|
|
});
|
|
|
|
|
} else if (!client && open) {
|
|
|
|
|
reset({
|
|
|
|
|
fullName: '',
|
|
|
|
|
contacts: [{ channel: 'email', value: '', isPrimary: true }],
|
|
|
|
|
isProxy: false,
|
|
|
|
|
tagIds: [],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [client, open, reset]);
|
|
|
|
|
|
|
|
|
|
const mutation = useMutation({
|
|
|
|
|
mutationFn: async (data: CreateClientInput) => {
|
|
|
|
|
if (isEdit) {
|
2026-03-26 12:06:18 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const { contacts, tagIds: tIds, ...rest } = data;
|
|
|
|
|
await apiFetch(`/api/v1/clients/${client!.id}`, { method: 'PATCH', body: rest });
|
|
|
|
|
if (tIds) {
|
|
|
|
|
await apiFetch(`/api/v1/clients/${client!.id}/tags`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: { tagIds: tIds },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
await apiFetch('/api/v1/clients', { method: 'POST', body: data });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<SheetContent className="w-full sm:max-w-2xl overflow-y-auto">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>{isEdit ? 'Edit Client' : 'New Client'}</SheetTitle>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={handleSubmit((data) => mutation.mutate(data))}
|
|
|
|
|
className="space-y-6 py-6"
|
|
|
|
|
>
|
|
|
|
|
{/* Basic Info */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Basic Information
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="col-span-2 space-y-1">
|
|
|
|
|
<Label>Full Name *</Label>
|
|
|
|
|
<Input {...register('fullName')} placeholder="John Smith" />
|
|
|
|
|
{errors.fullName && (
|
|
|
|
|
<p className="text-xs text-destructive">{errors.fullName.message}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Company Name</Label>
|
|
|
|
|
<Input {...register('companyName')} placeholder="Acme Corp" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Nationality</Label>
|
|
|
|
|
<Input {...register('nationality')} placeholder="British" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* Contacts */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Contacts
|
|
|
|
|
</h3>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
append({ channel: 'email', value: '', isPrimary: false })
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
|
|
|
Add Contact
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{errors.contacts?.root && (
|
|
|
|
|
<p className="text-xs text-destructive">{errors.contacts.root.message}</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{fields.map((field, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={field.id}
|
|
|
|
|
className="grid grid-cols-12 gap-2 items-end p-3 rounded-lg border bg-muted/30"
|
|
|
|
|
>
|
|
|
|
|
<div className="col-span-3 space-y-1">
|
|
|
|
|
<Label className="text-xs">Channel</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={watch(`contacts.${index}.channel`)}
|
|
|
|
|
onValueChange={(v) =>
|
2026-03-26 12:06:18 +01:00
|
|
|
setValue(`contacts.${index}.channel`, v as 'email' | 'phone' | 'whatsapp' | 'other')
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-8">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="email">Email</SelectItem>
|
|
|
|
|
<SelectItem value="phone">Phone</SelectItem>
|
|
|
|
|
<SelectItem value="whatsapp">WhatsApp</SelectItem>
|
|
|
|
|
<SelectItem value="other">Other</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="col-span-5 space-y-1">
|
|
|
|
|
<Label className="text-xs">Value</Label>
|
|
|
|
|
<Input
|
|
|
|
|
{...register(`contacts.${index}.value`)}
|
|
|
|
|
className="h-8"
|
|
|
|
|
placeholder="email@example.com"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="col-span-2 space-y-1">
|
|
|
|
|
<Label className="text-xs">Label</Label>
|
|
|
|
|
<Input
|
|
|
|
|
{...register(`contacts.${index}.label`)}
|
|
|
|
|
className="h-8"
|
|
|
|
|
placeholder="work"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="col-span-1 flex items-center gap-1 pb-1">
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={watch(`contacts.${index}.isPrimary`)}
|
|
|
|
|
onCheckedChange={(v) =>
|
|
|
|
|
setValue(`contacts.${index}.isPrimary`, !!v)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Label className="text-xs">Primary</Label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="col-span-1 flex justify-end pb-1">
|
|
|
|
|
{fields.length > 1 && (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-7 w-7 text-destructive"
|
|
|
|
|
onClick={() => remove(index)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* Proxy */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Proxy Information
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="isProxy"
|
|
|
|
|
checked={watch('isProxy')}
|
|
|
|
|
onCheckedChange={(v) => setValue('isProxy', !!v)}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="isProxy">This is a proxy client</Label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isProxy && (
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Proxy Type</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={watch('proxyType') ?? ''}
|
|
|
|
|
onValueChange={(v) => setValue('proxyType', v)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Select type" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="broker">Broker</SelectItem>
|
|
|
|
|
<SelectItem value="representative">Representative</SelectItem>
|
|
|
|
|
<SelectItem value="family_member">Family Member</SelectItem>
|
|
|
|
|
<SelectItem value="legal_counsel">Legal Counsel</SelectItem>
|
|
|
|
|
<SelectItem value="other">Other</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Actual Owner Name</Label>
|
|
|
|
|
<Input
|
|
|
|
|
{...register('actualOwnerName')}
|
|
|
|
|
placeholder="Actual owner"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* Yacht Details */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Yacht Details
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="col-span-2 space-y-1">
|
|
|
|
|
<Label>Yacht Name</Label>
|
|
|
|
|
<Input {...register('yachtName')} placeholder="My Yacht" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Berth Size Desired</Label>
|
|
|
|
|
<Input {...register('berthSizeDesired')} placeholder="e.g. 30m" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* Source & Preferences */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Source & Preferences
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Source</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={watch('source') ?? ''}
|
2026-03-26 12:06:18 +01:00
|
|
|
onValueChange={(v) => setValue('source', v as 'website' | 'manual' | 'referral' | 'broker')}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Select source" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="website">Website</SelectItem>
|
|
|
|
|
<SelectItem value="manual">Manual</SelectItem>
|
|
|
|
|
<SelectItem value="referral">Referral</SelectItem>
|
|
|
|
|
<SelectItem value="broker">Broker</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Preferred Contact Method</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={watch('preferredContactMethod') ?? ''}
|
2026-03-26 12:06:18 +01:00
|
|
|
onValueChange={(v) => setValue('preferredContactMethod', v as 'email' | 'phone' | 'whatsapp')}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Select method" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="email">Email</SelectItem>
|
|
|
|
|
<SelectItem value="phone">Phone</SelectItem>
|
|
|
|
|
<SelectItem value="whatsapp">WhatsApp</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Preferred Language</Label>
|
|
|
|
|
<Input {...register('preferredLanguage')} placeholder="English" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label>Timezone</Label>
|
|
|
|
|
<Input {...register('timezone')} placeholder="UTC+0" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="col-span-2 space-y-1">
|
|
|
|
|
<Label>Source Details</Label>
|
|
|
|
|
<Input
|
|
|
|
|
{...register('sourceDetails')}
|
|
|
|
|
placeholder="Referred by John Doe"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* Tags */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Tags</Label>
|
|
|
|
|
<TagPicker
|
|
|
|
|
selectedIds={tagIds}
|
|
|
|
|
onChange={(ids) => setValue('tagIds', ids)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<SheetFooter>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
|
|
|
|
|
{(isSubmitting || mutation.isPending) && (
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
)}
|
|
|
|
|
{isEdit ? 'Save Changes' : 'Create Client'}
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
|
|
|
|
);
|
|
|
|
|
}
|