@hed-hog/contact 0.0.301 → 0.0.303

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 (36) hide show
  1. package/dist/person/person.service.d.ts +2 -0
  2. package/dist/person/person.service.d.ts.map +1 -1
  3. package/dist/person/person.service.js +111 -127
  4. package/dist/person/person.service.js.map +1 -1
  5. package/dist/person/person.service.spec.d.ts +2 -0
  6. package/dist/person/person.service.spec.d.ts.map +1 -0
  7. package/dist/person/person.service.spec.js +106 -0
  8. package/dist/person/person.service.spec.js.map +1 -0
  9. package/dist/proposal/proposal.service.d.ts +5 -0
  10. package/dist/proposal/proposal.service.d.ts.map +1 -1
  11. package/dist/proposal/proposal.service.js +242 -19
  12. package/dist/proposal/proposal.service.js.map +1 -1
  13. package/dist/proposal/proposal.service.spec.js +153 -165
  14. package/dist/proposal/proposal.service.spec.js.map +1 -1
  15. package/hedhog/data/menu.yaml +35 -18
  16. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +517 -346
  17. package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +42 -17
  18. package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +1 -1
  19. package/hedhog/frontend/app/activities/page.tsx.ejs +315 -101
  20. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +172 -22
  21. package/hedhog/frontend/app/page.tsx.ejs +1 -1
  22. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1 -1
  23. package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +1 -1
  24. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +509 -441
  25. package/hedhog/frontend/app/pipeline/page.tsx.ejs +30 -4
  26. package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +773 -0
  27. package/hedhog/frontend/app/proposals/approvals/page.tsx.ejs +5 -0
  28. package/hedhog/frontend/app/proposals/page.tsx.ejs +5 -0
  29. package/hedhog/frontend/app/reports/page.tsx.ejs +431 -375
  30. package/hedhog/frontend/messages/en.json +100 -1
  31. package/hedhog/frontend/messages/pt.json +100 -1
  32. package/package.json +6 -6
  33. package/src/person/person.service.spec.ts +143 -0
  34. package/src/person/person.service.ts +147 -158
  35. package/src/proposal/proposal.service.spec.ts +196 -0
  36. package/src/proposal/proposal.service.ts +348 -18
