@hed-hog/contact 0.0.294 → 0.0.296

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/person/dto/account.dto.d.ts +28 -0
  2. package/dist/person/dto/account.dto.d.ts.map +1 -0
  3. package/dist/person/dto/account.dto.js +123 -0
  4. package/dist/person/dto/account.dto.js.map +1 -0
  5. package/dist/person/dto/activity.dto.d.ts +15 -0
  6. package/dist/person/dto/activity.dto.d.ts.map +1 -0
  7. package/dist/person/dto/activity.dto.js +65 -0
  8. package/dist/person/dto/activity.dto.js.map +1 -0
  9. package/dist/person/dto/dashboard-query.dto.d.ts +9 -0
  10. package/dist/person/dto/dashboard-query.dto.d.ts.map +1 -0
  11. package/dist/person/dto/dashboard-query.dto.js +40 -0
  12. package/dist/person/dto/dashboard-query.dto.js.map +1 -0
  13. package/dist/person/dto/followup-query.dto.d.ts +10 -0
  14. package/dist/person/dto/followup-query.dto.d.ts.map +1 -0
  15. package/dist/person/dto/followup-query.dto.js +45 -0
  16. package/dist/person/dto/followup-query.dto.js.map +1 -0
  17. package/dist/person/dto/reports-query.dto.d.ts +8 -0
  18. package/dist/person/dto/reports-query.dto.d.ts.map +1 -0
  19. package/dist/person/dto/reports-query.dto.js +33 -0
  20. package/dist/person/dto/reports-query.dto.js.map +1 -0
  21. package/dist/person/person.controller.d.ts +266 -5
  22. package/dist/person/person.controller.d.ts.map +1 -1
  23. package/dist/person/person.controller.js +164 -6
  24. package/dist/person/person.controller.js.map +1 -1
  25. package/dist/person/person.service.d.ts +295 -5
  26. package/dist/person/person.service.d.ts.map +1 -1
  27. package/dist/person/person.service.js +1752 -27
  28. package/dist/person/person.service.js.map +1 -1
  29. package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
  30. package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
  31. package/hedhog/data/route.yaml +68 -19
  32. package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +9 -9
  33. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +573 -477
  34. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +9 -6
  35. package/hedhog/frontend/app/accounts/page.tsx.ejs +970 -892
  36. package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +240 -0
  37. package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +66 -0
  38. package/hedhog/frontend/app/activities/page.tsx.ejs +460 -812
  39. package/hedhog/frontend/app/dashboard/_components/dashboard-types.ts.ejs +70 -0
  40. package/hedhog/frontend/app/dashboard/page.tsx.ejs +639 -491
  41. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +785 -696
  42. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +10 -12
  43. package/hedhog/frontend/app/reports/_components/report-types.ts.ejs +84 -0
  44. package/hedhog/frontend/app/reports/page.tsx.ejs +1196 -15
  45. package/hedhog/frontend/messages/en.json +242 -38
  46. package/hedhog/frontend/messages/pt.json +242 -38
  47. package/hedhog/table/crm_activity.yaml +68 -0
  48. package/hedhog/table/crm_stage_history.yaml +34 -0
  49. package/hedhog/table/person_company.yaml +27 -5
  50. package/package.json +9 -9
  51. package/src/person/dto/account.dto.ts +100 -0
  52. package/src/person/dto/activity.dto.ts +54 -0
  53. package/src/person/dto/dashboard-query.dto.ts +25 -0
  54. package/src/person/dto/followup-query.dto.ts +25 -0
  55. package/src/person/dto/reports-query.dto.ts +25 -0
  56. package/src/person/person.controller.ts +176 -43
  57. package/src/person/person.service.ts +4825 -2226
