@hed-hog/contact 0.0.279 → 0.0.285

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 (70) hide show
  1. package/README.md +2 -0
  2. package/dist/person/dto/create-followup.dto.d.ts +5 -0
  3. package/dist/person/dto/create-followup.dto.d.ts.map +1 -0
  4. package/dist/person/dto/create-followup.dto.js +31 -0
  5. package/dist/person/dto/create-followup.dto.js.map +1 -0
  6. package/dist/person/dto/create-interaction.dto.d.ts +12 -0
  7. package/dist/person/dto/create-interaction.dto.d.ts.map +1 -0
  8. package/dist/person/dto/create-interaction.dto.js +39 -0
  9. package/dist/person/dto/create-interaction.dto.js.map +1 -0
  10. package/dist/person/dto/create.dto.d.ts +24 -0
  11. package/dist/person/dto/create.dto.d.ts.map +1 -1
  12. package/dist/person/dto/create.dto.js +56 -1
  13. package/dist/person/dto/create.dto.js.map +1 -1
  14. package/dist/person/dto/duplicates-query.dto.d.ts +8 -0
  15. package/dist/person/dto/duplicates-query.dto.d.ts.map +1 -0
  16. package/dist/person/dto/duplicates-query.dto.js +45 -0
  17. package/dist/person/dto/duplicates-query.dto.js.map +1 -0
  18. package/dist/person/dto/merge.dto.d.ts +6 -0
  19. package/dist/person/dto/merge.dto.d.ts.map +1 -0
  20. package/dist/person/dto/merge.dto.js +35 -0
  21. package/dist/person/dto/merge.dto.js.map +1 -0
  22. package/dist/person/dto/update-lifecycle-stage.dto.d.ts +13 -0
  23. package/dist/person/dto/update-lifecycle-stage.dto.d.ts.map +1 -0
  24. package/dist/person/dto/update-lifecycle-stage.dto.js +34 -0
  25. package/dist/person/dto/update-lifecycle-stage.dto.js.map +1 -0
  26. package/dist/person/dto/update.dto.d.ts +8 -1
  27. package/dist/person/dto/update.dto.d.ts.map +1 -1
  28. package/dist/person/dto/update.dto.js +36 -0
  29. package/dist/person/dto/update.dto.js.map +1 -1
  30. package/dist/person/person.controller.d.ts +57 -1
  31. package/dist/person/person.controller.d.ts.map +1 -1
  32. package/dist/person/person.controller.js +85 -3
  33. package/dist/person/person.controller.js.map +1 -1
  34. package/dist/person/person.service.d.ts +79 -0
  35. package/dist/person/person.service.d.ts.map +1 -1
  36. package/dist/person/person.service.js +730 -9
  37. package/dist/person/person.service.js.map +1 -1
  38. package/hedhog/data/route.yaml +18 -0
  39. package/hedhog/frontend/app/_components/crm-coming-soon.tsx.ejs +110 -110
  40. package/hedhog/frontend/app/_components/crm-nav.tsx.ejs +73 -73
  41. package/hedhog/frontend/app/_lib/crm-mocks.ts.ejs +498 -256
  42. package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +81 -81
  43. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +477 -0
  44. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +62 -0
  45. package/hedhog/frontend/app/accounts/page.tsx.ejs +886 -15
  46. package/hedhog/frontend/app/activities/page.tsx.ejs +15 -15
  47. package/hedhog/frontend/app/contact-type/page.tsx.ejs +105 -91
  48. package/hedhog/frontend/app/dashboard/page.tsx.ejs +506 -573
  49. package/hedhog/frontend/app/document-type/page.tsx.ejs +105 -91
  50. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +15 -15
  51. package/hedhog/frontend/app/page.tsx.ejs +5 -5
  52. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1440 -1103
  53. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +4 -3
  54. package/hedhog/frontend/app/person/_components/person-types.ts.ejs +14 -0
  55. package/hedhog/frontend/app/person/page.tsx.ejs +108 -190
  56. package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +599 -0
  57. package/hedhog/frontend/app/pipeline/page.tsx.ejs +1074 -299
  58. package/hedhog/frontend/app/reports/page.tsx.ejs +15 -15
  59. package/hedhog/frontend/messages/en.json +107 -0
  60. package/hedhog/frontend/messages/pt.json +106 -0
  61. package/package.json +6 -6
  62. package/src/person/dto/create-followup.dto.ts +15 -0
  63. package/src/person/dto/create-interaction.dto.ts +23 -0
  64. package/src/person/dto/create.dto.ts +50 -0
  65. package/src/person/dto/duplicates-query.dto.ts +34 -0
  66. package/src/person/dto/merge.dto.ts +15 -0
  67. package/src/person/dto/update-lifecycle-stage.dto.ts +19 -0
  68. package/src/person/dto/update.dto.ts +31 -1
  69. package/src/person/person.controller.ts +63 -2
  70. package/src/person/person.service.ts +1096 -7
