@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
@@ -10,6 +10,7 @@ import {
10
10
  } from '@/components/entity-list';
11
11
  import { Badge } from '@/components/ui/badge';
12
12
  import { Button } from '@/components/ui/button';
13
+ import { Card, CardContent } from '@/components/ui/card';
13
14
  import {
14
15
  Command,
15
16
  CommandEmpty,
@@ -51,6 +52,7 @@ import {
51
52
  TableRow,
52
53
  } from '@/components/ui/table';
53
54
  import { Textarea } from '@/components/ui/textarea';
55
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
54
56
  import { formatDateTime } from '@/lib/format-date';
55
57
  import { cn } from '@/lib/utils';
56
58
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
@@ -62,6 +64,8 @@ import {
62
64
  CheckCircle2,
63
65
  ChevronsUpDown,
64
66
  Clock3,
67
+ LayoutGrid,
68
+ List,
65
69
  Loader2,
66
70
  Plus,
67
71
  RotateCw,
@@ -117,6 +121,10 @@ type FollowupStats = {
117
121
  upcoming: number;
118
122
  };
119
123
 
124
+ type FollowupViewMode = 'table' | 'cards';
125
+
126
+ const FOLLOWUPS_VIEW_STORAGE_KEY = 'contact-followups-view-mode';
127
+
120
128
  function toInputDateTimeValue(value?: string | null) {
121
129
  if (!value) {
122
130
  return '';
@@ -185,6 +193,7 @@ export default function CrmFollowupsPage() {
185
193
  const [dateTo, setDateTo] = useState('');
186
194
  const [page, setPage] = useState(1);
187
195
  const [pageSize, setPageSize] = useState(12);
196
+ const [viewMode, setViewMode] = useState<FollowupViewMode>('table');
188
197
  const [sheetOpen, setSheetOpen] = useState(false);
189
198
  const [personPickerOpen, setPersonPickerOpen] = useState(false);
190
199
  const [isSubmitting, setIsSubmitting] = useState(false);
@@ -205,6 +214,19 @@ export default function CrmFollowupsPage() {
205
214
  return () => clearTimeout(timeout);
206
215
  }, [personSearch]);
207
216
 
217
+ useEffect(() => {
218
+ try {
219
+ const savedViewMode = window.localStorage.getItem(
220
+ FOLLOWUPS_VIEW_STORAGE_KEY
221
+ );
222
+ if (savedViewMode === 'table' || savedViewMode === 'cards') {
223
+ setViewMode(savedViewMode);
224
+ }
225
+ } catch {
226
+ // Ignore storage read failures.
227
+ }
228
+ }, []);
229
+
208
230
  const {
209
231
  data: stats = {
210
232
  total: 0,
@@ -475,6 +497,19 @@ export default function CrmFollowupsPage() {
475
497
  },
476
498
  ];
477
499
 
500
+ const handleViewModeChange = (value: string) => {
501
+ if (value !== 'table' && value !== 'cards') {
502
+ return;
503
+ }
504
+
505
+ setViewMode(value);
506
+ try {
507
+ window.localStorage.setItem(FOLLOWUPS_VIEW_STORAGE_KEY, value);
508
+ } catch {
509
+ // Ignore storage write failures.
510
+ }
511
+ };
512
+
478
513
  return (
479
514
  <Page>
480
515
  <PageHeader
@@ -495,22 +530,59 @@ export default function CrmFollowupsPage() {
495
530
  />
496
531
 
497
532
  <div className="space-y-6">
498
- <KpiCardsGrid items={statsCards} />
499
-
500
- <SearchBar
501
- searchQuery={searchInput}
502
- onSearchChange={(value) => {
503
- setSearchInput(value);
504
- setPage(1);
505
- }}
506
- onSearch={() => {
507
- setDebouncedSearch(searchInput.trim());
508
- setPage(1);
509
- void Promise.all([refetchFollowups(), refetchStats()]);
510
- }}
511
- placeholder={t('filters.searchPlaceholder')}
512
- controls={searchControls}
513
- />
533
+ <KpiCardsGrid items={statsCards} className="mb-4" />
534
+
535
+ <div className="flex flex-col gap-4 xl:flex-row xl:items-center mb-4">
536
+ <div className="flex-1">
537
+ <SearchBar
538
+ searchQuery={searchInput}
539
+ onSearchChange={(value) => {
540
+ setSearchInput(value);
541
+ setPage(1);
542
+ }}
543
+ onSearch={() => {
544
+ setDebouncedSearch(searchInput.trim());
545
+ setPage(1);
546
+ void Promise.all([refetchFollowups(), refetchStats()]);
547
+ }}
548
+ placeholder={t('filters.searchPlaceholder')}
549
+ controls={searchControls}
550
+ />
551
+ </div>
552
+
553
+ <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap xl:justify-end">
554
+ <div className="flex items-center justify-between gap-3 sm:justify-start">
555
+ <span className="text-xs font-medium text-muted-foreground">
556
+ {t('viewMode')}
557
+ </span>
558
+ <ToggleGroup
559
+ type="single"
560
+ value={viewMode}
561
+ onValueChange={handleViewModeChange}
562
+ variant="outline"
563
+ size="sm"
564
+ aria-label={t('viewMode')}
565
+ >
566
+ <ToggleGroupItem
567
+ value="table"
568
+ className="gap-1.5 px-2.5"
569
+ aria-label={t('viewModeTable')}
570
+ >
571
+ <List className="h-4 w-4" />
572
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
573
+ </ToggleGroupItem>
574
+ <ToggleGroupItem
575
+ value="cards"
576
+ className="gap-1.5 px-2.5"
577
+ aria-label={t('viewModeCards')}
578
+ >
579
+ <LayoutGrid className="h-4 w-4" />
580
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
581
+ </ToggleGroupItem>
582
+ </ToggleGroup>
583
+ </div>
584
+ </div>
585
+ </div>
514
586
 
515
587
  {isLoading ? (
516
588
  <div className="space-y-2">
@@ -527,7 +599,7 @@ export default function CrmFollowupsPage() {
527
599
  actionIcon={<Plus className="mr-2 h-4 w-4" />}
528
600
  onAction={openCreateSheet}
529
601
  />
530
- ) : (
602
+ ) : viewMode === 'table' ? (
531
603
  <div className="overflow-x-auto rounded-md border">
532
604
  <Table>
533
605
  <TableHeader>
@@ -546,7 +618,7 @@ export default function CrmFollowupsPage() {
546
618
  {paginate.data.map((row) => (
547
619
  <TableRow key={`${row.person.id}-${row.next_action_at}`}>
548
620
  <TableCell>
549
- <div className="min-w-[180px]">
621
+ <div className="min-w-45">
550
622
  <div className="font-medium">{row.person.name}</div>
551
623
  <div className="text-xs text-muted-foreground">
552
624
  #{row.person.id}
@@ -604,6 +676,86 @@ export default function CrmFollowupsPage() {
604
676
  </TableBody>
605
677
  </Table>
606
678
  </div>
679
+ ) : (
680
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
681
+ {paginate.data.map((row) => (
682
+ <Card
683
+ key={`${row.person.id}-${row.next_action_at}`}
684
+ className="h-full overflow-hidden border-border/70 py-0"
685
+ >
686
+ <CardContent className="space-y-3 p-4">
687
+ <div className="flex items-start justify-between gap-3">
688
+ <div className="min-w-0 space-y-1">
689
+ <p className="line-clamp-2 text-sm font-semibold text-foreground">
690
+ {row.person.name}
691
+ </p>
692
+ <p className="text-xs text-muted-foreground">
693
+ #{row.person.id}
694
+ </p>
695
+ </div>
696
+ <Badge
697
+ variant="outline"
698
+ className={cn(
699
+ 'shrink-0 border',
700
+ getStatusBadgeClass(row.status)
701
+ )}
702
+ >
703
+ {t(`status.${row.status}`)}
704
+ </Badge>
705
+ </div>
706
+
707
+ <div className="grid gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-xs">
708
+ <div className="flex items-center justify-between gap-2">
709
+ <span className="text-muted-foreground">
710
+ {t('table.owner')}
711
+ </span>
712
+ <span className="truncate font-medium text-foreground">
713
+ {row.person.owner_user?.name || t('unassigned')}
714
+ </span>
715
+ </div>
716
+ <div className="flex items-center justify-between gap-2">
717
+ <span className="text-muted-foreground">
718
+ {t('table.nextAction')}
719
+ </span>
720
+ <span className="font-medium text-foreground">
721
+ {formatDateTime(
722
+ row.next_action_at,
723
+ getSettingValue,
724
+ currentLocaleCode
725
+ )}
726
+ </span>
727
+ </div>
728
+ <div className="flex items-center justify-between gap-2">
729
+ <span className="text-muted-foreground">
730
+ {t('table.lastInteraction')}
731
+ </span>
732
+ <span className="font-medium text-foreground">
733
+ {row.last_interaction_at
734
+ ? formatDateTime(
735
+ row.last_interaction_at,
736
+ getSettingValue,
737
+ currentLocaleCode
738
+ )
739
+ : '-'}
740
+ </span>
741
+ </div>
742
+ </div>
743
+
744
+ <div className="flex justify-end">
745
+ <Button
746
+ type="button"
747
+ size="sm"
748
+ variant="outline"
749
+ onClick={() => openRescheduleSheet(row)}
750
+ >
751
+ <RotateCw className="mr-2 h-3.5 w-3.5" />
752
+ {t('reschedule')}
753
+ </Button>
754
+ </div>
755
+ </CardContent>
756
+ </Card>
757
+ ))}
758
+ </div>
607
759
  )}
608
760
 
609
761
  <div className="border-t p-4">
@@ -640,7 +792,7 @@ export default function CrmFollowupsPage() {
640
792
  <Form {...form}>
641
793
  <form
642
794
  onSubmit={form.handleSubmit(handleSubmit)}
643
- className="mt-6 flex h-full flex-col gap-4"
795
+ className="mt-6 flex h-full flex-col gap-4 px-4"
644
796
  >
645
797
  <FormField
646
798
  control={form.control}
@@ -708,9 +860,7 @@ export default function CrmFollowupsPage() {
708
860
  <Check
709
861
  className={cn(
710
862
  'mr-2 h-4 w-4',
711
- isSelected
712
- ? 'opacity-100'
713
- : 'opacity-0'
863
+ isSelected ? 'opacity-100' : 'opacity-0'
714
864
  )}
715
865
  />
716
866
  <span className="truncate">
@@ -1,5 +1,5 @@
1
1
  import { redirect } from 'next/navigation';
2
2
 
3
3
  export default function ContactPage() {
4
- redirect('/contact/dashboard');
4
+ redirect('/contact/person');
5
5
  }
@@ -1421,7 +1421,7 @@ export function PersonFormSheet({
1421
1421
  return (
1422
1422
  <>
1423
1423
  <Sheet open={open} onOpenChange={handleSheetOpenChange}>
1424
- <SheetContent className="flex h-full w-full max-w-full flex-col overflow-hidden p-0 lg:max-w-4xl xl:max-w-5xl">
1424
+ <SheetContent className="flex h-full w-full max-w-[95vw] flex-col overflow-hidden p-0 sm:max-w-3xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl">
1425
1425
  <SheetHeader className="shrink-0 border-b p-4">
1426
1426
  <div className="flex items-center gap-3">
1427
1427
  <div
@@ -306,7 +306,7 @@ export function LeadDetailSheet({
306
306
 
307
307
  return (
308
308
  <Sheet open={open} onOpenChange={handleSheetOpenChange}>
309
- <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-lg">
309
+ <SheetContent className="flex h-full w-full max-w-[95vw] flex-col overflow-hidden p-0 sm:max-w-4xl xl:max-w-5xl">
310
310
  <SheetHeader className="shrink-0 border-b px-5 py-4 text-left">
311
311
  <div className="flex items-start gap-3 pr-6">
312
312
  <div className="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-muted text-muted-foreground">