@@ -1,477 +1,573 @@
1
- 'use client';
2
-
3
- import { Button } from '@/components/ui/button';
4
- import {
5
- Form,
6
- FormControl,
7
- FormField,
8
- FormItem,
9
- FormLabel,
10
- FormMessage,
11
- } from '@/components/ui/form';
12
- import { Input } from '@/components/ui/input';
13
- import {
14
- Select,
15
- SelectContent,
16
- SelectItem,
17
- SelectTrigger,
18
- SelectValue,
19
- } from '@/components/ui/select';
20
- import {
21
- Sheet,
22
- SheetContent,
23
- SheetDescription,
24
- SheetHeader,
25
- SheetTitle,
26
- } from '@/components/ui/sheet';
27
- import { zodResolver } from '@hookform/resolvers/zod';
28
- import { useEffect } from 'react';
29
- import { useForm } from 'react-hook-form';
30
- import { toast } from 'sonner';
31
- import { z } from 'zod';
32
- import type { Account, AccountFormValues, UserOption } from './account-types';
33
-
34
- const accountFormSchema = z.object({
35
- name: z.string().min(2, 'Name must be at least 2 characters'),
36
- trade_name: z.string().optional().nullable(),
37
- status: z.enum(['active', 'inactive']),
38
- industry: z.string().optional().nullable(),
39
- website: z.string().optional().nullable(),
40
- email: z
41
- .string()
42
- .email('Invalid email')
43
- .optional()
44
- .or(z.literal(''))
45
- .nullable(),
46
- phone: z.string().optional().nullable(),
47
- owner_user_id: z.number().optional().nullable(),
48
- annual_revenue: z.number().optional().nullable(),
49
- employee_count: z.number().optional().nullable(),
50
- lifecycle_stage: z
51
- .enum(['prospect', 'customer', 'churned', 'inactive'])
52
- .optional()
53
- .nullable(),
54
- city: z.string().optional().nullable(),
55
- state: z.string().optional().nullable(),
56
- });
57
-
58
- type AccountFormData = z.infer<typeof accountFormSchema>;
59
-
60
- interface AccountFormSheetProps {
61
- open: boolean;
62
- onOpenChange: (open: boolean) => void;
63
- account?: Account | null;
64
- owners: UserOption[];
65
- onSubmit: (data: AccountFormValues) => void;
66
- isLoading?: boolean;
67
- }
68
-
69
- export function AccountFormSheet({
70
- open,
71
- onOpenChange,
72
- account,
73
- owners,
74
- onSubmit,
75
- isLoading = false,
76
- }: AccountFormSheetProps) {
77
- const form = useForm<AccountFormData>({
78
- resolver: zodResolver(accountFormSchema),
79
- defaultValues: {
80
- name: '',
81
- trade_name: null,
82
- status: 'active',
83
- industry: null,
84
- website: null,
85
- email: null,
86
- phone: null,
87
- owner_user_id: null,
88
- annual_revenue: null,
89
- employee_count: null,
90
- lifecycle_stage: 'prospect' as const,
91
- city: null,
92
- state: null,
93
- },
94
- });
95
-
96
- useEffect(() => {
97
- if (account) {
98
- form.reset({
99
- name: account.name,
100
- trade_name: account.trade_name ?? null,
101
- status: account.status,
102
- industry: account.industry ?? null,
103
- website: account.website ?? null,
104
- email: account.email ?? null,
105
- phone: account.phone ?? null,
106
- owner_user_id: account.owner_user_id ?? null,
107
- annual_revenue: account.annual_revenue ?? null,
108
- employee_count: account.employee_count ?? null,
109
- lifecycle_stage: account.lifecycle_stage ?? 'prospect',
110
- city: account.city ?? null,
111
- state: account.state ?? null,
112
- });
113
- } else {
114
- form.reset({
115
- name: '',
116
- trade_name: null,
117
- status: 'active',
118
- industry: null,
119
- website: null,
120
- email: null,
121
- phone: null,
122
- owner_user_id: null,
123
- annual_revenue: null,
124
- employee_count: null,
125
- lifecycle_stage: 'prospect',
126
- city: null,
127
- state: null,
128
- });
129
- }
130
- }, [account, form, open]);
131
-
132
- const handleSubmit = (data: AccountFormData) => {
133
- try {
134
- onSubmit(data);
135
- form.reset();
136
- onOpenChange(false);
137
- } catch (error) {
138
- toast.error('Failed to save account');
139
- }
140
- };
141
-
142
- return (
143
- <Sheet open={open} onOpenChange={onOpenChange}>
144
- <SheetContent className="max-h-screen overflow-y-auto">
145
- <SheetHeader>
146
- <SheetTitle>{account ? 'Edit Account' : 'New Account'}</SheetTitle>
147
- <SheetDescription>
148
- {account
149
- ? 'Update account information and details'
150
- : 'Create a new account record in the CRM'}
151
- </SheetDescription>
152
- </SheetHeader>
153
-
154
- <Form {...form}>
155
- <form
156
- onSubmit={form.handleSubmit(handleSubmit)}
157
- className="space-y-4 py-4"
158
- >
159
- {/* Name and Trade Name */}
160
- <FormField
161
- control={form.control}
162
- name="name"
163
- render={({ field }) => (
164
- <FormItem>
165
- <FormLabel>Company Name</FormLabel>
166
- <FormControl>
167
- <Input
168
- placeholder="Company name"
169
- {...field}
170
- disabled={isLoading}
171
- />
172
- </FormControl>
173
- <FormMessage />
174
- </FormItem>
175
- )}
176
- />
177
-
178
- <FormField
179
- control={form.control}
180
- name="trade_name"
181
- render={({ field }) => (
182
- <FormItem>
183
- <FormLabel>Trade Name (optional)</FormLabel>
184
- <FormControl>
185
- <Input
186
- placeholder="Trade name or brand"
187
- value={field.value ?? ''}
188
- onChange={(e) => field.onChange(e.target.value || null)}
189
- disabled={isLoading}
190
- />
191
- </FormControl>
192
- <FormMessage />
193
- </FormItem>
194
- )}
195
- />
196
-
197
- {/* Status and Lifecycle Stage */}
198
- <div className="grid gap-4 grid-cols-2">
199
- <FormField
200
- control={form.control}
201
- name="status"
202
- render={({ field }) => (
203
- <FormItem>
204
- <FormLabel>Status</FormLabel>
205
- <Select
206
- value={field.value}
207
- onValueChange={field.onChange}
208
- disabled={isLoading}
209
- >
210
- <FormControl>
211
- <SelectTrigger>
212
- <SelectValue />
213
- </SelectTrigger>
214
- </FormControl>
215
- <SelectContent>
216
- <SelectItem value="active">Active</SelectItem>
217
- <SelectItem value="inactive">Inactive</SelectItem>
218
- </SelectContent>
219
- </Select>
220
- <FormMessage />
221
- </FormItem>
222
- )}
223
- />
224
-
225
- <FormField
226
- control={form.control}
227
- name="lifecycle_stage"
228
- render={({ field }) => (
229
- <FormItem>
230
- <FormLabel>Stage</FormLabel>
231
- <Select
232
- value={field.value ?? 'prospect'}
233
- onValueChange={(value) => field.onChange(value || null)}
234
- disabled={isLoading}
235
- >
236
- <FormControl>
237
- <SelectTrigger>
238
- <SelectValue />
239
- </SelectTrigger>
240
- </FormControl>
241
- <SelectContent>
242
- <SelectItem value="prospect">Prospect</SelectItem>
243
- <SelectItem value="customer">Customer</SelectItem>
244
- <SelectItem value="churned">Churned</SelectItem>
245
- <SelectItem value="inactive">Inactive</SelectItem>
246
- </SelectContent>
247
- </Select>
248
- <FormMessage />
249
- </FormItem>
250
- )}
251
- />
252
- </div>
253
-
254
- {/* Contact Information */}
255
- <FormField
256
- control={form.control}
257
- name="email"
258
- render={({ field }) => (
259
- <FormItem>
260
- <FormLabel>Email (optional)</FormLabel>
261
- <FormControl>
262
- <Input
263
- type="email"
264
- placeholder="contact@company.com"
265
- value={field.value ?? ''}
266
- onChange={(e) => field.onChange(e.target.value || null)}
267
- disabled={isLoading}
268
- />
269
- </FormControl>
270
- <FormMessage />
271
- </FormItem>
272
- )}
273
- />
274
-
275
- <FormField
276
- control={form.control}
277
- name="phone"
278
- render={({ field }) => (
279
- <FormItem>
280
- <FormLabel>Phone (optional)</FormLabel>
281
- <FormControl>
282
- <Input
283
- placeholder="(11) 99999-9999"
284
- value={field.value ?? ''}
285
- onChange={(e) => field.onChange(e.target.value || null)}
286
- disabled={isLoading}
287
- />
288
- </FormControl>
289
- <FormMessage />
290
- </FormItem>
291
- )}
292
- />
293
-
294
- <FormField
295
- control={form.control}
296
- name="website"
297
- render={({ field }) => (
298
- <FormItem>
299
- <FormLabel>Website (optional)</FormLabel>
300
- <FormControl>
301
- <Input
302
- placeholder="www.company.com"
303
- value={field.value ?? ''}
304
- onChange={(e) => field.onChange(e.target.value || null)}
305
- disabled={isLoading}
306
- />
307
- </FormControl>
308
- <FormMessage />
309
- </FormItem>
310
- )}
311
- />
312
-
313
- {/* Business Information */}
314
- <FormField
315
- control={form.control}
316
- name="industry"
317
- render={({ field }) => (
318
- <FormItem>
319
- <FormLabel>Industry (optional)</FormLabel>
320
- <FormControl>
321
- <Input
322
- placeholder="e.g., Technology, Healthcare"
323
- value={field.value ?? ''}
324
- onChange={(e) => field.onChange(e.target.value || null)}
325
- disabled={isLoading}
326
- />
327
- </FormControl>
328
- <FormMessage />
329
- </FormItem>
330
- )}
331
- />
332
-
333
- <div className="grid gap-4 grid-cols-2">
334
- <FormField
335
- control={form.control}
336
- name="annual_revenue"
337
- render={({ field }) => (
338
- <FormItem>
339
- <FormLabel>Annual Revenue (optional)</FormLabel>
340
- <FormControl>
341
- <Input
342
- type="number"
343
- placeholder="0"
344
- value={field.value ?? ''}
345
- onChange={(e) =>
346
- field.onChange(
347
- e.target.value ? Number(e.target.value) : null
348
- )
349
- }
350
- disabled={isLoading}
351
- />
352
- </FormControl>
353
- <FormMessage />
354
- </FormItem>
355
- )}
356
- />
357
-
358
- <FormField
359
- control={form.control}
360
- name="employee_count"
361
- render={({ field }) => (
362
- <FormItem>
363
- <FormLabel>Employees (optional)</FormLabel>
364
- <FormControl>
365
- <Input
366
- type="number"
367
- placeholder="0"
368
- value={field.value ?? ''}
369
- onChange={(e) =>
370
- field.onChange(
371
- e.target.value ? Number(e.target.value) : null
372
- )
373
- }
374
- disabled={isLoading}
375
- />
376
- </FormControl>
377
- <FormMessage />
378
- </FormItem>
379
- )}
380
- />
381
- </div>
382
-
383
- {/* Address Information */}
384
- <div className="grid gap-4 grid-cols-2">
385
- <FormField
386
- control={form.control}
387
- name="city"
388
- render={({ field }) => (
389
- <FormItem>
390
- <FormLabel>City (optional)</FormLabel>
391
- <FormControl>
392
- <Input
393
- placeholder="São Paulo"
394
- value={field.value ?? ''}
395
- onChange={(e) => field.onChange(e.target.value || null)}
396
- disabled={isLoading}
397
- />
398
- </FormControl>
399
- <FormMessage />
400
- </FormItem>
401
- )}
402
- />
403
-
404
- <FormField
405
- control={form.control}
406
- name="state"
407
- render={({ field }) => (
408
- <FormItem>
409
- <FormLabel>State (optional)</FormLabel>
410
- <FormControl>
411
- <Input
412
- placeholder="SP"
413
- maxLength={2}
414
- value={field.value ?? ''}
415
- onChange={(e) => field.onChange(e.target.value || null)}
416
- disabled={isLoading}
417
- />
418
- </FormControl>
419
- <FormMessage />
420
- </FormItem>
421
- )}
422
- />
423
- </div>
424
-
425
- {/* Owner */}
426
- <FormField
427
- control={form.control}
428
- name="owner_user_id"
429
- render={({ field }) => (
430
- <FormItem>
431
- <FormLabel>Owner (optional)</FormLabel>
432
- <Select
433
- value={field.value ? String(field.value) : ''}
434
- onValueChange={(value) =>
435
- field.onChange(value ? Number(value) : null)
436
- }
437
- disabled={isLoading}
438
- >
439
- <FormControl>
440
- <SelectTrigger>
441
- <SelectValue placeholder="Unassigned" />
442
- </SelectTrigger>
443
- </FormControl>
444
- <SelectContent>
445
- <SelectItem value="">Unassigned</SelectItem>
446
- {owners.map((owner) => (
447
- <SelectItem key={owner.id} value={String(owner.id)}>
448
- {owner.name}
449
- </SelectItem>
450
- ))}
451
- </SelectContent>
452
- </Select>
453
- <FormMessage />
454
- </FormItem>
455
- )}
456
- />
457
-
458
- {/* Submit Button */}
459
- <div className="flex justify-end gap-2 pt-4 border-t">
460
- <Button
461
- type="button"
462
- variant="outline"
463
- onClick={() => onOpenChange(false)}
464
- disabled={isLoading}
465
- >
466
- Cancel
467
- </Button>
468
- <Button type="submit" disabled={isLoading}>
469
- {isLoading ? 'Saving...' : 'Save Account'}
470
- </Button>
471
- </div>
472
- </form>
473
- </Form>
474
- </SheetContent>
475
- </Sheet>
476
- );
477
- }
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Form,
6
+ FormControl,
7
+ FormField,
8
+ FormItem,
9
+ FormLabel,
10
+ FormMessage,
11
+ } from '@/components/ui/form';
12
+ import { Input } from '@/components/ui/input';
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from '@/components/ui/select';
20
+ import {
21
+ Sheet,
22
+ SheetContent,
23
+ SheetDescription,
24
+ SheetHeader,
25
+ SheetTitle,
26
+ } from '@/components/ui/sheet';
27
+ import { zodResolver } from '@hookform/resolvers/zod';
28
+ import { useTranslations } from 'next-intl';
29
+ import { useEffect, useMemo } from 'react';
30
+ import { useForm } from 'react-hook-form';
31
+ import { z } from 'zod';
32
+ import type { Account, AccountFormValues, UserOption } from './account-types';
33
+
34
+ type AccountFormData = {
35
+ name: string;
36
+ trade_name: string | null;
37
+ status: 'active' | 'inactive';
38
+ industry: string | null;
39
+ website: string | null;
40
+ email: string | null;
41
+ phone: string | null;
42
+ owner_user_id: number | null;
43
+ annual_revenue: number | null;
44
+ employee_count: number | null;
45
+ lifecycle_stage: 'prospect' | 'customer' | 'churned' | 'inactive' | null;
46
+ city: string | null;
47
+ state: string | null;
48
+ };
49
+
50
+ interface AccountFormSheetProps {
51
+ open: boolean;
52
+ onOpenChange: (open: boolean) => void;
53
+ account?: Account | null;
54
+ owners: UserOption[];
55
+ onSubmit: (data: AccountFormValues) => Promise<void>;
56
+ isLoading?: boolean;
57
+ }
58
+
59
+ function emptyToNull(value: string | null | undefined) {
60
+ const normalized = String(value ?? '').trim();
61
+ return normalized ? normalized : null;
62
+ }
63
+
64
+ export function AccountFormSheet({
65
+ open,
66
+ onOpenChange,
67
+ account,
68
+ owners,
69
+ onSubmit,
70
+ isLoading = false,
71
+ }: AccountFormSheetProps) {
72
+ const t = useTranslations('contact.AccountsPage');
73
+
74
+ const accountFormSchema = useMemo(
75
+ () =>
76
+ z.object({
77
+ name: z.string().trim().min(2, t('form.validation.nameMinLength')),
78
+ trade_name: z.string().nullable(),
79
+ status: z.enum(['active', 'inactive']),
80
+ industry: z.string().nullable(),
81
+ website: z.string().nullable(),
82
+ email: z
83
+ .string()
84
+ .nullable()
85
+ .refine(
86
+ (value) => !value || z.string().email().safeParse(value).success,
87
+ t('form.validation.invalidEmail')
88
+ ),
89
+ phone: z.string().nullable(),
90
+ owner_user_id: z.number().nullable(),
91
+ annual_revenue: z
92
+ .number()
93
+ .nullable()
94
+ .refine(
95
+ (value) => value == null || value >= 0,
96
+ t('form.validation.nonNegativeNumber')
97
+ ),
98
+ employee_count: z
99
+ .number()
100
+ .int(t('form.validation.integerRequired'))
101
+ .nullable()
102
+ .refine(
103
+ (value) => value == null || value >= 0,
104
+ t('form.validation.nonNegativeNumber')
105
+ ),
106
+ lifecycle_stage: z
107
+ .enum(['prospect', 'customer', 'churned', 'inactive'])
108
+ .nullable(),
109
+ city: z.string().nullable(),
110
+ state: z
111
+ .string()
112
+ .nullable()
113
+ .refine(
114
+ (value) => !value || value.trim().length <= 2,
115
+ t('form.validation.stateMaxLength')
116
+ ),
117
+ }),
118
+ [t]
119
+ );
120
+
121
+ const form = useForm<AccountFormData>({
122
+ resolver: zodResolver(accountFormSchema),
123
+ defaultValues: {
124
+ name: '',
125
+ trade_name: null,
126
+ status: 'active',
127
+ industry: null,
128
+ website: null,
129
+ email: null,
130
+ phone: null,
131
+ owner_user_id: null,
132
+ annual_revenue: null,
133
+ employee_count: null,
134
+ lifecycle_stage: 'prospect',
135
+ city: null,
136
+ state: null,
137
+ },
138
+ });
139
+
140
+ useEffect(() => {
141
+ if (!open) {
142
+ return;
143
+ }
144
+
145
+ if (account) {
146
+ form.reset({
147
+ name: account.name,
148
+ trade_name: account.trade_name ?? null,
149
+ status: account.status,
150
+ industry: account.industry ?? null,
151
+ website: account.website ?? null,
152
+ email: account.email ?? null,
153
+ phone: account.phone ?? null,
154
+ owner_user_id: account.owner_user_id ?? null,
155
+ annual_revenue: account.annual_revenue ?? null,
156
+ employee_count: account.employee_count ?? null,
157
+ lifecycle_stage: account.lifecycle_stage ?? 'prospect',
158
+ city: account.city ?? null,
159
+ state: account.state ?? null,
160
+ });
161
+ return;
162
+ }
163
+
164
+ form.reset({
165
+ name: '',
166
+ trade_name: null,
167
+ status: 'active',
168
+ industry: null,
169
+ website: null,
170
+ email: null,
171
+ phone: null,
172
+ owner_user_id: null,
173
+ annual_revenue: null,
174
+ employee_count: null,
175
+ lifecycle_stage: 'prospect',
176
+ city: null,
177
+ state: null,
178
+ });
179
+ }, [account, form, open]);
180
+
181
+ const handleSubmit = async (data: AccountFormData) => {
182
+ await onSubmit({
183
+ name: data.name.trim(),
184
+ trade_name: emptyToNull(data.trade_name),
185
+ status: data.status,
186
+ industry: emptyToNull(data.industry),
187
+ website: emptyToNull(data.website),
188
+ email: emptyToNull(data.email),
189
+ phone: emptyToNull(data.phone),
190
+ owner_user_id: data.owner_user_id ?? null,
191
+ annual_revenue: data.annual_revenue ?? null,
192
+ employee_count: data.employee_count ?? null,
193
+ lifecycle_stage: data.lifecycle_stage ?? null,
194
+ city: emptyToNull(data.city),
195
+ state: emptyToNull(data.state)?.toUpperCase() ?? null,
196
+ });
197
+ };
198
+
199
+ return (
200
+ <Sheet open={open} onOpenChange={onOpenChange}>
201
+ <SheetContent className="max-h-screen overflow-y-auto">
202
+ <SheetHeader>
203
+ <SheetTitle>
204
+ {account ? t('form.editTitle') : t('form.createTitle')}
205
+ </SheetTitle>
206
+ <SheetDescription>
207
+ {account ? t('form.editDescription') : t('form.createDescription')}
208
+ </SheetDescription>
209
+ </SheetHeader>
210
+
211
+ <Form {...form}>
212
+ <form
213
+ onSubmit={form.handleSubmit(handleSubmit)}
214
+ className="space-y-4 py-4"
215
+ >
216
+ <FormField
217
+ control={form.control}
218
+ name="name"
219
+ render={({ field }) => (
220
+ <FormItem>
221
+ <FormLabel>{t('form.companyName')}</FormLabel>
222
+ <FormControl>
223
+ <Input
224
+ placeholder={t('form.placeholders.companyName')}
225
+ {...field}
226
+ disabled={isLoading}
227
+ />
228
+ </FormControl>
229
+ <FormMessage />
230
+ </FormItem>
231
+ )}
232
+ />
233
+
234
+ <FormField
235
+ control={form.control}
236
+ name="trade_name"
237
+ render={({ field }) => (
238
+ <FormItem>
239
+ <FormLabel>{t('form.tradeName')}</FormLabel>
240
+ <FormControl>
241
+ <Input
242
+ placeholder={t('form.placeholders.tradeName')}
243
+ value={field.value ?? ''}
244
+ onChange={(event) =>
245
+ field.onChange(event.target.value || null)
246
+ }
247
+ disabled={isLoading}
248
+ />
249
+ </FormControl>
250
+ <FormMessage />
251
+ </FormItem>
252
+ )}
253
+ />
254
+
255
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
256
+ <FormField
257
+ control={form.control}
258
+ name="status"
259
+ render={({ field }) => (
260
+ <FormItem>
261
+ <FormLabel>{t('form.status')}</FormLabel>
262
+ <Select
263
+ value={field.value}
264
+ onValueChange={field.onChange}
265
+ disabled={isLoading}
266
+ >
267
+ <FormControl>
268
+ <SelectTrigger>
269
+ <SelectValue />
270
+ </SelectTrigger>
271
+ </FormControl>
272
+ <SelectContent>
273
+ <SelectItem value="active">
274
+ {t('status_active')}
275
+ </SelectItem>
276
+ <SelectItem value="inactive">
277
+ {t('status_inactive')}
278
+ </SelectItem>
279
+ </SelectContent>
280
+ </Select>
281
+ <FormMessage />
282
+ </FormItem>
283
+ )}
284
+ />
285
+
286
+ <FormField
287
+ control={form.control}
288
+ name="lifecycle_stage"
289
+ render={({ field }) => (
290
+ <FormItem>
291
+ <FormLabel>{t('form.lifecycleStage')}</FormLabel>
292
+ <Select
293
+ value={field.value ?? 'prospect'}
294
+ onValueChange={(value) => field.onChange(value || null)}
295
+ disabled={isLoading}
296
+ >
297
+ <FormControl>
298
+ <SelectTrigger>
299
+ <SelectValue />
300
+ </SelectTrigger>
301
+ </FormControl>
302
+ <SelectContent>
303
+ <SelectItem value="prospect">
304
+ {t('stage_prospect')}
305
+ </SelectItem>
306
+ <SelectItem value="customer">
307
+ {t('stage_customer')}
308
+ </SelectItem>
309
+ <SelectItem value="churned">
310
+ {t('stage_churned')}
311
+ </SelectItem>
312
+ <SelectItem value="inactive">
313
+ {t('stage_inactive')}
314
+ </SelectItem>
315
+ </SelectContent>
316
+ </Select>
317
+ <FormMessage />
318
+ </FormItem>
319
+ )}
320
+ />
321
+ </div>
322
+
323
+ <FormField
324
+ control={form.control}
325
+ name="email"
326
+ render={({ field }) => (
327
+ <FormItem>
328
+ <FormLabel>{t('form.email')}</FormLabel>
329
+ <FormControl>
330
+ <Input
331
+ type="email"
332
+ placeholder={t('form.placeholders.email')}
333
+ value={field.value ?? ''}
334
+ onChange={(event) =>
335
+ field.onChange(event.target.value || null)
336
+ }
337
+ disabled={isLoading}
338
+ />
339
+ </FormControl>
340
+ <FormMessage />
341
+ </FormItem>
342
+ )}
343
+ />
344
+
345
+ <FormField
346
+ control={form.control}
347
+ name="phone"
348
+ render={({ field }) => (
349
+ <FormItem>
350
+ <FormLabel>{t('form.phone')}</FormLabel>
351
+ <FormControl>
352
+ <Input
353
+ placeholder={t('form.placeholders.phone')}
354
+ value={field.value ?? ''}
355
+ onChange={(event) =>
356
+ field.onChange(event.target.value || null)
357
+ }
358
+ disabled={isLoading}
359
+ />
360
+ </FormControl>
361
+ <FormMessage />
362
+ </FormItem>
363
+ )}
364
+ />
365
+
366
+ <FormField
367
+ control={form.control}
368
+ name="website"
369
+ render={({ field }) => (
370
+ <FormItem>
371
+ <FormLabel>{t('form.website')}</FormLabel>
372
+ <FormControl>
373
+ <Input
374
+ placeholder={t('form.placeholders.website')}
375
+ value={field.value ?? ''}
376
+ onChange={(event) =>
377
+ field.onChange(event.target.value || null)
378
+ }
379
+ disabled={isLoading}
380
+ />
381
+ </FormControl>
382
+ <FormMessage />
383
+ </FormItem>
384
+ )}
385
+ />
386
+
387
+ <FormField
388
+ control={form.control}
389
+ name="industry"
390
+ render={({ field }) => (
391
+ <FormItem>
392
+ <FormLabel>{t('form.industry')}</FormLabel>
393
+ <FormControl>
394
+ <Input
395
+ placeholder={t('form.placeholders.industry')}
396
+ value={field.value ?? ''}
397
+ onChange={(event) =>
398
+ field.onChange(event.target.value || null)
399
+ }
400
+ disabled={isLoading}
401
+ />
402
+ </FormControl>
403
+ <FormMessage />
404
+ </FormItem>
405
+ )}
406
+ />
407
+
408
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
409
+ <FormField
410
+ control={form.control}
411
+ name="annual_revenue"
412
+ render={({ field }) => (
413
+ <FormItem>
414
+ <FormLabel>{t('form.annualRevenue')}</FormLabel>
415
+ <FormControl>
416
+ <Input
417
+ type="number"
418
+ inputMode="decimal"
419
+ min="0"
420
+ step="0.01"
421
+ placeholder={t('form.placeholders.annualRevenue')}
422
+ value={field.value ?? ''}
423
+ onChange={(event) =>
424
+ field.onChange(
425
+ event.target.value
426
+ ? Number(event.target.value)
427
+ : null
428
+ )
429
+ }
430
+ disabled={isLoading}
431
+ />
432
+ </FormControl>
433
+ <FormMessage />
434
+ </FormItem>
435
+ )}
436
+ />
437
+
438
+ <FormField
439
+ control={form.control}
440
+ name="employee_count"
441
+ render={({ field }) => (
442
+ <FormItem>
443
+ <FormLabel>{t('form.employeeCount')}</FormLabel>
444
+ <FormControl>
445
+ <Input
446
+ type="number"
447
+ inputMode="numeric"
448
+ min="0"
449
+ step="1"
450
+ placeholder={t('form.placeholders.employeeCount')}
451
+ value={field.value ?? ''}
452
+ onChange={(event) =>
453
+ field.onChange(
454
+ event.target.value
455
+ ? Number(event.target.value)
456
+ : null
457
+ )
458
+ }
459
+ disabled={isLoading}
460
+ />
461
+ </FormControl>
462
+ <FormMessage />
463
+ </FormItem>
464
+ )}
465
+ />
466
+ </div>
467
+
468
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
469
+ <FormField
470
+ control={form.control}
471
+ name="city"
472
+ render={({ field }) => (
473
+ <FormItem>
474
+ <FormLabel>{t('form.city')}</FormLabel>
475
+ <FormControl>
476
+ <Input
477
+ placeholder={t('form.placeholders.city')}
478
+ value={field.value ?? ''}
479
+ onChange={(event) =>
480
+ field.onChange(event.target.value || null)
481
+ }
482
+ disabled={isLoading}
483
+ />
484
+ </FormControl>
485
+ <FormMessage />
486
+ </FormItem>
487
+ )}
488
+ />
489
+
490
+ <FormField
491
+ control={form.control}
492
+ name="state"
493
+ render={({ field }) => (
494
+ <FormItem>
495
+ <FormLabel>{t('form.state')}</FormLabel>
496
+ <FormControl>
497
+ <Input
498
+ placeholder={t('form.placeholders.state')}
499
+ maxLength={2}
500
+ value={field.value ?? ''}
501
+ onChange={(event) =>
502
+ field.onChange(event.target.value || null)
503
+ }
504
+ disabled={isLoading}
505
+ />
506
+ </FormControl>
507
+ <FormMessage />
508
+ </FormItem>
509
+ )}
510
+ />
511
+ </div>
512
+
513
+ <FormField
514
+ control={form.control}
515
+ name="owner_user_id"
516
+ render={({ field }) => (
517
+ <FormItem>
518
+ <FormLabel>{t('form.owner')}</FormLabel>
519
+ <Select
520
+ value={
521
+ field.value != null ? String(field.value) : 'unassigned'
522
+ }
523
+ onValueChange={(value) =>
524
+ field.onChange(
525
+ value === 'unassigned' ? null : Number(value)
526
+ )
527
+ }
528
+ disabled={isLoading}
529
+ >
530
+ <FormControl>
531
+ <SelectTrigger>
532
+ <SelectValue placeholder={t('unassigned')} />
533
+ </SelectTrigger>
534
+ </FormControl>
535
+ <SelectContent>
536
+ <SelectItem value="unassigned">
537
+ {t('unassigned')}
538
+ </SelectItem>
539
+ {owners.map((owner) => (
540
+ <SelectItem key={owner.id} value={String(owner.id)}>
541
+ {owner.name}
542
+ </SelectItem>
543
+ ))}
544
+ </SelectContent>
545
+ </Select>
546
+ <FormMessage />
547
+ </FormItem>
548
+ )}
549
+ />
550
+
551
+ <div className="flex justify-end gap-2 border-t pt-4">
552
+ <Button
553
+ type="button"
554
+ variant="outline"
555
+ onClick={() => onOpenChange(false)}
556
+ disabled={isLoading}
557
+ >
558
+ {t('cancel')}
559
+ </Button>
560
+ <Button type="submit" disabled={isLoading}>
561
+ {isLoading
562
+ ? t('form.saving')
563
+ : account
564
+ ? t('form.updateSubmit')
565
+ : t('form.createSubmit')}
566
+ </Button>
567
+ </div>
568
+ </form>
569
+ </Form>
570
+ </SheetContent>
571
+ </Sheet>
572
+ );
573
+ }