@hed-hog/contact 0.0.295 → 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 (42) hide show
  1. package/dist/person/dto/reports-query.dto.d.ts +8 -0
  2. package/dist/person/dto/reports-query.dto.d.ts.map +1 -0
  3. package/dist/person/dto/reports-query.dto.js +33 -0
  4. package/dist/person/dto/reports-query.dto.js.map +1 -0
  5. package/dist/person/person.controller.d.ts +65 -8
  6. package/dist/person/person.controller.d.ts.map +1 -1
  7. package/dist/person/person.controller.js +26 -6
  8. package/dist/person/person.controller.js.map +1 -1
  9. package/dist/person/person.service.d.ts +61 -5
  10. package/dist/person/person.service.d.ts.map +1 -1
  11. package/dist/person/person.service.js +656 -298
  12. package/dist/person/person.service.js.map +1 -1
  13. package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
  14. package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
  15. package/hedhog/data/menu.yaml +163 -163
  16. package/hedhog/data/route.yaml +68 -60
  17. package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +9 -9
  18. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +573 -573
  19. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +9 -9
  20. package/hedhog/frontend/app/accounts/page.tsx.ejs +970 -970
  21. package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +240 -240
  22. package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +66 -66
  23. package/hedhog/frontend/app/activities/page.tsx.ejs +460 -460
  24. package/hedhog/frontend/app/dashboard/_components/dashboard-types.ts.ejs +70 -70
  25. package/hedhog/frontend/app/dashboard/page.tsx.ejs +639 -639
  26. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +785 -785
  27. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +10 -12
  28. package/hedhog/frontend/app/reports/_components/report-types.ts.ejs +84 -0
  29. package/hedhog/frontend/app/reports/page.tsx.ejs +1196 -15
  30. package/hedhog/frontend/messages/en.json +242 -123
  31. package/hedhog/frontend/messages/pt.json +242 -123
  32. package/hedhog/table/crm_activity.yaml +68 -68
  33. package/hedhog/table/crm_stage_history.yaml +34 -0
  34. package/hedhog/table/person_company.yaml +27 -27
  35. package/package.json +9 -9
  36. package/src/person/dto/account.dto.ts +100 -100
  37. package/src/person/dto/activity.dto.ts +54 -54
  38. package/src/person/dto/dashboard-query.dto.ts +25 -25
  39. package/src/person/dto/followup-query.dto.ts +25 -25
  40. package/src/person/dto/reports-query.dto.ts +25 -0
  41. package/src/person/person.controller.ts +176 -159
  42. package/src/person/person.service.ts +4825 -4288
@@ -1,573 +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 { 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
- }
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
+ }