@hed-hog/contact 0.0.300 → 0.0.302
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/contact.module.d.ts.map +1 -1
- package/dist/contact.module.js +2 -0
- package/dist/contact.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- 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/dto/proposal.dto.d.ts +152 -0
- package/dist/proposal/dto/proposal.dto.d.ts.map +1 -0
- package/dist/proposal/dto/proposal.dto.js +396 -0
- package/dist/proposal/dto/proposal.dto.js.map +1 -0
- package/dist/proposal/proposal-contract.subscriber.d.ts +11 -0
- package/dist/proposal/proposal-contract.subscriber.d.ts.map +1 -0
- package/dist/proposal/proposal-contract.subscriber.js +51 -0
- package/dist/proposal/proposal-contract.subscriber.js.map +1 -0
- package/dist/proposal/proposal-event.types.d.ts +122 -0
- package/dist/proposal/proposal-event.types.d.ts.map +1 -0
- package/dist/proposal/proposal-event.types.js +13 -0
- package/dist/proposal/proposal-event.types.js.map +1 -0
- package/dist/proposal/proposal.controller.d.ts +56 -0
- package/dist/proposal/proposal.controller.d.ts.map +1 -0
- package/dist/proposal/proposal.controller.js +191 -0
- package/dist/proposal/proposal.controller.js.map +1 -0
- package/dist/proposal/proposal.module.d.ts +3 -0
- package/dist/proposal/proposal.module.d.ts.map +1 -0
- package/dist/proposal/proposal.module.js +32 -0
- package/dist/proposal/proposal.module.js.map +1 -0
- package/dist/proposal/proposal.service.d.ts +100 -0
- package/dist/proposal/proposal.service.d.ts.map +1 -0
- package/dist/proposal/proposal.service.js +2137 -0
- package/dist/proposal/proposal.service.js.map +1 -0
- package/dist/proposal/proposal.service.spec.d.ts +2 -0
- package/dist/proposal/proposal.service.spec.d.ts.map +1 -0
- package/dist/proposal/proposal.service.spec.js +175 -0
- package/dist/proposal/proposal.service.spec.js.map +1 -0
- package/hedhog/data/menu.yaml +35 -18
- package/hedhog/data/route.yaml +44 -0
- 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 +253 -210
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +1661 -0
- 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 +236 -43
- package/hedhog/frontend/messages/pt.json +235 -42
- package/hedhog/table/proposal.yaml +112 -0
- package/hedhog/table/proposal_approval.yaml +63 -0
- package/hedhog/table/proposal_document.yaml +77 -0
- package/hedhog/table/proposal_item.yaml +64 -0
- package/hedhog/table/proposal_revision.yaml +78 -0
- package/package.json +5 -4
- package/src/contact.module.ts +2 -0
- package/src/index.ts +3 -0
- package/src/person/person.service.spec.ts +143 -0
- package/src/person/person.service.ts +147 -158
- package/src/proposal/dto/proposal.dto.ts +341 -0
- package/src/proposal/proposal-contract.subscriber.ts +43 -0
- package/src/proposal/proposal-event.types.ts +130 -0
- package/src/proposal/proposal.controller.ts +168 -0
- package/src/proposal/proposal.module.ts +19 -0
- package/src/proposal/proposal.service.spec.ts +196 -0
- package/src/proposal/proposal.service.ts +2855 -0
|
@@ -18,7 +18,11 @@ import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
|
18
18
|
import { CalendarClock, CheckCircle2, Loader2 } from 'lucide-react';
|
|
19
19
|
import { useTranslations } from 'next-intl';
|
|
20
20
|
import type { ReactNode } from 'react';
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
ActivityDetail,
|
|
23
|
+
ActivityPriority,
|
|
24
|
+
ActivityStatus,
|
|
25
|
+
} from './activity-types';
|
|
22
26
|
|
|
23
27
|
function getStatusBadgeClass(status: ActivityStatus) {
|
|
24
28
|
if (status === 'completed') {
|
|
@@ -44,13 +48,7 @@ function getPriorityBadgeClass(priority: ActivityPriority) {
|
|
|
44
48
|
return 'border-slate-500/25 bg-slate-500/10 text-slate-700';
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
function DetailRow({
|
|
48
|
-
label,
|
|
49
|
-
value,
|
|
50
|
-
}: {
|
|
51
|
-
label: string;
|
|
52
|
-
value: ReactNode;
|
|
53
|
-
}) {
|
|
51
|
+
function DetailRow({ label, value }: { label: string; value: ReactNode }) {
|
|
54
52
|
return (
|
|
55
53
|
<div className="grid gap-1 sm:grid-cols-[140px_1fr] sm:items-start sm:gap-3">
|
|
56
54
|
<span className="text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
|
@@ -79,8 +77,17 @@ export function ActivityDetailSheet({
|
|
|
79
77
|
const t = useTranslations('contact.CrmActivities');
|
|
80
78
|
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
81
79
|
|
|
82
|
-
const {
|
|
83
|
-
|
|
80
|
+
const {
|
|
81
|
+
data: activity,
|
|
82
|
+
isLoading,
|
|
83
|
+
isFetching,
|
|
84
|
+
} = useQuery<ActivityDetail | null>({
|
|
85
|
+
queryKey: [
|
|
86
|
+
'contact-activity-detail',
|
|
87
|
+
activityId,
|
|
88
|
+
refreshKey,
|
|
89
|
+
currentLocaleCode,
|
|
90
|
+
],
|
|
84
91
|
enabled: open && !!activityId,
|
|
85
92
|
queryFn: async () => {
|
|
86
93
|
if (!activityId) {
|
|
@@ -104,7 +111,7 @@ export function ActivityDetailSheet({
|
|
|
104
111
|
|
|
105
112
|
return (
|
|
106
113
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
107
|
-
<SheetContent className="flex w-full flex-col sm:max-w-xl">
|
|
114
|
+
<SheetContent className="flex w-full flex-col sm:max-w-xl px-4">
|
|
108
115
|
<SheetHeader className="text-left">
|
|
109
116
|
<SheetTitle>{t('detail.title')}</SheetTitle>
|
|
110
117
|
<SheetDescription>{t('detail.description')}</SheetDescription>
|
|
@@ -139,7 +146,10 @@ export function ActivityDetailSheet({
|
|
|
139
146
|
</h2>
|
|
140
147
|
<Badge
|
|
141
148
|
variant="outline"
|
|
142
|
-
className={cn(
|
|
149
|
+
className={cn(
|
|
150
|
+
'border',
|
|
151
|
+
getStatusBadgeClass(activity.status)
|
|
152
|
+
)}
|
|
143
153
|
>
|
|
144
154
|
{t(`status.${activity.status}`)}
|
|
145
155
|
</Badge>
|
|
@@ -162,7 +172,10 @@ export function ActivityDetailSheet({
|
|
|
162
172
|
<Separator />
|
|
163
173
|
|
|
164
174
|
<div className="space-y-4">
|
|
165
|
-
<DetailRow
|
|
175
|
+
<DetailRow
|
|
176
|
+
label={t('detail.person')}
|
|
177
|
+
value={activity.person.name}
|
|
178
|
+
/>
|
|
166
179
|
<DetailRow
|
|
167
180
|
label={t('detail.personType')}
|
|
168
181
|
value={t(`detail.personTypeValue.${activity.person.type}`)}
|
|
@@ -174,7 +187,10 @@ export function ActivityDetailSheet({
|
|
|
174
187
|
/>
|
|
175
188
|
) : null}
|
|
176
189
|
<DetailRow label={t('detail.owner')} value={ownerName} />
|
|
177
|
-
<DetailRow
|
|
190
|
+
<DetailRow
|
|
191
|
+
label={t('detail.type')}
|
|
192
|
+
value={t(`type.${activity.type}`)}
|
|
193
|
+
/>
|
|
178
194
|
<DetailRow
|
|
179
195
|
label={t('detail.source')}
|
|
180
196
|
value={t(`detail.sourceKind.${activity.source_kind}`)}
|
|
@@ -207,10 +223,15 @@ export function ActivityDetailSheet({
|
|
|
207
223
|
: t('detail.notCompleted')
|
|
208
224
|
}
|
|
209
225
|
/>
|
|
210
|
-
<DetailRow
|
|
226
|
+
<DetailRow
|
|
227
|
+
label={t('detail.createdBy')}
|
|
228
|
+
value={createdByName}
|
|
229
|
+
/>
|
|
211
230
|
<DetailRow
|
|
212
231
|
label={t('detail.completedBy')}
|
|
213
|
-
value={
|
|
232
|
+
value={
|
|
233
|
+
isCompleted ? completedByName : t('detail.notCompleted')
|
|
234
|
+
}
|
|
214
235
|
/>
|
|
215
236
|
</div>
|
|
216
237
|
</>
|
|
@@ -218,7 +239,11 @@ export function ActivityDetailSheet({
|
|
|
218
239
|
</div>
|
|
219
240
|
|
|
220
241
|
<SheetFooter className="border-t pt-4">
|
|
221
|
-
<Button
|
|
242
|
+
<Button
|
|
243
|
+
type="button"
|
|
244
|
+
variant="outline"
|
|
245
|
+
onClick={() => onOpenChange(false)}
|
|
246
|
+
>
|
|
222
247
|
{t('detail.close')}
|
|
223
248
|
</Button>
|
|
224
249
|
<Button
|
|
@@ -9,7 +9,7 @@ export type ActivityType =
|
|
|
9
9
|
export type ActivityPriority = 'high' | 'medium' | 'low';
|
|
10
10
|
export type ActivityStatus = 'pending' | 'overdue' | 'completed';
|
|
11
11
|
export type ActivitySourceKind = 'manual' | 'followup' | 'interaction';
|
|
12
|
-
export type ActivityViewMode = 'table' | '
|
|
12
|
+
export type ActivityViewMode = 'table' | 'cards';
|
|
13
13
|
|
|
14
14
|
export type PaginatedResult<T> = {
|
|
15
15
|
data: T[];
|
|
@@ -69,14 +69,18 @@ function getTypeIcon(type: ActivityType) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function getStatusBadgeClass(status: ActivityStatus) {
|
|
72
|
-
if (status === 'completed')
|
|
73
|
-
|
|
72
|
+
if (status === 'completed')
|
|
73
|
+
return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-700';
|
|
74
|
+
if (status === 'overdue')
|
|
75
|
+
return 'border-red-500/25 bg-red-500/10 text-red-700';
|
|
74
76
|
return 'border-amber-500/25 bg-amber-500/10 text-amber-700';
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function getPriorityBadgeClass(priority: ActivityPriority) {
|
|
78
|
-
if (priority === 'high')
|
|
79
|
-
|
|
80
|
+
if (priority === 'high')
|
|
81
|
+
return 'border-red-500/25 bg-red-500/10 text-red-700';
|
|
82
|
+
if (priority === 'medium')
|
|
83
|
+
return 'border-sky-500/25 bg-sky-500/10 text-sky-700';
|
|
80
84
|
return 'border-slate-500/25 bg-slate-500/10 text-slate-700';
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -93,38 +97,47 @@ export default function CrmActivitiesPage() {
|
|
|
93
97
|
const [page, setPage] = useState(1);
|
|
94
98
|
const [pageSize, setPageSize] = useState(12);
|
|
95
99
|
const [viewMode, setViewMode] = useState<ActivityViewMode>('table');
|
|
96
|
-
const [selectedActivityId, setSelectedActivityId] = useState<number | null>(
|
|
100
|
+
const [selectedActivityId, setSelectedActivityId] = useState<number | null>(
|
|
101
|
+
null
|
|
102
|
+
);
|
|
97
103
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
98
|
-
const [completingActivityId, setCompletingActivityId] = useState<
|
|
104
|
+
const [completingActivityId, setCompletingActivityId] = useState<
|
|
105
|
+
number | null
|
|
106
|
+
>(null);
|
|
99
107
|
const [detailRefreshKey, setDetailRefreshKey] = useState(0);
|
|
100
108
|
|
|
101
109
|
useEffect(() => {
|
|
102
|
-
const timeout = setTimeout(
|
|
110
|
+
const timeout = setTimeout(
|
|
111
|
+
() => setDebouncedSearch(searchInput.trim()),
|
|
112
|
+
300
|
|
113
|
+
);
|
|
103
114
|
return () => clearTimeout(timeout);
|
|
104
115
|
}, [searchInput]);
|
|
105
116
|
|
|
106
117
|
useEffect(() => {
|
|
107
118
|
try {
|
|
108
119
|
const saved = window.localStorage.getItem(ACTIVITIES_VIEW_STORAGE_KEY);
|
|
109
|
-
if (saved === 'table' || saved === '
|
|
120
|
+
if (saved === 'table' || saved === 'cards') setViewMode(saved);
|
|
110
121
|
} catch {
|
|
111
122
|
// Ignore storage read failures.
|
|
112
123
|
}
|
|
113
124
|
}, []);
|
|
114
125
|
|
|
115
|
-
const {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const {
|
|
127
|
+
data: stats = { total: 0, pending: 0, overdue: 0, completed: 0 },
|
|
128
|
+
refetch: refetchStats,
|
|
129
|
+
} = useQuery<ActivityStats>({
|
|
130
|
+
queryKey: ['contact-activities-stats', currentLocaleCode],
|
|
131
|
+
queryFn: async () => {
|
|
132
|
+
const response = await request<ActivityStats>({
|
|
133
|
+
url: '/person/activities/stats',
|
|
134
|
+
method: 'GET',
|
|
135
|
+
});
|
|
136
|
+
return response.data;
|
|
137
|
+
},
|
|
138
|
+
placeholderData: (previous) =>
|
|
139
|
+
previous ?? { total: 0, pending: 0, overdue: 0, completed: 0 },
|
|
140
|
+
});
|
|
128
141
|
|
|
129
142
|
const {
|
|
130
143
|
data: paginate = {
|
|
@@ -233,14 +246,42 @@ export default function CrmActivitiesPage() {
|
|
|
233
246
|
];
|
|
234
247
|
|
|
235
248
|
const statsCards = [
|
|
236
|
-
{
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
249
|
+
{
|
|
250
|
+
key: 'total',
|
|
251
|
+
title: t('stats.total'),
|
|
252
|
+
value: stats.total,
|
|
253
|
+
icon: Activity,
|
|
254
|
+
accentClassName: 'from-violet-500/20 via-fuchsia-500/10 to-transparent',
|
|
255
|
+
iconContainerClassName: 'bg-violet-500/10 text-violet-700',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: 'pending',
|
|
259
|
+
title: t('stats.pending'),
|
|
260
|
+
value: stats.pending,
|
|
261
|
+
icon: Clock3,
|
|
262
|
+
accentClassName: 'from-amber-500/20 via-yellow-500/10 to-transparent',
|
|
263
|
+
iconContainerClassName: 'bg-amber-500/10 text-amber-700',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
key: 'overdue',
|
|
267
|
+
title: t('stats.overdue'),
|
|
268
|
+
value: stats.overdue,
|
|
269
|
+
icon: CircleAlert,
|
|
270
|
+
accentClassName: 'from-red-500/20 via-rose-500/10 to-transparent',
|
|
271
|
+
iconContainerClassName: 'bg-red-500/10 text-red-700',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
key: 'completed',
|
|
275
|
+
title: t('stats.completed'),
|
|
276
|
+
value: stats.completed,
|
|
277
|
+
icon: CalendarCheck,
|
|
278
|
+
accentClassName: 'from-emerald-500/20 via-green-500/10 to-transparent',
|
|
279
|
+
iconContainerClassName: 'bg-emerald-500/10 text-emerald-700',
|
|
280
|
+
},
|
|
240
281
|
];
|
|
241
282
|
|
|
242
283
|
const handleViewModeChange = (value: string) => {
|
|
243
|
-
if (value !== 'table' && value !== '
|
|
284
|
+
if (value !== 'table' && value !== 'cards') return;
|
|
244
285
|
setViewMode(value);
|
|
245
286
|
try {
|
|
246
287
|
window.localStorage.setItem(ACTIVITIES_VIEW_STORAGE_KEY, value);
|
|
@@ -252,7 +293,10 @@ export default function CrmActivitiesPage() {
|
|
|
252
293
|
const handleComplete = async (activityId: number) => {
|
|
253
294
|
setCompletingActivityId(activityId);
|
|
254
295
|
try {
|
|
255
|
-
await request({
|
|
296
|
+
await request({
|
|
297
|
+
url: `/person/activities/${activityId}/complete`,
|
|
298
|
+
method: 'POST',
|
|
299
|
+
});
|
|
256
300
|
setDetailRefreshKey((current) => current + 1);
|
|
257
301
|
await Promise.all([refetchActivities(), refetchStats()]);
|
|
258
302
|
toast.success(t('toasts.markedAsCompleted'));
|
|
@@ -276,31 +320,54 @@ export default function CrmActivitiesPage() {
|
|
|
276
320
|
/>
|
|
277
321
|
|
|
278
322
|
<div className="space-y-6">
|
|
279
|
-
<KpiCardsGrid items={statsCards} />
|
|
323
|
+
<KpiCardsGrid items={statsCards} className="mb-4" />
|
|
280
324
|
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
325
|
+
<div className="flex flex-col gap-4 xl:flex-row xl:items-center mb-4">
|
|
326
|
+
<div className="flex-1">
|
|
327
|
+
<SearchBar
|
|
328
|
+
searchQuery={searchInput}
|
|
329
|
+
onSearchChange={(value) => {
|
|
330
|
+
setSearchInput(value);
|
|
331
|
+
setPage(1);
|
|
332
|
+
}}
|
|
333
|
+
onSearch={() => setPage(1)}
|
|
334
|
+
placeholder={t('filters.searchPlaceholder')}
|
|
335
|
+
controls={controls}
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
291
338
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
339
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap xl:justify-end">
|
|
340
|
+
<div className="flex items-center justify-between gap-3 sm:justify-start">
|
|
341
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
342
|
+
{t('viewMode')}
|
|
343
|
+
</span>
|
|
344
|
+
<ToggleGroup
|
|
345
|
+
type="single"
|
|
346
|
+
value={viewMode}
|
|
347
|
+
onValueChange={handleViewModeChange}
|
|
348
|
+
variant="outline"
|
|
349
|
+
size="sm"
|
|
350
|
+
aria-label={t('viewMode')}
|
|
351
|
+
>
|
|
352
|
+
<ToggleGroupItem
|
|
353
|
+
value="table"
|
|
354
|
+
className="gap-1.5 px-2.5"
|
|
355
|
+
aria-label={t('viewModeTable')}
|
|
356
|
+
>
|
|
357
|
+
<List className="h-4 w-4" />
|
|
358
|
+
<span className="hidden sm:inline">{t('viewModeTable')}</span>
|
|
359
|
+
</ToggleGroupItem>
|
|
360
|
+
<ToggleGroupItem
|
|
361
|
+
value="cards"
|
|
362
|
+
className="gap-1.5 px-2.5"
|
|
363
|
+
aria-label={t('viewModeCards')}
|
|
364
|
+
>
|
|
365
|
+
<LayoutGrid className="h-4 w-4" />
|
|
366
|
+
<span className="hidden sm:inline">{t('viewModeCards')}</span>
|
|
367
|
+
</ToggleGroupItem>
|
|
368
|
+
</ToggleGroup>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
304
371
|
</div>
|
|
305
372
|
|
|
306
373
|
{isLoading ? (
|
|
@@ -335,7 +402,9 @@ export default function CrmActivitiesPage() {
|
|
|
335
402
|
<TableHead>{t('table.dueAt')}</TableHead>
|
|
336
403
|
<TableHead>{t('table.status')}</TableHead>
|
|
337
404
|
<TableHead>{t('table.priority')}</TableHead>
|
|
338
|
-
<TableHead className="text-right">
|
|
405
|
+
<TableHead className="text-right">
|
|
406
|
+
{t('table.actions')}
|
|
407
|
+
</TableHead>
|
|
339
408
|
</TableRow>
|
|
340
409
|
</TableHeader>
|
|
341
410
|
<TableBody>
|
|
@@ -347,30 +416,97 @@ export default function CrmActivitiesPage() {
|
|
|
347
416
|
return (
|
|
348
417
|
<TableRow key={item.id}>
|
|
349
418
|
<TableCell>
|
|
350
|
-
<div className="min-w-
|
|
419
|
+
<div className="min-w-65 space-y-1">
|
|
351
420
|
<div className="flex items-center gap-2">
|
|
352
421
|
<TypeIcon className="h-4 w-4 text-muted-foreground" />
|
|
353
422
|
<p className="font-medium">{item.subject}</p>
|
|
354
423
|
</div>
|
|
355
|
-
<p className="line-clamp-2 text-xs text-muted-foreground">
|
|
424
|
+
<p className="line-clamp-2 text-xs text-muted-foreground">
|
|
425
|
+
{item.notes || t('detail.emptyNotes')}
|
|
426
|
+
</p>
|
|
356
427
|
<div className="text-[11px] text-muted-foreground">
|
|
357
|
-
{t('table.createdAt')}:
|
|
428
|
+
{t('table.createdAt')}:{' '}
|
|
429
|
+
{formatDateTime(
|
|
430
|
+
item.created_at,
|
|
431
|
+
getSettingValue,
|
|
432
|
+
currentLocaleCode
|
|
433
|
+
)}
|
|
358
434
|
</div>
|
|
359
435
|
</div>
|
|
360
436
|
</TableCell>
|
|
361
|
-
<TableCell
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
437
|
+
<TableCell>
|
|
438
|
+
<div className="inline-flex items-center gap-2">
|
|
439
|
+
<Users className="h-3.5 w-3.5 text-muted-foreground" />
|
|
440
|
+
<span>{item.person.name}</span>
|
|
441
|
+
</div>
|
|
442
|
+
</TableCell>
|
|
443
|
+
<TableCell>
|
|
444
|
+
<div className="inline-flex items-center gap-2">
|
|
445
|
+
<User className="h-3.5 w-3.5 text-muted-foreground" />
|
|
446
|
+
<span>
|
|
447
|
+
{item.owner_user?.name || t('unassignedOwner')}
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
</TableCell>
|
|
451
|
+
<TableCell>
|
|
452
|
+
{formatDateTime(
|
|
453
|
+
item.due_at,
|
|
454
|
+
getSettingValue,
|
|
455
|
+
currentLocaleCode
|
|
456
|
+
)}
|
|
457
|
+
</TableCell>
|
|
458
|
+
<TableCell>
|
|
459
|
+
<Badge
|
|
460
|
+
variant="outline"
|
|
461
|
+
className={cn(
|
|
462
|
+
'border',
|
|
463
|
+
getStatusBadgeClass(item.status)
|
|
464
|
+
)}
|
|
465
|
+
>
|
|
466
|
+
{t(`status.${item.status}`)}
|
|
467
|
+
</Badge>
|
|
468
|
+
</TableCell>
|
|
469
|
+
<TableCell>
|
|
470
|
+
<Badge
|
|
471
|
+
variant="outline"
|
|
472
|
+
className={cn(
|
|
473
|
+
'border',
|
|
474
|
+
getPriorityBadgeClass(item.priority)
|
|
475
|
+
)}
|
|
476
|
+
>
|
|
477
|
+
{t(`priority.${item.priority}`)}
|
|
478
|
+
</Badge>
|
|
479
|
+
</TableCell>
|
|
366
480
|
<TableCell className="text-right">
|
|
367
481
|
<div className="inline-flex items-center gap-2">
|
|
368
|
-
<Button
|
|
482
|
+
<Button
|
|
483
|
+
type="button"
|
|
484
|
+
variant="outline"
|
|
485
|
+
size="sm"
|
|
486
|
+
onClick={() => {
|
|
487
|
+
setSelectedActivityId(item.id);
|
|
488
|
+
setDetailOpen(true);
|
|
489
|
+
}}
|
|
490
|
+
>
|
|
369
491
|
<Eye className="mr-2 h-3.5 w-3.5" />
|
|
370
492
|
{t('actions.view')}
|
|
371
493
|
</Button>
|
|
372
|
-
<Button
|
|
373
|
-
|
|
494
|
+
<Button
|
|
495
|
+
type="button"
|
|
496
|
+
variant="outline"
|
|
497
|
+
size="sm"
|
|
498
|
+
onClick={() => handleComplete(item.id)}
|
|
499
|
+
disabled={isCompleted || isCompleting}
|
|
500
|
+
className={cn(
|
|
501
|
+
(isCompleted || isCompleting) &&
|
|
502
|
+
'cursor-not-allowed opacity-60'
|
|
503
|
+
)}
|
|
504
|
+
>
|
|
505
|
+
{isCompleting ? (
|
|
506
|
+
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
|
507
|
+
) : (
|
|
508
|
+
<CheckCircle2 className="mr-2 h-3.5 w-3.5" />
|
|
509
|
+
)}
|
|
374
510
|
{t('actions.complete')}
|
|
375
511
|
</Button>
|
|
376
512
|
</div>
|
|
@@ -382,51 +518,129 @@ export default function CrmActivitiesPage() {
|
|
|
382
518
|
</Table>
|
|
383
519
|
</div>
|
|
384
520
|
) : (
|
|
385
|
-
<div className="
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const isCompleted = item.status === 'completed';
|
|
391
|
-
const isCompleting = completingActivityId === item.id;
|
|
521
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
|
522
|
+
{paginate.data.map((item) => {
|
|
523
|
+
const TypeIcon = getTypeIcon(item.type);
|
|
524
|
+
const isCompleted = item.status === 'completed';
|
|
525
|
+
const isCompleting = completingActivityId === item.id;
|
|
392
526
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
<
|
|
527
|
+
return (
|
|
528
|
+
<Card
|
|
529
|
+
key={item.id}
|
|
530
|
+
className="h-full overflow-hidden border-border/70 py-0"
|
|
531
|
+
>
|
|
532
|
+
<CardContent className="space-y-3 p-4">
|
|
533
|
+
<div className="flex items-start justify-between gap-3">
|
|
534
|
+
<div className="min-w-0 space-y-1">
|
|
535
|
+
<div className="flex items-center gap-2">
|
|
536
|
+
<TypeIcon className="h-4 w-4 text-muted-foreground" />
|
|
537
|
+
<p className="line-clamp-2 text-sm font-semibold">
|
|
538
|
+
{item.subject}
|
|
539
|
+
</p>
|
|
403
540
|
</div>
|
|
404
|
-
<
|
|
541
|
+
<p className="line-clamp-2 text-xs text-muted-foreground">
|
|
542
|
+
{item.notes || t('detail.emptyNotes')}
|
|
543
|
+
</p>
|
|
544
|
+
</div>
|
|
545
|
+
<Badge
|
|
546
|
+
variant="outline"
|
|
547
|
+
className={cn(
|
|
548
|
+
'shrink-0 border',
|
|
549
|
+
getStatusBadgeClass(item.status)
|
|
550
|
+
)}
|
|
551
|
+
>
|
|
552
|
+
{t(`status.${item.status}`)}
|
|
553
|
+
</Badge>
|
|
554
|
+
</div>
|
|
555
|
+
<div className="grid gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-xs">
|
|
556
|
+
<div className="flex items-center justify-between gap-2">
|
|
557
|
+
<span className="text-muted-foreground">
|
|
558
|
+
{t('timeline.dueLabel')}
|
|
559
|
+
</span>
|
|
560
|
+
<span className="font-medium text-foreground">
|
|
561
|
+
{formatDateTime(
|
|
562
|
+
item.due_at,
|
|
563
|
+
getSettingValue,
|
|
564
|
+
currentLocaleCode
|
|
565
|
+
)}
|
|
566
|
+
</span>
|
|
405
567
|
</div>
|
|
406
|
-
<div className="
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
<
|
|
568
|
+
<div className="flex items-center justify-between gap-2">
|
|
569
|
+
<span className="text-muted-foreground">
|
|
570
|
+
{t('timeline.createdLabel')}
|
|
571
|
+
</span>
|
|
572
|
+
<span className="font-medium text-foreground">
|
|
573
|
+
{formatDateTime(
|
|
574
|
+
item.created_at,
|
|
575
|
+
getSettingValue,
|
|
576
|
+
currentLocaleCode
|
|
577
|
+
)}
|
|
578
|
+
</span>
|
|
411
579
|
</div>
|
|
412
580
|
<div className="flex items-center justify-between gap-2">
|
|
413
|
-
<
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
<Button type="button" variant="outline" size="sm" onClick={() => handleComplete(item.id)} disabled={isCompleted || isCompleting} className={cn((isCompleted || isCompleting) && 'cursor-not-allowed opacity-60')}>
|
|
420
|
-
{isCompleting ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : <CheckCircle2 className="mr-2 h-3.5 w-3.5" />}
|
|
421
|
-
{t('actions.complete')}
|
|
422
|
-
</Button>
|
|
423
|
-
</div>
|
|
581
|
+
<span className="text-muted-foreground">
|
|
582
|
+
{t('table.person')}
|
|
583
|
+
</span>
|
|
584
|
+
<span className="truncate font-medium text-foreground">
|
|
585
|
+
{item.person.name}
|
|
586
|
+
</span>
|
|
424
587
|
</div>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
588
|
+
<div className="flex items-center justify-between gap-2">
|
|
589
|
+
<span className="text-muted-foreground">
|
|
590
|
+
{t('table.owner')}
|
|
591
|
+
</span>
|
|
592
|
+
<span className="truncate font-medium text-foreground">
|
|
593
|
+
{item.owner_user?.name || t('unassignedOwner')}
|
|
594
|
+
</span>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
<div className="flex items-center justify-between gap-2">
|
|
598
|
+
<Badge
|
|
599
|
+
variant="outline"
|
|
600
|
+
className={cn(
|
|
601
|
+
'border',
|
|
602
|
+
getPriorityBadgeClass(item.priority)
|
|
603
|
+
)}
|
|
604
|
+
>
|
|
605
|
+
{t(`priority.${item.priority}`)}
|
|
606
|
+
</Badge>
|
|
607
|
+
<div className="inline-flex items-center gap-2">
|
|
608
|
+
<Button
|
|
609
|
+
type="button"
|
|
610
|
+
variant="outline"
|
|
611
|
+
size="sm"
|
|
612
|
+
onClick={() => {
|
|
613
|
+
setSelectedActivityId(item.id);
|
|
614
|
+
setDetailOpen(true);
|
|
615
|
+
}}
|
|
616
|
+
>
|
|
617
|
+
<Eye className="mr-2 h-3.5 w-3.5" />
|
|
618
|
+
{t('actions.view')}
|
|
619
|
+
</Button>
|
|
620
|
+
<Button
|
|
621
|
+
type="button"
|
|
622
|
+
variant="outline"
|
|
623
|
+
size="sm"
|
|
624
|
+
onClick={() => handleComplete(item.id)}
|
|
625
|
+
disabled={isCompleted || isCompleting}
|
|
626
|
+
className={cn(
|
|
627
|
+
(isCompleted || isCompleting) &&
|
|
628
|
+
'cursor-not-allowed opacity-60'
|
|
629
|
+
)}
|
|
630
|
+
>
|
|
631
|
+
{isCompleting ? (
|
|
632
|
+
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
|
633
|
+
) : (
|
|
634
|
+
<CheckCircle2 className="mr-2 h-3.5 w-3.5" />
|
|
635
|
+
)}
|
|
636
|
+
{t('actions.complete')}
|
|
637
|
+
</Button>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
</CardContent>
|
|
641
|
+
</Card>
|
|
642
|
+
);
|
|
643
|
+
})}
|
|
430
644
|
</div>
|
|
431
645
|
)}
|
|
432
646
|
|