@hed-hog/contact 0.0.304 → 0.0.306

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 (44) hide show
  1. package/README.md +225 -17
  2. package/dist/person/dto/account.dto.d.ts +5 -0
  3. package/dist/person/dto/account.dto.d.ts.map +1 -1
  4. package/dist/person/dto/account.dto.js +29 -0
  5. package/dist/person/dto/account.dto.js.map +1 -1
  6. package/dist/person/dto/import-preview.dto.d.ts +7 -0
  7. package/dist/person/dto/import-preview.dto.d.ts.map +1 -0
  8. package/dist/person/dto/import-preview.dto.js +7 -0
  9. package/dist/person/dto/import-preview.dto.js.map +1 -0
  10. package/dist/person/dto/import.dto.d.ts +15 -0
  11. package/dist/person/dto/import.dto.d.ts.map +1 -0
  12. package/dist/person/dto/import.dto.js +51 -0
  13. package/dist/person/dto/import.dto.js.map +1 -0
  14. package/dist/person/person.controller.d.ts +14 -0
  15. package/dist/person/person.controller.d.ts.map +1 -1
  16. package/dist/person/person.controller.js +53 -0
  17. package/dist/person/person.controller.js.map +1 -1
  18. package/dist/person/person.service.d.ts +19 -0
  19. package/dist/person/person.service.d.ts.map +1 -1
  20. package/dist/person/person.service.js +481 -67
  21. package/dist/person/person.service.js.map +1 -1
  22. package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
  23. package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
  24. package/hedhog/data/route.yaml +6 -0
  25. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +2242 -484
  26. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +51 -0
  27. package/hedhog/frontend/app/accounts/page.tsx.ejs +181 -16
  28. package/hedhog/frontend/app/contact-type/page.tsx.ejs +223 -29
  29. package/hedhog/frontend/app/document-type/page.tsx.ejs +248 -37
  30. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +129 -19
  31. package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +78 -212
  32. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +760 -178
  33. package/hedhog/frontend/app/person/_components/person-import-sheet.tsx.ejs +1120 -0
  34. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +171 -4
  35. package/hedhog/frontend/app/person/page.tsx.ejs +17 -0
  36. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +160 -35
  37. package/hedhog/frontend/messages/en.json +104 -2
  38. package/hedhog/frontend/messages/pt.json +111 -9
  39. package/package.json +4 -4
  40. package/src/person/dto/account.dto.ts +31 -0
  41. package/src/person/dto/import-preview.dto.ts +6 -0
  42. package/src/person/dto/import.dto.ts +61 -0
  43. package/src/person/person.controller.ts +74 -12
  44. package/src/person/person.service.ts +615 -68
