@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,265 @@
1
+ /**
2
+ * CRM TopBar Component
3
+ * Professional top navigation bar for CRM dashboard
4
+ *
5
+ * Uses useQuickCreateEntities hook to dynamically show quick create options
6
+ * based on user permissions and entity configuration.
7
+ */
8
+
9
+ 'use client'
10
+
11
+ import Link from 'next/link'
12
+ import Image from 'next/image'
13
+ import { useAuth } from '@nextsparkjs/core/hooks/useAuth'
14
+ import { Button } from '@nextsparkjs/core/components/ui/button'
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuItem,
19
+ DropdownMenuLabel,
20
+ DropdownMenuSeparator,
21
+ DropdownMenuTrigger,
22
+ } from '@nextsparkjs/core/components/ui/dropdown-menu'
23
+ import {
24
+ Bell,
25
+ Search,
26
+ Plus,
27
+ User,
28
+ Settings,
29
+ CreditCard,
30
+ LogOut,
31
+ Sun,
32
+ Moon,
33
+ HelpCircle,
34
+ ChevronDown,
35
+ Loader2
36
+ } from 'lucide-react'
37
+ import { useState, useCallback, useMemo } from 'react'
38
+ import { cn } from '@nextsparkjs/core/lib/utils'
39
+ import { useTheme } from 'next-themes'
40
+ import { useQuickCreateEntities } from '@nextsparkjs/core/hooks/useQuickCreateEntities'
41
+
42
+ export function CRMTopBar() {
43
+ const { user, signOut, isLoading } = useAuth()
44
+ const { theme, setTheme } = useTheme()
45
+ const [searchQuery, setSearchQuery] = useState('')
46
+ const [showSearch, setShowSearch] = useState(false)
47
+
48
+ // Get quick create entities filtered by permissions
49
+ const { entities: quickCreateEntities, isLoading: isLoadingEntities, hasEntities } = useQuickCreateEntities()
50
+
51
+ // Transform entities to quick actions format
52
+ const quickActions = useMemo(() => {
53
+ return quickCreateEntities.map(entity => ({
54
+ label: `New ${entity.names?.singular || entity.slug}`,
55
+ href: `/dashboard/${entity.slug}/create`,
56
+ icon: entity.icon
57
+ }))
58
+ }, [quickCreateEntities])
59
+
60
+ // Generate user initials
61
+ const getUserInitials = (user: { firstName?: string; lastName?: string; email: string }) => {
62
+ if (user.firstName && user.lastName) {
63
+ return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
64
+ }
65
+ if (user.firstName) {
66
+ return user.firstName.slice(0, 2).toUpperCase()
67
+ }
68
+ return user.email.slice(0, 2).toUpperCase()
69
+ }
70
+
71
+ const handleSignOut = useCallback(async () => {
72
+ try {
73
+ await signOut()
74
+ } catch (error) {
75
+ console.error('Sign out failed:', error)
76
+ }
77
+ }, [signOut])
78
+
79
+ return (
80
+ <header
81
+ data-cy="crm-topbar"
82
+ className={cn(
83
+ 'hidden lg:block bg-background/80 backdrop-blur-md border-b border-border/50 fixed top-0 right-0 z-40 transition-all duration-300 ease-out'
84
+ )}
85
+ style={{ left: 'var(--crm-sidebar-width, 4rem)' }}
86
+ >
87
+ <div className="h-16 px-6 flex items-center justify-between gap-4">
88
+ {/* Center: Search */}
89
+ <div className="hidden md:flex flex-1 max-w-lg">
90
+ <div className="relative w-full">
91
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
92
+ <input
93
+ type="text"
94
+ placeholder="Search leads, contacts, deals..."
95
+ value={searchQuery}
96
+ onChange={(e) => setSearchQuery(e.target.value)}
97
+ className="w-full pl-10 pr-4 py-2 bg-muted/50 border border-border rounded-lg text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
98
+ data-cy="crm-topbar-search-input"
99
+ />
100
+ {searchQuery && (
101
+ <kbd className="absolute right-3 top-1/2 -translate-y-1/2 hidden sm:inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
102
+ ESC
103
+ </kbd>
104
+ )}
105
+ </div>
106
+ </div>
107
+
108
+ {/* Right side: Actions */}
109
+ <div className="flex items-center gap-2">
110
+ {/* Mobile search toggle */}
111
+ <Button
112
+ variant="ghost"
113
+ size="icon"
114
+ className="md:hidden h-9 w-9"
115
+ onClick={() => setShowSearch(!showSearch)}
116
+ data-cy="crm-topbar-search-mobile-toggle"
117
+ >
118
+ <Search className="h-4 w-4" />
119
+ </Button>
120
+
121
+ {/* Quick Create - Permission-filtered */}
122
+ {isLoadingEntities ? (
123
+ <Button size="sm" className="gap-2 h-9" disabled>
124
+ <Loader2 className="h-4 w-4 animate-spin" />
125
+ <span className="hidden sm:inline">New</span>
126
+ </Button>
127
+ ) : hasEntities ? (
128
+ <DropdownMenu>
129
+ <DropdownMenuTrigger asChild>
130
+ <Button size="sm" className="gap-2 h-9" data-cy="crm-topbar-quick-create-btn">
131
+ <Plus className="h-4 w-4" />
132
+ <span className="hidden sm:inline">New</span>
133
+ <ChevronDown className="h-3 w-3 opacity-50" />
134
+ </Button>
135
+ </DropdownMenuTrigger>
136
+ <DropdownMenuContent align="end" className="w-48" data-cy="crm-topbar-quick-create-dropdown">
137
+ {quickActions.map((action) => (
138
+ <DropdownMenuItem key={action.href} asChild>
139
+ <Link href={action.href} className="flex items-center gap-2">
140
+ {action.icon && <action.icon className="h-4 w-4" />}
141
+ {action.label}
142
+ </Link>
143
+ </DropdownMenuItem>
144
+ ))}
145
+ </DropdownMenuContent>
146
+ </DropdownMenu>
147
+ ) : null}
148
+
149
+ {/* Notifications */}
150
+ <Button variant="ghost" size="icon" className="h-9 w-9 relative" data-cy="crm-topbar-notifications-btn">
151
+ <Bell className="h-4 w-4" />
152
+ <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-destructive rounded-full" />
153
+ </Button>
154
+
155
+ {/* Help */}
156
+ <Button variant="ghost" size="icon" className="h-9 w-9 hidden sm:flex" data-cy="crm-topbar-help-btn">
157
+ <HelpCircle className="h-4 w-4" />
158
+ </Button>
159
+
160
+ {/* Theme Toggle */}
161
+ <Button
162
+ variant="ghost"
163
+ size="icon"
164
+ className="h-9 w-9"
165
+ onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
166
+ data-cy="crm-topbar-theme-toggle"
167
+ >
168
+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
169
+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
170
+ </Button>
171
+
172
+ {/* User Menu */}
173
+ {isLoading ? (
174
+ <div className="h-9 w-9 bg-muted animate-pulse rounded-full" />
175
+ ) : user ? (
176
+ <DropdownMenu>
177
+ <DropdownMenuTrigger asChild>
178
+ <Button variant="ghost" className="h-9 gap-2 pl-2 pr-3" data-cy="crm-topbar-user-menu-trigger">
179
+ {user.image ? (
180
+ <Image
181
+ src={user.image}
182
+ alt=""
183
+ width={28}
184
+ height={28}
185
+ className="h-7 w-7 rounded-full object-cover"
186
+ />
187
+ ) : (
188
+ <div className="h-7 w-7 rounded-full bg-primary flex items-center justify-center text-xs font-medium text-primary-foreground">
189
+ {getUserInitials(user)}
190
+ </div>
191
+ )}
192
+ <ChevronDown className="h-3 w-3 opacity-50 hidden sm:block" />
193
+ </Button>
194
+ </DropdownMenuTrigger>
195
+ <DropdownMenuContent align="end" className="w-56" data-cy="crm-topbar-user-menu-content">
196
+ <DropdownMenuLabel>
197
+ <div className="flex flex-col space-y-1">
198
+ <p className="text-sm font-medium">{user.firstName || 'User'}</p>
199
+ <p className="text-xs text-muted-foreground">{user.email}</p>
200
+ </div>
201
+ </DropdownMenuLabel>
202
+ <DropdownMenuSeparator />
203
+ <DropdownMenuItem asChild data-cy="crm-topbar-menu-profile">
204
+ <Link href="/dashboard/settings/profile" className="flex items-center">
205
+ <User className="mr-2 h-4 w-4" />
206
+ Profile
207
+ </Link>
208
+ </DropdownMenuItem>
209
+ <DropdownMenuItem asChild data-cy="crm-topbar-menu-settings">
210
+ <Link href="/dashboard/settings" className="flex items-center">
211
+ <Settings className="mr-2 h-4 w-4" />
212
+ Settings
213
+ </Link>
214
+ </DropdownMenuItem>
215
+ <DropdownMenuItem asChild data-cy="crm-topbar-menu-billing">
216
+ <Link href="/dashboard/settings/billing" className="flex items-center">
217
+ <CreditCard className="mr-2 h-4 w-4" />
218
+ Billing
219
+ </Link>
220
+ </DropdownMenuItem>
221
+ <DropdownMenuSeparator />
222
+ <DropdownMenuItem
223
+ onClick={handleSignOut}
224
+ className="text-destructive focus:text-destructive"
225
+ data-cy="crm-topbar-menu-signout"
226
+ >
227
+ <LogOut className="mr-2 h-4 w-4" />
228
+ Sign out
229
+ </DropdownMenuItem>
230
+ </DropdownMenuContent>
231
+ </DropdownMenu>
232
+ ) : (
233
+ <div className="flex items-center gap-2">
234
+ <Button variant="ghost" size="sm" asChild>
235
+ <Link href="/login">Sign in</Link>
236
+ </Button>
237
+ <Button size="sm" asChild>
238
+ <Link href="/signup">Sign up</Link>
239
+ </Button>
240
+ </div>
241
+ )}
242
+ </div>
243
+ </div>
244
+
245
+ {/* Mobile search bar */}
246
+ {showSearch && (
247
+ <div className="md:hidden px-4 pb-4 animate-in slide-in-from-top-2">
248
+ <div className="relative">
249
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
250
+ <input
251
+ type="text"
252
+ placeholder="Search..."
253
+ value={searchQuery}
254
+ onChange={(e) => setSearchQuery(e.target.value)}
255
+ className="w-full pl-10 pr-4 py-2 bg-muted/50 border border-border rounded-lg text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
256
+ autoFocus
257
+ />
258
+ </div>
259
+ </div>
260
+ )}
261
+ </header>
262
+ )
263
+ }
264
+
265
+ export default CRMTopBar
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Deal Card Component
3
+ * Professional card for opportunities in the Kanban board
4
+ */
5
+
6
+ 'use client'
7
+
8
+ import React from 'react'
9
+ import { formatCurrency, formatRelativeDate, isDealRotten } from '@/themes/crm/lib/crm-utils'
10
+ import { Building2, Clock, AlertTriangle, TrendingUp } from 'lucide-react'
11
+ import { cn } from '@nextsparkjs/core/lib/utils'
12
+
13
+ export interface Deal {
14
+ id: string
15
+ name: string
16
+ companyId: string
17
+ companyName?: string
18
+ amount: number
19
+ currency?: string
20
+ probability: number
21
+ assignedTo?: string
22
+ assignedToName?: string
23
+ updatedAt: string | Date
24
+ stageId: string
25
+ }
26
+
27
+ interface DealCardProps {
28
+ deal: Deal
29
+ onClick?: () => void
30
+ isDragging?: boolean
31
+ rottenDays?: number
32
+ }
33
+
34
+ // Probability color based on percentage
35
+ const getProbabilityStyle = (probability: number) => {
36
+ if (probability >= 75) return { bg: 'bg-emerald-500/10', text: 'text-emerald-600', fill: 'bg-emerald-500' }
37
+ if (probability >= 50) return { bg: 'bg-amber-500/10', text: 'text-amber-600', fill: 'bg-amber-500' }
38
+ if (probability >= 25) return { bg: 'bg-orange-500/10', text: 'text-orange-600', fill: 'bg-orange-500' }
39
+ return { bg: 'bg-muted', text: 'text-muted-foreground', fill: 'bg-muted-foreground' }
40
+ }
41
+
42
+ export function DealCard({ deal, onClick, isDragging, rottenDays = 30 }: DealCardProps) {
43
+ const isRotten = isDealRotten(deal.updatedAt, rottenDays)
44
+ const probStyle = getProbabilityStyle(deal.probability)
45
+
46
+ return (
47
+ <div
48
+ className={cn(
49
+ 'bg-card border rounded-xl p-4 transition-all duration-200 cursor-grab active:cursor-grabbing',
50
+ 'hover:shadow-md hover:border-primary/30',
51
+ isDragging && 'opacity-50 shadow-lg scale-105 rotate-2',
52
+ isRotten && 'border-l-4 border-l-destructive'
53
+ )}
54
+ onClick={onClick}
55
+ role="button"
56
+ tabIndex={0}
57
+ data-cy={`deal-card-${deal.id}`}
58
+ >
59
+ {/* Header */}
60
+ <div className="mb-3">
61
+ <h4 className="font-semibold text-sm text-foreground leading-tight mb-1 line-clamp-2" data-cy={`deal-card-name-${deal.id}`}>
62
+ {deal.name}
63
+ </h4>
64
+
65
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
66
+ <Building2 className="w-3 h-3" />
67
+ <span className="truncate" data-cy={`deal-card-company-${deal.id}`}>{deal.companyName || 'No company'}</span>
68
+ </div>
69
+ </div>
70
+
71
+ {/* Amount and Probability */}
72
+ <div className="flex items-end justify-between mb-3">
73
+ <div className="font-bold text-lg text-primary" data-cy={`deal-card-amount-${deal.id}`}>
74
+ {formatCurrency(deal.amount, deal.currency || 'USD')}
75
+ </div>
76
+
77
+ <div className={cn('flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium', probStyle.bg, probStyle.text)}>
78
+ <TrendingUp className="w-3 h-3" />
79
+ {deal.probability}%
80
+ </div>
81
+ </div>
82
+
83
+ {/* Probability bar */}
84
+ <div className="h-1.5 bg-muted rounded-full overflow-hidden mb-3">
85
+ <div
86
+ className={cn('h-full rounded-full transition-all duration-300', probStyle.fill)}
87
+ style={{ width: `${deal.probability}%` }}
88
+ />
89
+ </div>
90
+
91
+ {/* Footer */}
92
+ <div className="flex items-center justify-between">
93
+ {deal.assignedToName ? (
94
+ <div className="flex items-center gap-1.5">
95
+ <div className="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center text-[10px] font-semibold text-primary">
96
+ {deal.assignedToName.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()}
97
+ </div>
98
+ <span className="text-xs text-muted-foreground truncate max-w-[80px]">
99
+ {deal.assignedToName.split(' ')[0]}
100
+ </span>
101
+ </div>
102
+ ) : (
103
+ <span className="text-xs text-muted-foreground">Unassigned</span>
104
+ )}
105
+
106
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
107
+ <Clock className="w-3 h-3" />
108
+ <span>{formatRelativeDate(deal.updatedAt)}</span>
109
+ </div>
110
+ </div>
111
+
112
+ {/* Rotten warning */}
113
+ {isRotten && (
114
+ <div className="mt-3 pt-3 border-t border-destructive/20 flex items-center gap-1.5 text-xs text-destructive font-medium">
115
+ <AlertTriangle className="w-3.5 h-3.5" />
116
+ <span>Needs attention - no activity for {rottenDays}+ days</span>
117
+ </div>
118
+ )}
119
+ </div>
120
+ )
121
+ }
122
+
123
+ export default DealCard
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Entity Card Component
3
+ * Reusable card for displaying entity summaries
4
+ */
5
+
6
+ import React from 'react'
7
+ import '@/contents/themes/crm/styles/crm-theme.css'
8
+
9
+ interface EntityCardProps {
10
+ title: string
11
+ subtitle?: string
12
+ meta?: string
13
+ status?: {
14
+ label: string
15
+ variant: 'success' | 'warning' | 'danger' | 'info' | 'neutral'
16
+ }
17
+ onClick?: () => void
18
+ children?: React.ReactNode
19
+ className?: string
20
+ }
21
+
22
+ export function EntityCard({
23
+ title,
24
+ subtitle,
25
+ meta,
26
+ status,
27
+ onClick,
28
+ children,
29
+ className = '',
30
+ }: EntityCardProps) {
31
+ return (
32
+ <div
33
+ className={`crm-entity-card ${className}`}
34
+ onClick={onClick}
35
+ role={onClick ? 'button' : undefined}
36
+ tabIndex={onClick ? 0 : undefined}
37
+ >
38
+ <div className="crm-entity-card-header">
39
+ <div>
40
+ <div className="crm-entity-card-title">{title}</div>
41
+ {subtitle && <div className="crm-entity-card-meta">{subtitle}</div>}
42
+ </div>
43
+
44
+ {status && (
45
+ <span className={`crm-badge ${status.variant}`}>
46
+ {status.label}
47
+ </span>
48
+ )}
49
+ </div>
50
+
51
+ {meta && <div className="crm-entity-card-meta">{meta}</div>}
52
+
53
+ {children && <div className="mt-3">{children}</div>}
54
+ </div>
55
+ )
56
+ }
57
+
58
+ export default EntityCard