@nextsparkjs/theme-crm 0.1.0-beta.1

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 (140) hide show
  1. package/CRM_PLAN.md +343 -0
  2. package/about.md +122 -0
  3. package/config/app.config.ts +185 -0
  4. package/config/billing.config.ts +187 -0
  5. package/config/dashboard.config.ts +372 -0
  6. package/config/dev.config.ts +55 -0
  7. package/config/features.config.ts +336 -0
  8. package/config/flows.config.ts +511 -0
  9. package/config/permissions.config.ts +297 -0
  10. package/config/theme.config.ts +111 -0
  11. package/entities/activities/activities.config.ts +61 -0
  12. package/entities/activities/activities.fields.ts +362 -0
  13. package/entities/activities/activities.service.ts +503 -0
  14. package/entities/activities/activities.types.ts +117 -0
  15. package/entities/activities/messages/en.json +123 -0
  16. package/entities/activities/messages/es.json +123 -0
  17. package/entities/activities/migrations/020_activities_table.sql +123 -0
  18. package/entities/activities/migrations/021_activities_metas.sql +114 -0
  19. package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
  20. package/entities/campaigns/campaigns.config.ts +61 -0
  21. package/entities/campaigns/campaigns.fields.ts +413 -0
  22. package/entities/campaigns/campaigns.service.ts +426 -0
  23. package/entities/campaigns/campaigns.types.ts +124 -0
  24. package/entities/campaigns/messages/en.json +145 -0
  25. package/entities/campaigns/messages/es.json +145 -0
  26. package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
  27. package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
  28. package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
  29. package/entities/companies/companies.config.ts +61 -0
  30. package/entities/companies/companies.fields.ts +429 -0
  31. package/entities/companies/companies.service.ts +566 -0
  32. package/entities/companies/companies.types.ts +125 -0
  33. package/entities/companies/messages/en.json +146 -0
  34. package/entities/companies/messages/es.json +146 -0
  35. package/entities/companies/migrations/001_companies_table.sql +150 -0
  36. package/entities/companies/migrations/002_companies_metas.sql +114 -0
  37. package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
  38. package/entities/contacts/contacts.config.ts +61 -0
  39. package/entities/contacts/contacts.fields.ts +359 -0
  40. package/entities/contacts/contacts.service.ts +509 -0
  41. package/entities/contacts/contacts.types.ts +108 -0
  42. package/entities/contacts/messages/en.json +117 -0
  43. package/entities/contacts/messages/es.json +117 -0
  44. package/entities/contacts/migrations/001_contacts_table.sql +134 -0
  45. package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
  46. package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
  47. package/entities/leads/leads.config.ts +61 -0
  48. package/entities/leads/leads.fields.ts +336 -0
  49. package/entities/leads/leads.service.ts +496 -0
  50. package/entities/leads/leads.types.ts +114 -0
  51. package/entities/leads/messages/en.json +132 -0
  52. package/entities/leads/messages/es.json +132 -0
  53. package/entities/leads/migrations/001_leads_table.sql +150 -0
  54. package/entities/leads/migrations/002_leads_metas.sql +120 -0
  55. package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
  56. package/entities/notes/messages/en.json +114 -0
  57. package/entities/notes/messages/es.json +114 -0
  58. package/entities/notes/migrations/020_notes_table.sql +118 -0
  59. package/entities/notes/migrations/021_notes_metas.sql +114 -0
  60. package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
  61. package/entities/notes/notes.config.ts +61 -0
  62. package/entities/notes/notes.fields.ts +283 -0
  63. package/entities/notes/notes.service.ts +320 -0
  64. package/entities/notes/notes.types.ts +102 -0
  65. package/entities/opportunities/messages/en.json +107 -0
  66. package/entities/opportunities/messages/es.json +107 -0
  67. package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
  68. package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
  69. package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
  70. package/entities/opportunities/opportunities.config.ts +61 -0
  71. package/entities/opportunities/opportunities.fields.ts +416 -0
  72. package/entities/opportunities/opportunities.service.ts +525 -0
  73. package/entities/opportunities/opportunities.types.ts +135 -0
  74. package/entities/pipelines/messages/en.json +115 -0
  75. package/entities/pipelines/messages/es.json +115 -0
  76. package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
  77. package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
  78. package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
  79. package/entities/pipelines/pipelines.config.ts +62 -0
  80. package/entities/pipelines/pipelines.fields.ts +193 -0
  81. package/entities/pipelines/pipelines.service.ts +383 -0
  82. package/entities/pipelines/pipelines.types.ts +78 -0
  83. package/entities/products/messages/en.json +135 -0
  84. package/entities/products/messages/es.json +135 -0
  85. package/entities/products/migrations/001_products_table.sql +117 -0
  86. package/entities/products/migrations/002_products_metas.sql +114 -0
  87. package/entities/products/migrations/003_products_sample_data.sql +247 -0
  88. package/entities/products/products.config.ts +62 -0
  89. package/entities/products/products.fields.ts +361 -0
  90. package/entities/products/products.service.ts +437 -0
  91. package/entities/products/products.types.ts +125 -0
  92. package/lib/crm-constants.ts +77 -0
  93. package/lib/crm-utils.ts +185 -0
  94. package/lib/selectors.ts +333 -0
  95. package/messages/en.json +131 -0
  96. package/messages/es.json +131 -0
  97. package/migrations/999_theme_sample_data.sql +473 -0
  98. package/package.json +18 -0
  99. package/pendings.md +205 -0
  100. package/permissions-matrix.md +216 -0
  101. package/styles/components.css +414 -0
  102. package/styles/crm-theme.css +358 -0
  103. package/styles/globals.css +576 -0
  104. package/styles/variables.css +111 -0
  105. package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
  106. package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
  107. package/templates/dashboard/(main)/activities/page.tsx +297 -0
  108. package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
  109. package/templates/dashboard/(main)/companies/page.tsx +296 -0
  110. package/templates/dashboard/(main)/contacts/page.tsx +347 -0
  111. package/templates/dashboard/(main)/layout.tsx +98 -0
  112. package/templates/dashboard/(main)/leads/page.tsx +335 -0
  113. package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
  114. package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
  115. package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
  116. package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
  117. package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
  118. package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
  119. package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
  120. package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
  121. package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
  122. package/templates/dashboard/(main)/products/create/page.tsx +96 -0
  123. package/templates/dashboard/(main)/products/page.tsx +308 -0
  124. package/templates/shared/ActionButtons.tsx +41 -0
  125. package/templates/shared/CRMDashboard.tsx +519 -0
  126. package/templates/shared/CRMDataTable.tsx +441 -0
  127. package/templates/shared/CRMMetricCard.tsx +76 -0
  128. package/templates/shared/CRMMobileNav.tsx +172 -0
  129. package/templates/shared/CRMSidebar.tsx +346 -0
  130. package/templates/shared/CRMTopBar.tsx +265 -0
  131. package/templates/shared/DealCard.tsx +123 -0
  132. package/templates/shared/EntityCard.tsx +58 -0
  133. package/templates/shared/OpportunityForm.tsx +649 -0
  134. package/templates/shared/PipelineForm.tsx +367 -0
  135. package/templates/shared/PipelineKanban.tsx +194 -0
  136. package/templates/shared/QuickFilters.tsx +47 -0
  137. package/templates/shared/StageColumn.tsx +175 -0
  138. package/templates/shared/StageSelect.tsx +177 -0
  139. package/templates/shared/StagesRepeater.tsx +317 -0
  140. package/templates/shared/index.ts +9 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Activity Card Component