@@ -1,6 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { Button } from '@/components/ui/button';
3
+ import {
4
+ Collapsible,
5
+ CollapsibleContent,
6
+ CollapsibleTrigger,
7
+ } from '@/components/ui/collapsible';
8
+ import { FormActions } from '@/components/ui/form-actions';
4
9
  import {
5
10
  Form,
6
11
  FormControl,
@@ -17,6 +22,7 @@ import {
17
22
  SelectTrigger,
18
23
  SelectValue,
19
24
  } from '@/components/ui/select';
25
+ import { Separator } from '@/components/ui/separator';
20
26
  import {
21
27
  Sheet,
22
28
  SheetContent,
@@ -25,9 +31,10 @@ import {
25
31
  SheetTitle,
26
32
  } from '@/components/ui/sheet';
27
33
  import { zodResolver } from '@hookform/resolvers/zod';
34
+ import { Building2, ChevronDown, Globe, MapPin, Users } from 'lucide-react';
28
35
  import { useTranslations } from 'next-intl';
29
- import { useEffect, useMemo } from 'react';
30
- import { useForm } from 'react-hook-form';
36
+ import { type ReactNode, useEffect, useMemo } from 'react';
37
+ import { useForm, useWatch } from 'react-hook-form';
31
38
  import { z } from 'zod';
32
39
  import type { Account, AccountFormValues, UserOption } from './account-types';
33
40
 
@@ -61,6 +68,28 @@ function emptyToNull(value: string | null | undefined) {
61
68
  return normalized ? normalized : null;
62
69
  }
63
70
 
71
+ function SectionHeader({
72
+ title,
73
+ description,
74
+ icon,
75
+ }: {
76
+ title: string;
77
+ description: string;
78
+ icon: ReactNode;
79
+ }) {
80
+ return (
81
+ <div className="flex items-start gap-2.5">
82
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border bg-background text-muted-foreground">
83
+ {icon}
84
+ </div>
85
+ <div className="space-y-0.5">
86
+ <h3 className="text-sm font-semibold text-foreground">{title}</h3>
87
+ <p className="text-xs text-muted-foreground">{description}</p>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
64
93
  export function AccountFormSheet({
65
94
  open,
66
95
  onOpenChange,
@@ -196,375 +225,517 @@ export function AccountFormSheet({
196
225
  });
197
226
  };
198
227
 
228
+ const watchedName = useWatch({
229
+ control: form.control,
230
+ name: 'name',
231
+ });
232
+ const additionalDetailsDefaultOpen = Boolean(
233
+ account?.industry ||
234
+ account?.annual_revenue != null ||
235
+ account?.employee_count != null
236
+ );
237
+
238
+ const companyPreviewName =
239
+ watchedName?.trim() || account?.name || t('form.summary.newCompany');
240
+
241
+ const handleSheetOpenChange = (nextOpen: boolean) => {
242
+ if (isLoading && !nextOpen) {
243
+ return;
244
+ }
245
+
246
+ onOpenChange(nextOpen);
247
+ };
248
+
199
249
  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>
250
+ <Sheet open={open} onOpenChange={handleSheetOpenChange}>
251
+ <SheetContent className="flex h-full w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-lg">
252
+ <SheetHeader className="shrink-0 border-b px-4 py-4 text-left">
253
+ <div className="flex items-start gap-3">
254
+ <div
255
+ className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl ${
256
+ account
257
+ ? 'bg-amber-500/10 text-amber-600'
258
+ : 'bg-blue-500/10 text-blue-600'
259
+ }`}
260
+ >
261
+ <Building2 className="h-5 w-5" />
262
+ </div>
263
+ <div className="min-w-0 space-y-1">
264
+ <SheetTitle>
265
+ {account ? t('form.editTitle') : t('form.createTitle')}
266
+ </SheetTitle>
267
+ <SheetDescription>
268
+ {account
269
+ ? t('form.editDescription')
270
+ : t('form.createDescription')}
271
+ </SheetDescription>
272
+ </div>
273
+ </div>
209
274
  </SheetHeader>
210
275
 
211
276
  <Form {...form}>
212
277
  <form
213
278
  onSubmit={form.handleSubmit(handleSubmit)}
214
- className="space-y-4 py-4"
279
+ className="flex h-full flex-col [&_button[role='combobox']]:h-9 [&_input]:h-9 [&_label]:text-xs [&_label]:font-medium"
215
280
  >
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}
281
+ <div className="flex-1 overflow-y-auto">
282
+ <div className="space-y-5 px-4 py-4">
283
+ <div className="rounded-xl border bg-muted/20 p-3">
284
+ <p className="truncate text-sm font-semibold text-foreground">
285
+ {companyPreviewName}
286
+ </p>
287
+ <p className="mt-1 text-xs text-muted-foreground">
288
+ {account
289
+ ? t('form.summary.editHint')
290
+ : t('form.summary.createHint')}
291
+ </p>
292
+ </div>
293
+
294
+ <section className="space-y-3">
295
+ <SectionHeader
296
+ title={t('form.sections.identityTitle')}
297
+ description={t('form.sections.identityDescription')}
298
+ icon={<Building2 className="h-4 w-4" />}
299
+ />
300
+
301
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
302
+ <FormField
303
+ control={form.control}
304
+ name="name"
305
+ render={({ field }) => (
306
+ <FormItem className="space-y-1.5 sm:col-span-2">
307
+ <FormLabel>{t('form.companyName')}</FormLabel>
308
+ <FormControl>
309
+ <Input
310
+ placeholder={t('form.placeholders.companyName')}
311
+ autoFocus={open}
312
+ autoComplete="organization"
313
+ {...field}
314
+ disabled={isLoading}
315
+ />
316
+ </FormControl>
317
+ <FormMessage />
318
+ </FormItem>
319
+ )}
227
320
  />
228
- </FormControl>
229
- <FormMessage />
230
- </FormItem>
231
- )}
232
- />
233
321
 
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}
322
+ <FormField
323
+ control={form.control}
324
+ name="trade_name"
325
+ render={({ field }) => (
326
+ <FormItem className="space-y-1.5 sm:col-span-2">
327
+ <FormLabel>{t('form.tradeName')}</FormLabel>
328
+ <FormControl>
329
+ <Input
330
+ placeholder={t('form.placeholders.tradeName')}
331
+ autoComplete="organization-title"
332
+ value={field.value ?? ''}
333
+ onChange={(event) =>
334
+ field.onChange(event.target.value || null)
335
+ }
336
+ disabled={isLoading}
337
+ />
338
+ </FormControl>
339
+ <FormMessage />
340
+ </FormItem>
341
+ )}
248
342
  />
249
- </FormControl>
250
- <FormMessage />
251
- </FormItem>
252
- )}
253
- />
343
+ </div>
344
+ </section>
254
345
 
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>
346
+ <section className="space-y-3">
347
+ <SectionHeader
348
+ title={t('form.sections.relationshipTitle')}
349
+ description={t('form.sections.relationshipDescription')}
350
+ icon={<Users className="h-4 w-4" />}
351
+ />
322
352
 
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}
353
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
354
+ <FormField
355
+ control={form.control}
356
+ name="status"
357
+ render={({ field }) => (
358
+ <FormItem className="space-y-1.5">
359
+ <FormLabel>{t('form.status')}</FormLabel>
360
+ <Select
361
+ value={field.value}
362
+ onValueChange={field.onChange}
363
+ disabled={isLoading}
364
+ >
365
+ <FormControl>
366
+ <SelectTrigger className="w-full">
367
+ <SelectValue />
368
+ </SelectTrigger>
369
+ </FormControl>
370
+ <SelectContent>
371
+ <SelectItem value="active">
372
+ {t('status_active')}
373
+ </SelectItem>
374
+ <SelectItem value="inactive">
375
+ {t('status_inactive')}
376
+ </SelectItem>
377
+ </SelectContent>
378
+ </Select>
379
+ <FormMessage />
380
+ </FormItem>
381
+ )}
338
382
  />
339
- </FormControl>
340
- <FormMessage />
341
- </FormItem>
342
- )}
343
- />
344
383
 
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}
384
+ <FormField
385
+ control={form.control}
386
+ name="lifecycle_stage"
387
+ render={({ field }) => (
388
+ <FormItem className="space-y-1.5">
389
+ <FormLabel>{t('form.lifecycleStage')}</FormLabel>
390
+ <Select
391
+ value={field.value ?? 'prospect'}
392
+ onValueChange={(value) =>
393
+ field.onChange(value || null)
394
+ }
395
+ disabled={isLoading}
396
+ >
397
+ <FormControl>
398
+ <SelectTrigger className="w-full">
399
+ <SelectValue />
400
+ </SelectTrigger>
401
+ </FormControl>
402
+ <SelectContent>
403
+ <SelectItem value="prospect">
404
+ {t('stage_prospect')}
405
+ </SelectItem>
406
+ <SelectItem value="customer">
407
+ {t('stage_customer')}
408
+ </SelectItem>
409
+ <SelectItem value="churned">
410
+ {t('stage_churned')}
411
+ </SelectItem>
412
+ <SelectItem value="inactive">
413
+ {t('stage_inactive')}
414
+ </SelectItem>
415
+ </SelectContent>
416
+ </Select>
417
+ <FormMessage />
418
+ </FormItem>
419
+ )}
359
420
  />
360
- </FormControl>
361
- <FormMessage />
362
- </FormItem>
363
- )}
364
- />
365
421
 
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}
422
+ <FormField
423
+ control={form.control}
424
+ name="owner_user_id"
425
+ render={({ field }) => (
426
+ <FormItem className="space-y-1.5 sm:col-span-2">
427
+ <FormLabel>{t('form.owner')}</FormLabel>
428
+ <Select
429
+ value={
430
+ field.value != null
431
+ ? String(field.value)
432
+ : 'unassigned'
433
+ }
434
+ onValueChange={(value) =>
435
+ field.onChange(
436
+ value === 'unassigned' ? null : Number(value)
437
+ )
438
+ }
439
+ disabled={isLoading}
440
+ >
441
+ <FormControl>
442
+ <SelectTrigger className="w-full">
443
+ <SelectValue placeholder={t('unassigned')} />
444
+ </SelectTrigger>
445
+ </FormControl>
446
+ <SelectContent>
447
+ <SelectItem value="unassigned">
448
+ {t('unassigned')}
449
+ </SelectItem>
450
+ {owners.map((owner) => (
451
+ <SelectItem
452
+ key={owner.id}
453
+ value={String(owner.id)}
454
+ >
455
+ {owner.name}
456
+ </SelectItem>
457
+ ))}
458
+ </SelectContent>
459
+ </Select>
460
+ <FormMessage />
461
+ </FormItem>
462
+ )}
380
463
  />
381
- </FormControl>
382
- <FormMessage />
383
- </FormItem>
384
- )}
385
- />
464
+ </div>
465
+ </section>
466
+
467
+ <section className="space-y-3">
468
+ <SectionHeader
469
+ title={t('form.sections.contactTitle')}
470
+ description={t('form.sections.contactDescription')}
471
+ icon={<Globe className="h-4 w-4" />}
472
+ />
386
473
 
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}
474
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
475
+ <FormField
476
+ control={form.control}
477
+ name="email"
478
+ render={({ field }) => (
479
+ <FormItem className="space-y-1.5">
480
+ <FormLabel>{t('form.email')}</FormLabel>
481
+ <FormControl>
482
+ <Input
483
+ type="email"
484
+ placeholder={t('form.placeholders.email')}
485
+ autoComplete="email"
486
+ value={field.value ?? ''}
487
+ onChange={(event) =>
488
+ field.onChange(event.target.value || null)
489
+ }
490
+ disabled={isLoading}
491
+ />
492
+ </FormControl>
493
+ <FormMessage />
494
+ </FormItem>
495
+ )}
401
496
  />
402
- </FormControl>
403
- <FormMessage />
404
- </FormItem>
405
- )}
406
- />
407
497
 
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>
498
+ <FormField
499
+ control={form.control}
500
+ name="phone"
501
+ render={({ field }) => (
502
+ <FormItem className="space-y-1.5">
503
+ <FormLabel>{t('form.phone')}</FormLabel>
504
+ <FormControl>
505
+ <Input
506
+ type="tel"
507
+ placeholder={t('form.placeholders.phone')}
508
+ autoComplete="tel"
509
+ value={field.value ?? ''}
510
+ onChange={(event) =>
511
+ field.onChange(event.target.value || null)
512
+ }
513
+ disabled={isLoading}
514
+ />
515
+ </FormControl>
516
+ <FormMessage />
517
+ </FormItem>
518
+ )}
519
+ />
467
520
 
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>
521
+ <FormField
522
+ control={form.control}
523
+ name="website"
524
+ render={({ field }) => (
525
+ <FormItem className="space-y-1.5 sm:col-span-2">
526
+ <FormLabel>{t('form.website')}</FormLabel>
527
+ <FormControl>
528
+ <Input
529
+ placeholder={t('form.placeholders.website')}
530
+ autoCapitalize="none"
531
+ autoCorrect="off"
532
+ value={field.value ?? ''}
533
+ onChange={(event) =>
534
+ field.onChange(event.target.value || null)
535
+ }
536
+ disabled={isLoading}
537
+ />
538
+ </FormControl>
539
+ <FormMessage />
540
+ </FormItem>
541
+ )}
542
+ />
543
+ </div>
544
+ </section>
512
545
 
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
- />
546
+ <section className="space-y-3">
547
+ <SectionHeader
548
+ title={t('form.sections.locationTitle')}
549
+ description={t('form.sections.locationDescription')}
550
+ icon={<MapPin className="h-4 w-4" />}
551
+ />
552
+
553
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
554
+ <FormField
555
+ control={form.control}
556
+ name="city"
557
+ render={({ field }) => (
558
+ <FormItem className="space-y-1.5">
559
+ <FormLabel>{t('form.city')}</FormLabel>
560
+ <FormControl>
561
+ <Input
562
+ placeholder={t('form.placeholders.city')}
563
+ autoComplete="address-level2"
564
+ value={field.value ?? ''}
565
+ onChange={(event) =>
566
+ field.onChange(event.target.value || null)
567
+ }
568
+ disabled={isLoading}
569
+ />
570
+ </FormControl>
571
+ <FormMessage />
572
+ </FormItem>
573
+ )}
574
+ />
550
575
 
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
576
+ <FormField
577
+ control={form.control}
578
+ name="state"
579
+ render={({ field }) => (
580
+ <FormItem className="space-y-1.5">
581
+ <FormLabel>{t('form.state')}</FormLabel>
582
+ <FormControl>
583
+ <Input
584
+ placeholder={t('form.placeholders.state')}
585
+ autoComplete="address-level1"
586
+ className="uppercase"
587
+ maxLength={2}
588
+ value={field.value ?? ''}
589
+ onChange={(event) =>
590
+ field.onChange(
591
+ event.target.value.toUpperCase() || null
592
+ )
593
+ }
594
+ disabled={isLoading}
595
+ />
596
+ </FormControl>
597
+ <FormMessage />
598
+ </FormItem>
599
+ )}
600
+ />
601
+ </div>
602
+ </section>
603
+
604
+ <Separator />
605
+
606
+ <Collapsible defaultOpen={additionalDetailsDefaultOpen}>
607
+ <div className="rounded-xl border border-dashed bg-muted/10">
608
+ <CollapsibleTrigger asChild>
609
+ <button
610
+ type="button"
611
+ className="group flex w-full items-center justify-between gap-3 px-3 py-3 text-left transition-colors hover:bg-muted/20"
612
+ >
613
+ <div>
614
+ <p className="text-sm font-semibold text-foreground">
615
+ {t('form.sections.additionalTitle')}
616
+ </p>
617
+ <p className="text-xs text-muted-foreground">
618
+ {t('form.sections.additionalDescription')}
619
+ </p>
620
+ </div>
621
+
622
+ <div className="flex items-center text-muted-foreground">
623
+ <ChevronDown className="h-4 w-4 transition-transform group-data-[state=open]:rotate-180" />
624
+ </div>
625
+ </button>
626
+ </CollapsibleTrigger>
627
+
628
+ <CollapsibleContent className="border-t px-3 pb-3 pt-3">
629
+ <div className="space-y-3">
630
+ <FormField
631
+ control={form.control}
632
+ name="industry"
633
+ render={({ field }) => (
634
+ <FormItem className="space-y-1.5">
635
+ <FormLabel>{t('form.industry')}</FormLabel>
636
+ <FormControl>
637
+ <Input
638
+ placeholder={t('form.placeholders.industry')}
639
+ value={field.value ?? ''}
640
+ onChange={(event) =>
641
+ field.onChange(event.target.value || null)
642
+ }
643
+ disabled={isLoading}
644
+ />
645
+ </FormControl>
646
+ <FormMessage />
647
+ </FormItem>
648
+ )}
649
+ />
650
+
651
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
652
+ <FormField
653
+ control={form.control}
654
+ name="annual_revenue"
655
+ render={({ field }) => (
656
+ <FormItem className="space-y-1.5">
657
+ <FormLabel>{t('form.annualRevenue')}</FormLabel>
658
+ <FormControl>
659
+ <Input
660
+ type="number"
661
+ inputMode="decimal"
662
+ min="0"
663
+ step="0.01"
664
+ placeholder={t(
665
+ 'form.placeholders.annualRevenue'
666
+ )}
667
+ value={field.value ?? ''}
668
+ onChange={(event) =>
669
+ field.onChange(
670
+ event.target.value
671
+ ? Number(event.target.value)
672
+ : null
673
+ )
674
+ }
675
+ disabled={isLoading}
676
+ />
677
+ </FormControl>
678
+ <FormMessage />
679
+ </FormItem>
680
+ )}
681
+ />
682
+
683
+ <FormField
684
+ control={form.control}
685
+ name="employee_count"
686
+ render={({ field }) => (
687
+ <FormItem className="space-y-1.5">
688
+ <FormLabel>{t('form.employeeCount')}</FormLabel>
689
+ <FormControl>
690
+ <Input
691
+ type="number"
692
+ inputMode="numeric"
693
+ min="0"
694
+ step="1"
695
+ placeholder={t(
696
+ 'form.placeholders.employeeCount'
697
+ )}
698
+ value={field.value ?? ''}
699
+ onChange={(event) =>
700
+ field.onChange(
701
+ event.target.value
702
+ ? Number(event.target.value)
703
+ : null
704
+ )
705
+ }
706
+ disabled={isLoading}
707
+ />
708
+ </FormControl>
709
+ <FormMessage />
710
+ </FormItem>
711
+ )}
712
+ />
713
+ </div>
714
+ </div>
715
+ </CollapsibleContent>
716
+ </div>
717
+ </Collapsible>
718
+ </div>
719
+ </div>
720
+
721
+ <FormActions
722
+ sheet
723
+ cancelLabel={t('cancel')}
724
+ onCancel={() => {
725
+ if (!isLoading) {
726
+ onOpenChange(false);
727
+ }
728
+ }}
729
+ submitDisabled={isLoading}
730
+ submitLabel={
731
+ isLoading
562
732
  ? t('form.saving')
563
733
  : account
564
734
  ? t('form.updateSubmit')
565
- : t('form.createSubmit')}
566
- </Button>
567
- </div>
735
+ : t('form.createSubmit')
736
+ }
737
+ submitType="submit"
738
+ />
568
739
  </form>
569
740
  </Form>
570
741
  </SheetContent>