@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.
- package/dist/person/person.service.d.ts +2 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +111 -127
- package/dist/person/person.service.js.map +1 -1
- package/dist/person/person.service.spec.d.ts +2 -0
- package/dist/person/person.service.spec.d.ts.map +1 -0
- package/dist/person/person.service.spec.js +106 -0
- package/dist/person/person.service.spec.js.map +1 -0
- package/dist/proposal/proposal.service.d.ts +5 -0
- package/dist/proposal/proposal.service.d.ts.map +1 -1
- package/dist/proposal/proposal.service.js +242 -19
- package/dist/proposal/proposal.service.js.map +1 -1
- package/dist/proposal/proposal.service.spec.js +153 -165
- package/dist/proposal/proposal.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +35 -18
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +517 -346
- package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +42 -17
- package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +1 -1
- package/hedhog/frontend/app/activities/page.tsx.ejs +315 -101
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +172 -22
- package/hedhog/frontend/app/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +509 -441
- package/hedhog/frontend/app/pipeline/page.tsx.ejs +30 -4
- package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +773 -0
- package/hedhog/frontend/app/proposals/approvals/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/proposals/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/reports/page.tsx.ejs +431 -375
- package/hedhog/frontend/messages/en.json +100 -1
- package/hedhog/frontend/messages/pt.json +100 -1
- package/package.json +6 -6
- package/src/person/person.service.spec.ts +143 -0
- package/src/person/person.service.ts +147 -158
- package/src/proposal/proposal.service.spec.ts +196 -0
- 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
|
-
<
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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-
|
|
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">
|
|
@@ -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-
|
|
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-
|
|
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">
|