3
+ * Professional card design for activities with modern styling
4
+ */
5
+
6
+ 'use client'
7
+
8
+ import React from 'react'
9
+ import { formatRelativeDate } from '@/themes/crm/lib/crm-utils'
10
+ import {
11
+ Phone,
12
+ Mail,
13
+ Users,
14
+ CheckSquare,
15
+ FileText,
16
+ Presentation,
17
+ Monitor,
18
+ Clock,
19
+ Building2,
20
+ User,
21
+ type LucideIcon
22
+ } from 'lucide-react'
23
+ import { cn } from '@nextsparkjs/core/lib/utils'
24
+
25
+ export interface Activity {
26
+ id: string
27
+ type: 'call' | 'email' | 'meeting' | 'task' | 'note' | 'demo' | 'presentation'
28
+ subject: string
29
+ description?: string
30
+ status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'overdue'
31
+ priority: 'low' | 'medium' | 'high' | 'urgent'
32
+ dueDate: string | Date
33
+ completedAt?: string | Date
34
+ assignedTo?: string
35
+ assignedToName?: string
36
+ contactId?: string
37
+ contactName?: string
38
+ companyId?: string
39
+ companyName?: string
40
+ }
41
+
42
+ interface ActivityCardProps {
43
+ activity: Activity
44
+ onClick?: () => void
45
+ }
46
+
47
+ // Type configuration with icons and colors
48
+ const activityTypeConfig: Record<Activity['type'], { icon: LucideIcon; bgClass: string; textClass: string }> = {
49
+ call: { icon: Phone, bgClass: 'bg-[oklch(0.55_0.22_286/0.15)]', textClass: 'text-[oklch(0.55_0.22_286)]' },
50
+ email: { icon: Mail, bgClass: 'bg-[oklch(0.55_0.18_150/0.15)]', textClass: 'text-[oklch(0.55_0.18_150)]' },
51
+ meeting: { icon: Users, bgClass: 'bg-[oklch(0.65_0.18_80/0.15)]', textClass: 'text-[oklch(0.65_0.18_80)]' },
52
+ task: { icon: CheckSquare, bgClass: 'bg-[oklch(0.55_0.2_320/0.15)]', textClass: 'text-[oklch(0.55_0.2_320)]' },
53
+ note: { icon: FileText, bgClass: 'bg-muted', textClass: 'text-muted-foreground' },
54
+ demo: { icon: Monitor, bgClass: 'bg-[oklch(0.55_0.22_286/0.15)]', textClass: 'text-[oklch(0.55_0.22_286)]' },
55
+ presentation: { icon: Presentation, bgClass: 'bg-[oklch(0.65_0.18_80/0.15)]', textClass: 'text-[oklch(0.65_0.18_80)]' },
56
+ }
57
+
58
+ // Status badge configuration
59
+ const statusConfig: Record<Activity['status'], { label: string; className: string }> = {
60
+ scheduled: { label: 'Scheduled', className: 'bg-primary/10 text-primary' },
61
+ in_progress: { label: 'In Progress', className: 'bg-amber-500/10 text-amber-600' },
62
+ completed: { label: 'Completed', className: 'bg-emerald-500/10 text-emerald-600' },
63
+ cancelled: { label: 'Cancelled', className: 'bg-muted text-muted-foreground' },
64
+ overdue: { label: 'Overdue', className: 'bg-destructive/10 text-destructive' },
65
+ }
66
+
67
+ // Priority indicator configuration
68
+ const priorityConfig: Record<Activity['priority'], { className: string }> = {
69
+ low: { className: 'bg-muted-foreground/50' },
70
+ medium: { className: 'bg-amber-500' },
71
+ high: { className: 'bg-destructive' },
72
+ urgent: { className: 'bg-destructive shadow-[0_0_8px_oklch(0.55_0.22_25)]' },
73
+ }
74
+
75
+ // Default fallback config for unknown activity types
76
+ const defaultTypeConfig = { icon: FileText, bgClass: 'bg-muted', textClass: 'text-muted-foreground' }
77
+ const defaultStatusConfig = { label: 'Unknown', className: 'bg-muted text-muted-foreground' }
78
+ const defaultPriorityConfig = { className: 'bg-muted-foreground/50' }
79
+
80
+ export function ActivityCard({ activity, onClick }: ActivityCardProps) {
81
+ const typeConfig = activityTypeConfig[activity.type] || defaultTypeConfig
82
+ const status = statusConfig[activity.status] || defaultStatusConfig
83
+ const priority = priorityConfig[activity.priority] || defaultPriorityConfig
84
+ const Icon = typeConfig.icon
85
+
86
+ return (
87
+ <div className="relative flex gap-4 pb-6 last:pb-0 group">
88
+ {/* Timeline line */}
89
+ <div className="absolute left-5 top-10 bottom-0 w-px bg-border group-last:hidden" />
90
+
91
+ {/* Icon marker */}
92
+ <div className={cn(
93
+ 'relative z-10 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl shadow-sm',
94
+ typeConfig.bgClass
95
+ )}>
96
+ <Icon className={cn('h-5 w-5', typeConfig.textClass)} />
97
+ </div>
98
+
99
+ {/* Content card */}
100
+ <div
101
+ className={cn(
102
+ 'flex-1 rounded-xl border bg-card p-4 shadow-sm transition-all duration-200',
103
+ onClick && 'cursor-pointer hover:shadow-md hover:border-primary/30'
104
+ )}
105
+ onClick={onClick}
106
+ role={onClick ? 'button' : undefined}
107
+ tabIndex={onClick ? 0 : undefined}
108
+ >
109
+ {/* Header */}
110
+ <div className="flex items-start justify-between gap-3 mb-2">
111
+ <div className="flex-1 min-w-0">
112
+ <div className="flex items-center gap-2 mb-1">
113
+ <span className={cn('w-2 h-2 rounded-full shrink-0', priority.className)} />
114
+ <h4 className="font-semibold text-sm text-foreground truncate">
115
+ {activity.subject}
116
+ </h4>
117
+ </div>
118
+
119
+ {activity.description && (
120
+ <p className="text-sm text-muted-foreground line-clamp-2 leading-relaxed">
121
+ {activity.description}
122
+ </p>
123
+ )}
124
+ </div>
125
+
126
+ <span className={cn(
127
+ 'shrink-0 px-2.5 py-1 rounded-md text-xs font-medium capitalize',
128
+ status.className
129
+ )}>
130
+ {status.label}
131
+ </span>
132
+ </div>
133
+
134
+ {/* Meta information */}
135
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-2 mt-3 pt-3 border-t border-border/50">
136
+ {activity.assignedToName && (
137
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
138
+ <div className="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center text-[10px] font-semibold text-primary">
139
+ {activity.assignedToName.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()}
140
+ </div>
141
+ <span>{activity.assignedToName}</span>
142
+ </div>
143
+ )}
144
+
145
+ {activity.companyName && (
146
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
147
+ <Building2 className="w-3.5 h-3.5" />
148
+ <span>{activity.companyName}</span>
149
+ </div>
150
+ )}
151
+
152
+ {activity.contactName && (
153
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
154
+ <User className="w-3.5 h-3.5" />
155
+ <span>{activity.contactName}</span>
156
+ </div>
157
+ )}
158
+
159
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground ml-auto">
160
+ <Clock className="w-3.5 h-3.5" />
161
+ <span>{formatRelativeDate(activity.dueDate)}</span>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ export default ActivityCard
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Activity Timeline Component
3
+ * Professional timeline view with grouped dates and animations
4
+ */
5
+
6
+ 'use client'
7
+
8
+ import React from 'react'
9
+ import { ActivityCard, type Activity } from './ActivityCard'
10
+ import { CalendarDays, Inbox } from 'lucide-react'
11
+ import { cn } from '@nextsparkjs/core/lib/utils'
12
+
13
+ interface ActivityTimelineProps {
14
+ activities: Activity[]
15
+ onActivityClick?: (activity: Activity) => void
16
+ groupByDate?: boolean
17
+ }
18
+
19
+ export function ActivityTimeline({
20
+ activities,
21
+ onActivityClick,
22
+ groupByDate = true,
23
+ }: ActivityTimelineProps) {
24
+ // Group activities by date
25
+ const groupedActivities = activities.reduce((acc, activity) => {
26
+ const date = new Date(activity.dueDate)
27
+ const dateKey = date.toDateString()
28
+
29
+ if (!acc[dateKey]) {
30
+ acc[dateKey] = []
31
+ }
32
+ acc[dateKey].push(activity)
33
+ return acc
34
+ }, {} as Record<string, Activity[]>)
35
+
36
+ // Sort date keys
37
+ const sortedDates = Object.keys(groupedActivities).sort((a, b) => {
38
+ return new Date(a).getTime() - new Date(b).getTime()
39
+ })
40
+
41
+ const formatDateHeader = (dateStr: string): { main: string; sub?: string; isToday?: boolean; isPast?: boolean } => {
42
+ const date = new Date(dateStr)
43
+ const today = new Date()
44
+ const tomorrow = new Date(today)
45
+ tomorrow.setDate(tomorrow.getDate() + 1)
46
+ const yesterday = new Date(today)
47
+ yesterday.setDate(yesterday.getDate() - 1)
48
+
49
+ const isToday = date.toDateString() === today.toDateString()
50
+ const isPast = date < today && !isToday
51
+
52
+ if (isToday) {
53
+ return { main: 'Today', sub: date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }), isToday: true }
54
+ }
55
+ if (date.toDateString() === tomorrow.toDateString()) {
56
+ return { main: 'Tomorrow', sub: date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) }
57
+ }
58
+ if (date.toDateString() === yesterday.toDateString()) {
59
+ return { main: 'Yesterday', sub: date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }), isPast: true }
60
+ }
61
+
62
+ return {
63
+ main: date.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }),
64
+ sub: date.getFullYear() !== today.getFullYear() ? date.getFullYear().toString() : undefined,
65
+ isPast
66
+ }
67
+ }
68
+
69
+ if (activities.length === 0) {
70
+ return (
71
+ <div className="flex flex-col items-center justify-center py-16 px-4">
72
+ <div className="w-16 h-16 rounded-2xl bg-muted flex items-center justify-center mb-4">
73
+ <Inbox className="w-8 h-8 text-muted-foreground" />
74
+ </div>
75
+ <h3 className="text-lg font-semibold text-foreground mb-2">
76
+ No activities yet
77
+ </h3>
78
+ <p className="text-sm text-muted-foreground text-center max-w-sm">
79
+ Schedule calls, meetings, and tasks to keep track of your sales activities.
80
+ </p>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ if (!groupByDate) {
86
+ return (
87
+ <div className="space-y-0">
88
+ {activities.map((activity, index) => (
89
+ <div
90
+ key={activity.id}
91
+ className="animate-in fade-in slide-in-from-bottom-2"
92
+ style={{ animationDelay: `${index * 50}ms`, animationFillMode: 'backwards' }}
93
+ >
94
+ <ActivityCard
95
+ activity={activity}
96
+ onClick={() => onActivityClick?.(activity)}
97
+ />
98
+ </div>
99
+ ))}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ return (
105
+ <div className="space-y-8">
106
+ {sortedDates.map((dateKey, groupIndex) => {
107
+ const dateInfo = formatDateHeader(dateKey)
108
+
109
+ return (
110
+ <div
111
+ key={dateKey}
112
+ className="animate-in fade-in slide-in-from-bottom-3"
113
+ style={{ animationDelay: `${groupIndex * 100}ms`, animationFillMode: 'backwards' }}
114
+ >
115
+ {/* Date header */}
116
+ <div className="flex items-center gap-3 mb-4">
117
+ <div className={cn(
118
+ 'flex items-center justify-center w-9 h-9 rounded-lg',
119
+ dateInfo.isToday ? 'bg-primary text-primary-foreground' : 'bg-muted'
120
+ )}>
121
+ <CalendarDays className={cn(
122
+ 'w-4 h-4',
123
+ dateInfo.isToday ? 'text-primary-foreground' : 'text-muted-foreground'
124
+ )} />
125
+ </div>
126
+ <div>
127
+ <h3 className={cn(
128
+ 'text-base font-semibold',
129
+ dateInfo.isToday ? 'text-primary' : dateInfo.isPast ? 'text-muted-foreground' : 'text-foreground'
130
+ )}>
131
+ {dateInfo.main}
132
+ </h3>
133
+ {dateInfo.sub && (
134
+ <p className="text-xs text-muted-foreground">{dateInfo.sub}</p>
135
+ )}
136
+ </div>
137
+ <div className="flex-1 h-px bg-border ml-2" />
138
+ <span className="text-xs font-medium text-muted-foreground bg-muted px-2 py-1 rounded-md">
139
+ {groupedActivities[dateKey].length} {groupedActivities[dateKey].length === 1 ? 'activity' : 'activities'}
140
+ </span>
141
+ </div>
142
+
143
+ {/* Activities list */}
144
+ <div className="space-y-0 pl-1">
145
+ {groupedActivities[dateKey].map((activity, index) => (
146
+ <div
147
+ key={activity.id}
148
+ className="animate-in fade-in slide-in-from-left-2"
149
+ style={{ animationDelay: `${(groupIndex * 100) + (index * 50)}ms`, animationFillMode: 'backwards' }}
150
+ >
151
+ <ActivityCard
152
+ activity={activity}
153
+ onClick={() => onActivityClick?.(activity)}
154
+ />
155
+ </div>
156
+ ))}
157
+ </div>
158
+ </div>
159
+ )
160
+ })}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ export default ActivityTimeline
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Activities Page
3
+ * Professional activities management with timeline view and stats
4
+ */
5
+
6
+ 'use client'
7
+
8
+ import { useRouter } from 'next/navigation'
9
+ import { useEffect, useState } from 'react'
10
+ import { ActivityTimeline } from './components/ActivityTimeline'
11
+ import { Button } from '@nextsparkjs/core/components/ui/button'
12
+ import {
13
+ Plus,
14
+ CalendarDays,
15
+ LayoutList,
16
+ Phone,
17
+ Mail,
18
+ Users,
19
+ CheckSquare,
20
+ FileText,
21
+ Activity,
22
+ Clock,
23
+ CheckCircle2
24
+ } from 'lucide-react'
25
+ import type { Activity as ActivityType } from './components/ActivityCard'
26
+ import { fetchWithTeam } from '@nextsparkjs/core/lib/api/entities'
27
+ import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
28
+ import { cn } from '@nextsparkjs/core/lib/utils'
29
+
30
+ type ViewMode = 'timeline' | 'list'
31
+ type FilterType = 'all' | 'call' | 'email' | 'meeting' | 'task' | 'note'
32
+
33
+ // Filter configuration with icons
34
+ const filterConfig: Record<FilterType, { icon: typeof Phone; label: string }> = {
35
+ all: { icon: Activity, label: 'All' },
36
+ call: { icon: Phone, label: 'Calls' },
37
+ email: { icon: Mail, label: 'Emails' },
38
+ meeting: { icon: Users, label: 'Meetings' },
39
+ task: { icon: CheckSquare, label: 'Tasks' },
40
+ note: { icon: FileText, label: 'Notes' },
41
+ }
42
+
43
+ export default function ActivitiesPage() {
44
+ const router = useRouter()
45
+ const { currentTeam, isLoading: teamLoading } = useTeamContext()
46
+ const [activities, setActivities] = useState<ActivityType[]>([])
47
+ const [isLoading, setIsLoading] = useState(true)
48
+ const [viewMode, setViewMode] = useState<ViewMode>('timeline')
49
+ const [filter, setFilter] = useState<FilterType>('all')
50
+
51
+ useEffect(() => {
52
+ if (teamLoading || !currentTeam) return
53
+
54
+ async function fetchActivities() {
55
+ try {
56
+ const response = await fetchWithTeam('/api/v1/activities')
57
+ if (!response.ok) throw new Error('Failed to fetch activities')
58
+ const result = await response.json()
59
+ const data = result.data || []
60
+
61
+ const transformedActivities: ActivityType[] = data
62
+ .map((act: any) => ({
63
+ id: act.id,
64
+ type: act.type,
65
+ subject: act.subject,
66
+ description: act.description,
67
+ status: act.status,
68
+ priority: act.priority || 'medium',
69
+ dueDate: act.dueDate,
70
+ completedAt: act.completedAt,
71
+ assignedTo: act.assignedTo,
72
+ assignedToName: act.assignedToName,
73
+ contactId: act.contactId,
74
+ contactName: act.contactName,
75
+ companyId: act.companyId,
76
+ companyName: act.companyName,
77
+ }))
78
+ .sort((a: ActivityType, b: ActivityType) =>
79
+ new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
80
+ )
81
+
82
+ setActivities(transformedActivities)
83
+ } catch (error) {
84
+ console.error('Error loading activities:', error)
85
+ } finally {
86
+ setIsLoading(false)
87
+ }
88
+ }
89
+
90
+ fetchActivities()
91
+ }, [teamLoading, currentTeam])
92
+
93
+ const handleActivityClick = (activity: ActivityType) => {
94
+ router.push(`/dashboard/activities/${activity.id}`)
95
+ }
96
+
97
+ const handleAddActivity = () => {
98
+ router.push('/dashboard/activities/create')
99
+ }
100
+
101
+ // Filter activities by type
102
+ const filteredActivities = filter === 'all'
103
+ ? activities
104
+ : activities.filter(act => act.type === filter)
105
+
106
+ // Calculate stats
107
+ const stats = {
108
+ total: activities.length,
109
+ scheduled: activities.filter(a => a.status === 'scheduled').length,
110
+ completed: activities.filter(a => a.status === 'completed').length,
111
+ overdue: activities.filter(a => a.status === 'overdue').length,
112
+ }
113
+
114
+ // Calculate filter counts
115
+ const filterCounts: Record<FilterType, number> = {
116
+ all: activities.length,
117
+ call: activities.filter(a => a.type === 'call').length,
118
+ email: activities.filter(a => a.type === 'email').length,
119
+ meeting: activities.filter(a => a.type === 'meeting').length,
120
+ task: activities.filter(a => a.type === 'task').length,
121
+ note: activities.filter(a => a.type === 'note').length,
122
+ }
123
+
124
+ if (isLoading) {
125
+ return (
126
+ <div className="p-6 space-y-6">
127
+ {/* Header skeleton */}
128
+ <div className="flex items-center justify-between">
129
+ <div className="space-y-2">
130
+ <div className="h-8 w-48 bg-muted animate-pulse rounded-lg" />
131
+ <div className="h-4 w-64 bg-muted animate-pulse rounded-md" />
132
+ </div>
133
+ <div className="h-10 w-36 bg-muted animate-pulse rounded-lg" />
134
+ </div>
135
+
136
+ {/* Stats skeleton */}
137
+ <div className="grid grid-cols-4 gap-4">
138
+ {[1, 2, 3, 4].map(i => (
139
+ <div key={i} className="h-20 bg-muted animate-pulse rounded-xl" />
140
+ ))}
141
+ </div>
142
+
143
+ {/* Content skeleton */}
144
+ <div className="space-y-4">
145
+ {[1, 2, 3].map(i => (
146
+ <div key={i} className="h-28 bg-muted animate-pulse rounded-xl" />
147
+ ))}
148
+ </div>
149
+ </div>
150
+ )
151
+ }
152
+
153
+ return (
154
+ <div className="p-6 space-y-6">
155
+ {/* Header */}
156
+ <div className="flex items-start justify-between">
157
+ <div>
158
+ <h1 className="text-2xl font-bold text-foreground tracking-tight">
159
+ Activities
160
+ </h1>
161
+ <p className="text-sm text-muted-foreground mt-1">
162
+ Track and manage your sales activities
163
+ </p>
164
+ </div>
165
+
166
+ <div className="flex items-center gap-3">
167
+ {/* View toggle */}
168
+ <div className="flex p-1 bg-muted rounded-lg">
169
+ <button
170
+ className={cn(
171
+ 'flex items-center justify-center w-9 h-9 rounded-md transition-all',
172
+ viewMode === 'timeline'
173
+ ? 'bg-background shadow-sm text-primary'
174
+ : 'text-muted-foreground hover:text-foreground'
175
+ )}
176
+ onClick={() => setViewMode('timeline')}
177
+ title="Timeline view"
178
+ >
179
+ <CalendarDays className="w-4 h-4" />
180
+ </button>
181
+ <button
182
+ className={cn(
183
+ 'flex items-center justify-center w-9 h-9 rounded-md transition-all',
184
+ viewMode === 'list'
185
+ ? 'bg-background shadow-sm text-primary'
186
+ : 'text-muted-foreground hover:text-foreground'
187
+ )}
188
+ onClick={() => setViewMode('list')}
189
+ title="List view"
190
+ >
191
+ <LayoutList className="w-4 h-4" />
192
+ </button>
193
+ </div>
194
+
195
+ <Button onClick={handleAddActivity} className="gap-2" data-cy="activities-add">
196
+ <Plus className="w-4 h-4" />
197
+ New Activity
198
+ </Button>
199
+ </div>
200
+ </div>
201
+
202
+ {/* Stats cards */}
203
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
204
+ <div className="bg-card border rounded-xl p-4">
205
+ <div className="flex items-center gap-3">
206
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
207
+ <Activity className="w-5 h-5 text-primary" />
208
+ </div>
209
+ <div>
210
+ <p className="text-2xl font-bold text-foreground">{stats.total}</p>
211
+ <p className="text-xs text-muted-foreground">Total Activities</p>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ <div className="bg-card border rounded-xl p-4">
217
+ <div className="flex items-center gap-3">
218
+ <div className="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center">
219
+ <Clock className="w-5 h-5 text-amber-600" />
220
+ </div>
221
+ <div>
222
+ <p className="text-2xl font-bold text-foreground">{stats.scheduled}</p>
223
+ <p className="text-xs text-muted-foreground">Scheduled</p>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <div className="bg-card border rounded-xl p-4">
229
+ <div className="flex items-center gap-3">
230
+ <div className="w-10 h-10 rounded-lg bg-emerald-500/10 flex items-center justify-center">
231
+ <CheckCircle2 className="w-5 h-5 text-emerald-600" />
232
+ </div>
233
+ <div>
234
+ <p className="text-2xl font-bold text-foreground">{stats.completed}</p>
235
+ <p className="text-xs text-muted-foreground">Completed</p>
236
+ </div>
237
+ </div>
238
+ </div>
239
+
240
+ <div className="bg-card border rounded-xl p-4">
241
+ <div className="flex items-center gap-3">
242
+ <div className="w-10 h-10 rounded-lg bg-destructive/10 flex items-center justify-center">
243
+ <Clock className="w-5 h-5 text-destructive" />
244
+ </div>
245
+ <div>
246
+ <p className="text-2xl font-bold text-foreground">{stats.overdue}</p>
247
+ <p className="text-xs text-muted-foreground">Overdue</p>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ {/* Filter pills */}
254
+ <div className="flex flex-wrap gap-2">
255
+ {(Object.keys(filterConfig) as FilterType[]).map((key) => {
256
+ const config = filterConfig[key]
257
+ const Icon = config.icon
258
+ const count = filterCounts[key]
259
+ const isActive = filter === key
260
+
261
+ return (
262
+ <button
263
+ key={key}
264
+ onClick={() => setFilter(key)}
265
+ className={cn(
266
+ 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
267
+ isActive
268
+ ? 'bg-primary text-primary-foreground shadow-sm'
269
+ : 'bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground'
270
+ )}
271
+ >
272
+ <Icon className="w-4 h-4" />
273
+ {config.label}
274
+ <span className={cn(
275
+ 'ml-1 px-1.5 py-0.5 rounded text-xs',
276
+ isActive
277
+ ? 'bg-primary-foreground/20 text-primary-foreground'
278
+ : 'bg-background text-muted-foreground'
279
+ )}>
280
+ {count}
281
+ </span>
282
+ </button>
283
+ )
284
+ })}
285
+ </div>
286
+
287
+ {/* Timeline */}
288
+ <div className="mt-2" data-cy="activities-list">
289
+ <ActivityTimeline
290
+ activities={filteredActivities}
291
+ onActivityClick={handleActivityClick}
292
+ groupByDate={viewMode === 'timeline'}
293
+ />
294
+ </div>
295
+ </div>
296
+ )
297
+ }