@@ -119,9 +119,10 @@ export function PersonInteractionDialog({
119
119
  try {
120
120
  setIsSubmitting(true);
121
121
  await request({
122
- url: `/person/${person.id}`,
123
- method: 'PATCH',
124
- data: { next_action_at: values.next_action_at },
122
+ url: `/person/${person.id}/followup`,
123
+ method: 'POST',
124
+ data: values,
125
+ showErrors: false,
125
126
  });
126
127
  toast.success(t('followupSuccess'));
127
128
  onOpenChange(false);
@@ -128,6 +128,11 @@ export type Person = {
128
128
  source?: PersonSource | null;
129
129
  lifecycle_stage?: PersonLifecycleStage | null;
130
130
  next_action_at?: string | null;
131
+ score?: number | null;
132
+ deal_value?: number | null;
133
+ tags?: string[] | null;
134
+ last_interaction_at?: string | null;
135
+ interaction_count?: number | null;
131
136
  employer_company_id?: number | null;
132
137
  employer_company?: PersonRelationSummary | null;
133
138
  created_at: string;
@@ -136,6 +141,15 @@ export type Person = {
136
141
  document?: PersonDocument[];
137
142
  };
138
143
 
144
+ export type PersonInteraction = {
145
+ id: number;
146
+ type: PersonInteractionType;
147
+ notes?: string | null;
148
+ created_at: string;
149
+ user_id?: number | null;
150
+ user_name?: string | null;
151
+ };
152
+
139
153
  export type ContactTypeOption = {
140
154
  id: number;
141
155
  code: string;
@@ -1,8 +1,15 @@
1
1
  'use client';
2
2
 
3
- import { CrmNav } from '../_components/crm-nav';
4
3
  import { CopyButton } from '@/components/copy-button';
5
- import { Page, PageHeader } from '@/components/entity-list';
4
+ import {
5
+ EmptyState,
6
+ Page,
7
+ PageHeader,
8
+ PaginationFooter,
9
+ SearchBar,
10
+ type SearchBarControl,
11
+ StatsCards,
12
+ } from '@/components/entity-list';
6
13
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
7
14
  import { Badge } from '@/components/ui/badge';
8
15
  import { Button } from '@/components/ui/button';
@@ -14,14 +21,6 @@ import {
14
21
  DropdownMenuSeparator,
15
22
  DropdownMenuTrigger,
16
23
  } from '@/components/ui/dropdown-menu';
17
- import { Input } from '@/components/ui/input';
18
- import {
19
- Select,
20
- SelectContent,
21
- SelectItem,
22
- SelectTrigger,
23
- SelectValue,
24
- } from '@/components/ui/select';
25
24
  import { Skeleton } from '@/components/ui/skeleton';
26
25
  import {
27
26
  Table,
@@ -48,10 +47,6 @@ import {
48
47
  Building2,
49
48
  CalendarDays,
50
49
  CheckCircle2,
51
- ChevronLeft,
52
- ChevronRight,
53
- ChevronsLeft,
54
- ChevronsRight,
55
50
  FileText,
56
51
  LayoutGrid,
57
52
  List,
@@ -61,7 +56,6 @@ import {
61
56
  Pencil,
62
57
  Phone,
63
58
  Plus,
64
- Search,
65
59
  Trash2,
66
60
  User,
67
61
  UserCheck,
@@ -718,6 +712,74 @@ export default function PeoplePage() {
718
712
 
719
713
  const peopleRows = table.getRowModel().rows;
720
714
 
715
+ const statsCards = [
716
+ {
717
+ title: t('statsTotal'),
718
+ value: stats.total,
719
+ icon: <Users className="h-5 w-5" />,
720
+ iconBgColor: 'bg-slate-100',
721
+ iconColor: 'text-slate-700',
722
+ },
723
+ {
724
+ title: t('statsActive'),
725
+ value: stats.active,
726
+ icon: <CheckCircle2 className="h-5 w-5" />,
727
+ iconBgColor: 'bg-green-50',
728
+ iconColor: 'text-green-600',
729
+ },
730
+ {
731
+ title: t('statsIndividual'),
732
+ value: stats.individual,
733
+ icon: <UserCheck className="h-5 w-5" />,
734
+ iconBgColor: 'bg-blue-50',
735
+ iconColor: 'text-blue-600',
736
+ },
737
+ {
738
+ title: t('statsCompany'),
739
+ value: stats.company,
740
+ icon: <Building2 className="h-5 w-5" />,
741
+ iconBgColor: 'bg-amber-50',
742
+ iconColor: 'text-amber-600',
743
+ },
744
+ ];
745
+
746
+ const searchControls: SearchBarControl[] = [
747
+ ...(allowCompanyRegistration
748
+ ? [
749
+ {
750
+ id: 'type-filter',
751
+ type: 'select' as const,
752
+ value: typeFilter,
753
+ onChange: (value: string) => {
754
+ setTypeFilter(value);
755
+ setPage(1);
756
+ },
757
+ placeholder: t('filterByType'),
758
+ options: [
759
+ { value: 'all', label: t('allTypes') },
760
+ { value: 'individual', label: t('individual') },
761
+ { value: 'company', label: t('company') },
762
+ ],
763
+ },
764
+ ]
765
+ : []),
766
+ {
767
+ id: 'status-filter',
768
+ type: 'select',
769
+ value: statusFilter,
770
+ onChange: (value: string) => {
771
+ setStatusFilter(value);
772
+ setPage(1);
773
+ },
774
+ placeholder: t('filterByStatus'),
775
+ options: [
776
+ { value: 'all', label: t('allStatuses') },
777
+ { value: 'active', label: t('active') },
778
+ { value: 'inactive', label: t('inactive') },
779
+ ],
780
+ },
781
+ ];
782
+
721
783
  return (
722
784
  <Page>
723
785
  <PageHeader
@@ -745,104 +807,26 @@ export default function PeoplePage() {
745
807
  ]}
746
808
  />
747
809
 
748
- <CrmNav currentHref="/contact/person" />
749
-
750
- <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
751
- <Card className="py-0">
752
- <CardContent className="flex items-center justify-between p-4">
753
- <div>
754
- <p className="text-xs text-muted-foreground">{t('statsTotal')}</p>
755
- <p className="text-xl font-semibold">{stats.total}</p>
756
- </div>
757
- <Users className="h-5 w-5 text-muted-foreground" />
758
- </CardContent>
759
- </Card>
760
- <Card className="py-0">
761
- <CardContent className="flex items-center justify-between p-4">
762
- <div>
763
- <p className="text-xs text-muted-foreground">
764
- {t('statsActive')}
765
- </p>
766
- <p className="text-xl font-semibold">{stats.active}</p>
767
- </div>
768
- <CheckCircle2 className="h-5 w-5 text-green-600" />
769
- </CardContent>
770
- </Card>
771
- <Card className="py-0">
772
- <CardContent className="flex items-center justify-between p-4">
773
- <div>
774
- <p className="text-xs text-muted-foreground">
775
- {t('statsIndividual')}
776
- </p>
777
- <p className="text-xl font-semibold">{stats.individual}</p>
778
- </div>
779
- <UserCheck className="h-5 w-5 text-blue-600" />
780
- </CardContent>
781
- </Card>
782
- <Card className="py-0">
783
- <CardContent className="flex items-center justify-between p-4">
784
- <div>
785
- <p className="text-xs text-muted-foreground">
786
- {t('statsCompany')}
787
- </p>
788
- <p className="text-xl font-semibold">{stats.company}</p>
789
- </div>
790
- <Building2 className="h-5 w-5 text-amber-600" />
791
- </CardContent>
792
- </Card>
793
- </div>
810
+ <StatsCards stats={statsCards} />
794
811
 
795
812
  <div className="flex flex-col gap-4 xl:flex-row xl:items-center">
796
- <div className="relative flex-1">
797
- <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
798
- <Input
799
- value={searchInput}
800
- onChange={(event) => {
801
- setSearchInput(event.target.value);
813
+ <div className="flex-1">
814
+ <SearchBar
815
+ searchQuery={searchInput}
816
+ onSearchChange={(value) => {
817
+ setSearchInput(value);
802
818
  setPage(1);
803
819
  }}
820
+ onSearch={() => {
821
+ setPage(1);
822
+ void refetch();
823
+ }}
804
824
  placeholder={t('searchByNamePlaceholder')}
805
- className="pl-9"
825
+ controls={searchControls}
806
826
  />
807
827
  </div>
808
828
 
809
829
  <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap xl:justify-end">
810
- {allowCompanyRegistration ? (
811
- <Select
812
- value={typeFilter}
813
- onValueChange={(value) => {
814
- setTypeFilter(value);
815
- setPage(1);
816
- }}
817
- >
818
- <SelectTrigger className="w-full sm:w-44">
819
- <SelectValue placeholder={t('filterByType')} />
820
- </SelectTrigger>
821
- <SelectContent>
822
- <SelectItem value="all">{t('allTypes')}</SelectItem>
823
- <SelectItem value="individual">{t('individual')}</SelectItem>
824
- <SelectItem value="company">{t('company')}</SelectItem>
825
- </SelectContent>
826
- </Select>
827
- ) : null}
828
-
829
- <Select
830
- value={statusFilter}
831
- onValueChange={(value) => {
832
- setStatusFilter(value);
833
- setPage(1);
834
- }}
835
- >
836
- <SelectTrigger className="w-full sm:w-36">
837
- <SelectValue placeholder={t('filterByStatus')} />
838
- </SelectTrigger>
839
- <SelectContent>
840
- <SelectItem value="all">{t('allStatuses')}</SelectItem>
841
- <SelectItem value="active">{t('active')}</SelectItem>
842
- <SelectItem value="inactive">{t('inactive')}</SelectItem>
843
- </SelectContent>
844
- </Select>
845
-
846
830
  <div className="flex items-center justify-between gap-3 sm:justify-start">
847
831
  <span className="text-xs font-medium text-muted-foreground">
848
832
  {t('viewMode')}
@@ -908,17 +892,14 @@ export default function PeoplePage() {
908
892
  </div>
909
893
  )
910
894
  ) : paginate.data.length === 0 ? (
911
- <div className="flex flex-col items-center justify-center py-16 text-center">
912
- <Users className="mb-4 h-12 w-12 text-muted-foreground/50" />
913
- <h3 className="text-lg font-medium">{t('emptyStateTitle')}</h3>
914
- <p className="mb-4 max-w-sm text-sm text-muted-foreground">
915
- {t('emptyStateDescription')}
916
- </p>
917
- <Button onClick={openCreateSheet}>
918
- <Plus className="mr-2 h-4 w-4" />
919
- {t('newPerson')}
920
- </Button>
921
- </div>
895
+ <EmptyState
896
+ icon={<Users className="h-12 w-12" />}
897
+ title={t('emptyStateTitle')}
898
+ description={t('emptyStateDescription')}
899
+ actionLabel={t('newPerson')}
900
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
901
+ onAction={openCreateSheet}
902
+ />
922
903
  ) : (
923
904
  <>
924
905
  {viewMode === 'table' ? (
@@ -1230,81 +1211,18 @@ export default function PeoplePage() {
1230
1211
  </div>
1231
1212
  )}
1232
1213
 
1233
- <div className="flex flex-col gap-4 border-t p-4 lg:flex-row lg:items-center lg:justify-between">
1234
- <div className="text-sm text-muted-foreground">
1235
- {t('resultsCount', { count: paginate.total })}
1236
- </div>
1237
-
1238
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
1239
- <div className="flex items-center gap-2">
1240
- <span className="text-sm text-muted-foreground">
1241
- {t('rowsPerPage')}
1242
- </span>
1243
- <Select
1244
- value={String(pageSize)}
1245
- onValueChange={(value) => {
1246
- setPageSize(Number(value));
1247
- setPage(1);
1248
- }}
1249
- >
1250
- <SelectTrigger className="w-20">
1251
- <SelectValue />
1252
- </SelectTrigger>
1253
- <SelectContent>
1254
- {[12, 20, 30, 40, 50].map((size) => (
1255
- <SelectItem key={size} value={String(size)}>
1256
- {size}
1257
- </SelectItem>
1258
- ))}
1259
- </SelectContent>
1260
- </Select>
1261
- </div>
1262
-
1263
- <div className="flex items-center gap-2">
1264
- <Button
1265
- variant="outline"
1266
- size="icon"
1267
- onClick={() => setPage(1)}
1268
- disabled={page <= 1}
1269
- >
1270
- <ChevronsLeft className="h-4 w-4" />
1271
- </Button>
1272
- <Button
1273
- variant="outline"
1274
- size="icon"
1275
- onClick={() =>
1276
- setPage((previous) => Math.max(1, previous - 1))
1277
- }
1278
- disabled={page <= 1}
1279
- >
1280
- <ChevronLeft className="h-4 w-4" />
1281
- </Button>
1282
- <span className="min-w-32 text-center text-sm">
1283
- {t('paginationLabel', {
1284
- current: page,
1285
- total: pageCount,
1286
- })}
1287
- </span>
1288
- <Button
1289
- variant="outline"
1290
- size="icon"
1291
- onClick={() =>
1292
- setPage((previous) => Math.min(pageCount, previous + 1))
1293
- }
1294
- disabled={page >= pageCount}
1295
- >
1296
- <ChevronRight className="h-4 w-4" />
1297
- </Button>
1298
- <Button
1299
- variant="outline"
1300
- size="icon"
1301
- onClick={() => setPage(pageCount)}
1302
- disabled={page >= pageCount}
1303
- >
1304
- <ChevronsRight className="h-4 w-4" />
1305
- </Button>
1306
- </div>
1307
- </div>
1214
+ <div className="border-t p-4">
1215
+ <PaginationFooter
1216
+ currentPage={page}
1217
+ pageSize={pageSize}
1218
+ totalItems={paginate.total}
1219
+ onPageChange={setPage}
1220
+ onPageSizeChange={(nextPageSize) => {
1221
+ setPageSize(nextPageSize);
1222
+ setPage(1);
1223
+ }}
1224
+ pageSizeOptions={[12, 20, 30, 40, 50]}
1225
+ />
1308
1226
  </div>
1309
1227
  </>
1310
1228
  )}