@@ -1,14 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Button } from '@/components/ui/button';
4
- import {
5
- Command,
6
- CommandEmpty,
7
- CommandGroup,
8
- CommandInput,
9
- CommandItem,
10
- CommandList,
11
- } from '@/components/ui/command';
4
+ import { EntityPicker } from '@/components/ui/entity-picker';
12
5
  import {
13
6
  Form,
14
7
  FormControl,
@@ -18,12 +11,6 @@ import {
18
11
  FormMessage,
19
12
  } from '@/components/ui/form';
20
13
  import { Input } from '@/components/ui/input';
21
- import { Label } from '@/components/ui/label';
22
- import {
23
- Popover,
24
- PopoverContent,
25
- PopoverTrigger,
26
- } from '@/components/ui/popover';
27
14
  import {
28
15
  Select,
29
16
  SelectContent,
@@ -40,16 +27,10 @@ import {
40
27
  } from '@/components/ui/sheet';
41
28
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
42
29
  import { zodResolver } from '@hookform/resolvers/zod';
43
- import { ChevronsUpDown, Plus, X } from 'lucide-react';
30
+ import { Plus } from 'lucide-react';
44
31
  import { useTranslations } from 'next-intl';
45
32
  import { useEffect, useMemo, useRef, useState } from 'react';
46
- import {
47
- Controller,
48
- FieldValues,
49
- Path,
50
- UseFormReturn,
51
- useForm,
52
- } from 'react-hook-form';
33
+ import { FieldValues, Path, UseFormReturn, useForm } from 'react-hook-form';
53
34
  import { z } from 'zod';
54
35
 
55
36
  type PersonOption = {
@@ -103,9 +84,10 @@ function usePersonFieldWithCreateTranslations() {
103
84
  // Filter values to only include types compatible with next-intl
104
85
  const filteredValues = values
105
86
  ? Object.fromEntries(
106
- Object.entries(values).filter(
107
- ([_, v]) => typeof v === 'string' || typeof v === 'number'
108
- )
87
+ Object.entries(values).filter(([key, v]) => {
88
+ void key;
89
+ return typeof v === 'string' || typeof v === 'number';
90
+ })
109
91
  )
110
92
  : undefined;
111
93
  if (tRoot.has(contactKey)) {
@@ -574,23 +556,12 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
574
556
  }) {
575
557
  const { request } = useApp();
576
558
  const t = usePersonFieldWithCreateTranslations();
577
- const [personOpen, setPersonOpen] = useState(false);
578
- const [personSearch, setPersonSearch] = useState('');
579
- const [debouncedPersonSearch, setDebouncedPersonSearch] = useState('');
580
559
  const [createPersonOpen, setCreatePersonOpen] = useState(false);
581
560
  const [selectedPersonLabel, setSelectedPersonLabel] =
582
561
  useState(initialSelectedLabel);
583
562
  const parentScrollContainerRef = useRef<HTMLElement | null>(null);
584
563
  const parentScrollTopRef = useRef(0);
585
564
 
586
- useEffect(() => {
587
- const timeout = setTimeout(() => {
588
- setDebouncedPersonSearch(personSearch);
589
- }, 300);
590
-
591
- return () => clearTimeout(timeout);
592
- }, [personSearch]);
593
-
594
565
  useEffect(() => {
595
566
  setSelectedPersonLabel(initialSelectedLabel);
596
567
  }, [initialSelectedLabel]);
@@ -638,187 +609,83 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
638
609
  setTimeout(restore, 120);
639
610
  };
640
611
 
641
- const { data: personOptionsData = [], isLoading: isLoadingPersons } =
642
- useQuery<PersonOption[]>({
643
- queryKey: [
644
- 'person-autocomplete-generic',
645
- entityLabel,
646
- debouncedPersonSearch,
647
- personTypeFilter,
648
- ],
649
- queryFn: async () => {
650
- const params = new URLSearchParams();
651
- params.set('page', '1');
652
- params.set('pageSize', '20');
653
- if (personTypeFilter !== 'all') {
654
- params.set('type', personTypeFilter);
655
- }
656
- if (debouncedPersonSearch.trim()) {
657
- params.set('search', debouncedPersonSearch.trim());
658
- }
659
-
660
- const response = await request<
661
- PaginatedResponse<PersonOption> | PersonOption[]
662
- >({
663
- url: `/person?${params.toString()}`,
664
- method: 'GET',
665
- });
666
-
667
- const payload = response?.data;
668
- if (Array.isArray(payload)) {
669
- return payload as PersonOption[];
670
- }
671
-
672
- if (payload && 'data' in payload && Array.isArray(payload.data)) {
673
- return payload.data as PersonOption[];
674
- }
675
-
676
- return [];
677
- },
678
- placeholderData: (old) => old ?? [],
679
- });
680
-
681
612
  const parseFieldValue = (personId: string | number) =>
682
613
  valueType === 'number' ? Number(personId) : String(personId);
683
614
 
684
- const fieldState = form.getFieldState(name, form.formState);
685
-
686
615
  return (
687
616
  <>
688
- <div className="grid gap-2">
689
- <Label
690
- data-error={!!fieldState.error}
691
- className="data-[error=true]:text-destructive"
692
- >
693
- {label}
694
- </Label>
695
- <Controller
696
- control={form.control}
697
- name={name}
698
- render={({ field }) => {
699
- const hasValue =
700
- field.value !== undefined &&
701
- field.value !== null &&
702
- String(field.value).length > 0;
703
-
704
- return (
705
- <div className="flex w-full min-w-0 items-center gap-2">
706
- <Popover open={personOpen} onOpenChange={setPersonOpen}>
707
- <PopoverTrigger asChild>
708
- <Button
709
- type="button"
710
- variant="outline"
711
- role="combobox"
712
- className="flex-1 min-w-0 justify-between overflow-hidden"
713
- >
714
- <span className="truncate text-left">
715
- {hasValue
716
- ? (personOptionsData.find(
717
- (person) =>
718
- String(person.id) === String(field.value)
719
- )?.name ??
720
- selectedPersonLabel ??
721
- `ID #${String(field.value)}`)
722
- : selectPlaceholder}
723
- </span>
724
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
725
- </Button>
726
- </PopoverTrigger>
727
- <PopoverContent
728
- className="p-0"
729
- style={{ width: 'var(--radix-popover-trigger-width)' }}
730
- >
731
- <Command shouldFilter={false}>
732
- <CommandInput
733
- placeholder={t('search.placeholder')}
734
- value={personSearch}
735
- onValueChange={setPersonSearch}
736
- />
737
- <CommandList>
738
- <CommandEmpty>
739
- {isLoadingPersons ? (
740
- t('search.loading')
741
- ) : (
742
- <div className="space-y-2 p-2 text-center">
743
- <p className="text-sm text-muted-foreground">
744
- {t('search.noResults')}
745
- </p>
746
- <Button
747
- type="button"
748
- variant="outline"
749
- className="w-full"
750
- onClick={(event) => {
751
- captureParentScrollPosition(
752
- event.currentTarget
753
- );
754
- setPersonOpen(false);
755
- setCreatePersonOpen(true);
756
- }}
757
- >
758
- {t('actions.createNew')}
759
- </Button>
760
- </div>
761
- )}
762
- </CommandEmpty>
763
- <CommandGroup>
764
- {personOptionsData.map((person) => (
765
- <CommandItem
766
- key={String(person.id)}
767
- value={`${person.name}-${person.id}`}
768
- onSelect={() => {
769
- field.onChange(parseFieldValue(person.id));
770
- setSelectedPersonLabel(person.name);
771
- setPersonOpen(false);
772
- }}
773
- >
774
- {person.name}
775
- </CommandItem>
776
- ))}
777
- </CommandGroup>
778
- </CommandList>
779
- </Command>
780
- </PopoverContent>
781
- </Popover>
782
-
783
- {hasValue ? (
784
- <Button
785
- type="button"
786
- variant="outline"
787
- size="icon"
788
- className="shrink-0"
789
- onClick={() => {
790
- field.onChange(valueType === 'number' ? null : '');
791
- setPersonSearch('');
792
- setSelectedPersonLabel('');
793
- setPersonOpen(false);
794
- }}
795
- aria-label={t('actions.clearSelection')}
796
- >
797
- <X className="h-4 w-4" />
798
- </Button>
799
- ) : null}
800
-
801
- <Button
802
- type="button"
803
- variant="outline"
804
- size="icon"
805
- className="shrink-0"
806
- onClick={(event) => {
807
- captureParentScrollPosition(event.currentTarget);
808
- setPersonOpen(false);
809
- setCreatePersonOpen(true);
810
- }}
811
- aria-label={t('actions.createEntityAria', { entityLabel })}
812
- >
813
- <Plus className="h-4 w-4" />
814
- </Button>
815
- </div>
816
- );
617
+ <div className="flex w-full items-end gap-2">
618
+ <div className="min-w-0 flex-1">
619
+ <EntityPicker<PersonOption, TFieldValues>
620
+ form={form}
621
+ name={name}
622
+ label={label}
623
+ placeholder={selectPlaceholder}
624
+ entityLabel={entityLabel}
625
+ valueType={valueType}
626
+ initialSelectedLabel={selectedPersonLabel}
627
+ searchPlaceholder={t('search.placeholder')}
628
+ emptyStateDescription={t('search.noResults')}
629
+ loadingLabel={t('search.loading')}
630
+ showCreateButton={false}
631
+ clearable
632
+ loadOptions={async ({ page, pageSize, search }) => {
633
+ const params = new URLSearchParams();
634
+ params.set('page', String(page));
635
+ params.set('pageSize', String(pageSize));
636
+
637
+ if (personTypeFilter !== 'all') {
638
+ params.set('type', personTypeFilter);
639
+ }
640
+
641
+ if (search.trim()) {
642
+ params.set('search', search.trim());
643
+ }
644
+
645
+ const response = await request<
646
+ PaginatedResponse<PersonOption> | PersonOption[]
647
+ >({
648
+ url: `/person?${params.toString()}`,
649
+ method: 'GET',
650
+ });
651
+
652
+ const payload = response?.data;
653
+ if (Array.isArray(payload)) {
654
+ return {
655
+ items: payload as PersonOption[],
656
+ hasMore: false,
657
+ };
658
+ }
659
+
660
+ const items = Array.isArray(payload?.data) ? payload.data : [];
661
+
662
+ return {
663
+ items,
664
+ hasMore: items.length >= pageSize,
665
+ };
666
+ }}
667
+ getOptionValue={(person) => person.id}
668
+ getOptionLabel={(person) => person.name}
669
+ onChange={(value, option) => {
670
+ void value;
671
+ setSelectedPersonLabel(option?.name ?? '');
672
+ }}
673
+ />
674
+ </div>
675
+
676
+ <Button
677
+ type="button"
678
+ variant="outline"
679
+ size="icon"
680
+ className="h-10 w-10 shrink-0"
681
+ onClick={(event) => {
682
+ captureParentScrollPosition(event.currentTarget);
683
+ setCreatePersonOpen(true);
817
684
  }}
818
- />
819
- {fieldState.error?.message ? (
820
- <p className="text-destructive text-sm">{fieldState.error.message}</p>
821
- ) : null}
685
+ aria-label={t('actions.createEntityAria', { entityLabel })}
686
+ >
687
+ <Plus className="h-4 w-4" />
688
+ </Button>
822
689
  </div>
823
690
 
824
691
  <CreatePersonSheet
@@ -839,7 +706,6 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
839
706
  shouldTouch: true,
840
707
  });
841
708
  setSelectedPersonLabel(person.name);
842
- setPersonSearch(person.name);
843
709
  setCreatePersonOpen(false);
844
710
  }}
845
711
  />