@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.
Files changed (78) hide show
  1. package/dist/contact.module.d.ts.map +1 -1
  2. package/dist/contact.module.js +2 -0
  3. package/dist/contact.module.js.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/person/person.service.d.ts +2 -0
  9. package/dist/person/person.service.d.ts.map +1 -1
  10. package/dist/person/person.service.js +111 -127
  11. package/dist/person/person.service.js.map +1 -1
  12. package/dist/person/person.service.spec.d.ts +2 -0
  13. package/dist/person/person.service.spec.d.ts.map +1 -0
  14. package/dist/person/person.service.spec.js +106 -0
  15. package/dist/person/person.service.spec.js.map +1 -0
  16. package/dist/proposal/dto/proposal.dto.d.ts +152 -0
  17. package/dist/proposal/dto/proposal.dto.d.ts.map +1 -0
  18. package/dist/proposal/dto/proposal.dto.js +396 -0
  19. package/dist/proposal/dto/proposal.dto.js.map +1 -0
  20. package/dist/proposal/proposal-contract.subscriber.d.ts +11 -0
  21. package/dist/proposal/proposal-contract.subscriber.d.ts.map +1 -0
  22. package/dist/proposal/proposal-contract.subscriber.js +51 -0
  23. package/dist/proposal/proposal-contract.subscriber.js.map +1 -0
  24. package/dist/proposal/proposal-event.types.d.ts +122 -0
  25. package/dist/proposal/proposal-event.types.d.ts.map +1 -0
  26. package/dist/proposal/proposal-event.types.js +13 -0
  27. package/dist/proposal/proposal-event.types.js.map +1 -0
  28. package/dist/proposal/proposal.controller.d.ts +56 -0
  29. package/dist/proposal/proposal.controller.d.ts.map +1 -0
  30. package/dist/proposal/proposal.controller.js +191 -0
  31. package/dist/proposal/proposal.controller.js.map +1 -0
  32. package/dist/proposal/proposal.module.d.ts +3 -0
  33. package/dist/proposal/proposal.module.d.ts.map +1 -0
  34. package/dist/proposal/proposal.module.js +32 -0
  35. package/dist/proposal/proposal.module.js.map +1 -0
  36. package/dist/proposal/proposal.service.d.ts +100 -0
  37. package/dist/proposal/proposal.service.d.ts.map +1 -0
  38. package/dist/proposal/proposal.service.js +2137 -0
  39. package/dist/proposal/proposal.service.js.map +1 -0
  40. package/dist/proposal/proposal.service.spec.d.ts +2 -0
  41. package/dist/proposal/proposal.service.spec.d.ts.map +1 -0
  42. package/dist/proposal/proposal.service.spec.js +175 -0
  43. package/dist/proposal/proposal.service.spec.js.map +1 -0
  44. package/hedhog/data/menu.yaml +35 -18
  45. package/hedhog/data/route.yaml +44 -0
  46. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +517 -346
  47. package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +42 -17
  48. package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +1 -1
  49. package/hedhog/frontend/app/activities/page.tsx.ejs +315 -101
  50. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +172 -22
  51. package/hedhog/frontend/app/page.tsx.ejs +1 -1
  52. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1 -1
  53. package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +253 -210
  54. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +1661 -0
  55. package/hedhog/frontend/app/pipeline/page.tsx.ejs +30 -4
  56. package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +773 -0
  57. package/hedhog/frontend/app/proposals/approvals/page.tsx.ejs +5 -0
  58. package/hedhog/frontend/app/proposals/page.tsx.ejs +5 -0
  59. package/hedhog/frontend/app/reports/page.tsx.ejs +431 -375
  60. package/hedhog/frontend/messages/en.json +236 -43
  61. package/hedhog/frontend/messages/pt.json +235 -42
  62. package/hedhog/table/proposal.yaml +112 -0
  63. package/hedhog/table/proposal_approval.yaml +63 -0
  64. package/hedhog/table/proposal_document.yaml +77 -0
  65. package/hedhog/table/proposal_item.yaml +64 -0
  66. package/hedhog/table/proposal_revision.yaml +78 -0
  67. package/package.json +5 -4
  68. package/src/contact.module.ts +2 -0
  69. package/src/index.ts +3 -0
  70. package/src/person/person.service.spec.ts +143 -0
  71. package/src/person/person.service.ts +147 -158
  72. package/src/proposal/dto/proposal.dto.ts +341 -0
  73. package/src/proposal/proposal-contract.subscriber.ts +43 -0
  74. package/src/proposal/proposal-event.types.ts +130 -0
  75. package/src/proposal/proposal.controller.ts +168 -0
  76. package/src/proposal/proposal.module.ts +19 -0
  77. package/src/proposal/proposal.service.spec.ts +196 -0
  78. 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 { ActivityDetail, ActivityPriority, ActivityStatus } from './activity-types';
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 { data: activity, isLoading, isFetching } = useQuery<ActivityDetail | null>({
83
- queryKey: ['contact-activity-detail', activityId, refreshKey, currentLocaleCode],
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('border', getStatusBadgeClass(activity.status))}
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 label={t('detail.person')} value={activity.person.name} />
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 label={t('detail.type')} value={t(`type.${activity.type}`)} />
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 label={t('detail.createdBy')} value={createdByName} />
226
+ <DetailRow
227
+ label={t('detail.createdBy')}
228
+ value={createdByName}
229
+ />
211
230
  <DetailRow
212
231
  label={t('detail.completedBy')}
213
- value={isCompleted ? completedByName : t('detail.notCompleted')}
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 type="button" variant="outline" onClick={() => onOpenChange(false)}>
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' | 'timeline';
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') return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-700';
73
- if (status === 'overdue') return 'border-red-500/25 bg-red-500/10 text-red-700';
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') return 'border-red-500/25 bg-red-500/10 text-red-700';
79
- if (priority === 'medium') return 'border-sky-500/25 bg-sky-500/10 text-sky-700';
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>(null);
100
+ const [selectedActivityId, setSelectedActivityId] = useState<number | null>(
101
+ null
102
+ );
97
103
  const [detailOpen, setDetailOpen] = useState(false);
98
- const [completingActivityId, setCompletingActivityId] = useState<number | null>(null);
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(() => setDebouncedSearch(searchInput.trim()), 300);
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 === 'timeline') setViewMode(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 { data: stats = { total: 0, pending: 0, overdue: 0, completed: 0 }, refetch: refetchStats } =
116
- useQuery<ActivityStats>({
117
- queryKey: ['contact-activities-stats', currentLocaleCode],
118
- queryFn: async () => {
119
- const response = await request<ActivityStats>({
120
- url: '/person/activities/stats',
121
- method: 'GET',
122
- });
123
- return response.data;
124
- },
125
- placeholderData: (previous) =>
126
- previous ?? { total: 0, pending: 0, overdue: 0, completed: 0 },
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
- { key: 'total', title: t('stats.total'), value: stats.total, icon: Activity, accentClassName: 'from-violet-500/20 via-fuchsia-500/10 to-transparent', iconContainerClassName: 'bg-violet-500/10 text-violet-700' },
237
- { key: 'pending', title: t('stats.pending'), value: stats.pending, icon: Clock3, accentClassName: 'from-amber-500/20 via-yellow-500/10 to-transparent', iconContainerClassName: 'bg-amber-500/10 text-amber-700' },
238
- { key: 'overdue', title: t('stats.overdue'), value: stats.overdue, icon: CircleAlert, accentClassName: 'from-red-500/20 via-rose-500/10 to-transparent', iconContainerClassName: 'bg-red-500/10 text-red-700' },
239
- { key: 'completed', title: t('stats.completed'), value: stats.completed, icon: CalendarCheck, accentClassName: 'from-emerald-500/20 via-green-500/10 to-transparent', iconContainerClassName: 'bg-emerald-500/10 text-emerald-700' },
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 !== 'timeline') return;
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({ url: `/person/activities/${activityId}/complete`, method: 'POST' });
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
- <SearchBar
282
- searchQuery={searchInput}
283
- onSearchChange={(value) => {
284
- setSearchInput(value);
285
- setPage(1);
286
- }}
287
- onSearch={() => setPage(1)}
288
- placeholder={t('filters.searchPlaceholder')}
289
- controls={controls}
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
- <div className="flex items-center justify-end gap-3">
293
- <span className="text-xs font-medium text-muted-foreground">{t('viewMode')}</span>
294
- <ToggleGroup type="single" value={viewMode} onValueChange={handleViewModeChange} variant="outline" size="sm" aria-label={t('viewMode')}>
295
- <ToggleGroupItem value="table" className="gap-1.5 px-2.5" aria-label={t('viewModeTable')}>
296
- <List className="h-4 w-4" />
297
- <span className="hidden sm:inline">{t('viewModeTable')}</span>
298
- </ToggleGroupItem>
299
- <ToggleGroupItem value="timeline" className="gap-1.5 px-2.5" aria-label={t('viewModeTimeline')}>
300
- <LayoutGrid className="h-4 w-4" />
301
- <span className="hidden sm:inline">{t('viewModeTimeline')}</span>
302
- </ToggleGroupItem>
303
- </ToggleGroup>
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">{t('table.actions')}</TableHead>
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-[260px] space-y-1">
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">{item.notes || t('detail.emptyNotes')}</p>
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')}: {formatDateTime(item.created_at, getSettingValue, currentLocaleCode)}
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><div className="inline-flex items-center gap-2"><Users className="h-3.5 w-3.5 text-muted-foreground" /><span>{item.person.name}</span></div></TableCell>
362
- <TableCell><div className="inline-flex items-center gap-2"><User className="h-3.5 w-3.5 text-muted-foreground" /><span>{item.owner_user?.name || t('unassignedOwner')}</span></div></TableCell>
363
- <TableCell>{formatDateTime(item.due_at, getSettingValue, currentLocaleCode)}</TableCell>
364
- <TableCell><Badge variant="outline" className={cn('border', getStatusBadgeClass(item.status))}>{t(`status.${item.status}`)}</Badge></TableCell>
365
- <TableCell><Badge variant="outline" className={cn('border', getPriorityBadgeClass(item.priority))}>{t(`priority.${item.priority}`)}</Badge></TableCell>
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 type="button" variant="outline" size="sm" onClick={() => { setSelectedActivityId(item.id); setDetailOpen(true); }}>
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 type="button" variant="outline" size="sm" onClick={() => handleComplete(item.id)} disabled={isCompleted || isCompleting} className={cn((isCompleted || isCompleting) && 'cursor-not-allowed opacity-60')}>
373
- {isCompleting ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : <CheckCircle2 className="mr-2 h-3.5 w-3.5" />}
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="space-y-4">
386
- <div className="rounded-md border border-dashed border-border/70 bg-muted/20 px-4 py-3 text-sm text-muted-foreground">{t('timeline.description')}</div>
387
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
388
- {paginate.data.map((item) => {
389
- const TypeIcon = getTypeIcon(item.type);
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
- return (
394
- <Card key={item.id} className="h-full overflow-hidden border-border/70 py-0">
395
- <CardContent className="space-y-3 p-4">
396
- <div className="flex items-start justify-between gap-3">
397
- <div className="min-w-0 space-y-1">
398
- <div className="flex items-center gap-2">
399
- <TypeIcon className="h-4 w-4 text-muted-foreground" />
400
- <p className="line-clamp-2 text-sm font-semibold">{item.subject}</p>
401
- </div>
402
- <p className="line-clamp-2 text-xs text-muted-foreground">{item.notes || t('detail.emptyNotes')}</p>
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
- <Badge variant="outline" className={cn('shrink-0 border', getStatusBadgeClass(item.status))}>{t(`status.${item.status}`)}</Badge>
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="grid gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-xs">
407
- <div className="flex items-center justify-between gap-2"><span className="text-muted-foreground">{t('timeline.dueLabel')}</span><span className="font-medium text-foreground">{formatDateTime(item.due_at, getSettingValue, currentLocaleCode)}</span></div>
408
- <div className="flex items-center justify-between gap-2"><span className="text-muted-foreground">{t('timeline.createdLabel')}</span><span className="font-medium text-foreground">{formatDateTime(item.created_at, getSettingValue, currentLocaleCode)}</span></div>
409
- <div className="flex items-center justify-between gap-2"><span className="text-muted-foreground">{t('table.person')}</span><span className="truncate font-medium text-foreground">{item.person.name}</span></div>
410
- <div className="flex items-center justify-between gap-2"><span className="text-muted-foreground">{t('table.owner')}</span><span className="truncate font-medium text-foreground">{item.owner_user?.name || t('unassignedOwner')}</span></div>
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
- <Badge variant="outline" className={cn('border', getPriorityBadgeClass(item.priority))}>{t(`priority.${item.priority}`)}</Badge>
414
- <div className="inline-flex items-center gap-2">
415
- <Button type="button" variant="outline" size="sm" onClick={() => { setSelectedActivityId(item.id); setDetailOpen(true); }}>
416
- <Eye className="mr-2 h-3.5 w-3.5" />
417
- {t('actions.view')}
418
- </Button>
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
- </CardContent>
426
- </Card>
427
- );
428
- })}
429
- </div>
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