@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.
- package/CRM_PLAN.md +343 -0
- package/about.md +122 -0
- package/config/app.config.ts +185 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +372 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +336 -0
- package/config/flows.config.ts +511 -0
- package/config/permissions.config.ts +297 -0
- package/config/theme.config.ts +111 -0
- package/entities/activities/activities.config.ts +61 -0
- package/entities/activities/activities.fields.ts +362 -0
- package/entities/activities/activities.service.ts +503 -0
- package/entities/activities/activities.types.ts +117 -0
- package/entities/activities/messages/en.json +123 -0
- package/entities/activities/messages/es.json +123 -0
- package/entities/activities/migrations/020_activities_table.sql +123 -0
- package/entities/activities/migrations/021_activities_metas.sql +114 -0
- package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
- package/entities/campaigns/campaigns.config.ts +61 -0
- package/entities/campaigns/campaigns.fields.ts +413 -0
- package/entities/campaigns/campaigns.service.ts +426 -0
- package/entities/campaigns/campaigns.types.ts +124 -0
- package/entities/campaigns/messages/en.json +145 -0
- package/entities/campaigns/messages/es.json +145 -0
- package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
- package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
- package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
- package/entities/companies/companies.config.ts +61 -0
- package/entities/companies/companies.fields.ts +429 -0
- package/entities/companies/companies.service.ts +566 -0
- package/entities/companies/companies.types.ts +125 -0
- package/entities/companies/messages/en.json +146 -0
- package/entities/companies/messages/es.json +146 -0
- package/entities/companies/migrations/001_companies_table.sql +150 -0
- package/entities/companies/migrations/002_companies_metas.sql +114 -0
- package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
- package/entities/contacts/contacts.config.ts +61 -0
- package/entities/contacts/contacts.fields.ts +359 -0
- package/entities/contacts/contacts.service.ts +509 -0
- package/entities/contacts/contacts.types.ts +108 -0
- package/entities/contacts/messages/en.json +117 -0
- package/entities/contacts/messages/es.json +117 -0
- package/entities/contacts/migrations/001_contacts_table.sql +134 -0
- package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
- package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
- package/entities/leads/leads.config.ts +61 -0
- package/entities/leads/leads.fields.ts +336 -0
- package/entities/leads/leads.service.ts +496 -0
- package/entities/leads/leads.types.ts +114 -0
- package/entities/leads/messages/en.json +132 -0
- package/entities/leads/messages/es.json +132 -0
- package/entities/leads/migrations/001_leads_table.sql +150 -0
- package/entities/leads/migrations/002_leads_metas.sql +120 -0
- package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
- package/entities/notes/messages/en.json +114 -0
- package/entities/notes/messages/es.json +114 -0
- package/entities/notes/migrations/020_notes_table.sql +118 -0
- package/entities/notes/migrations/021_notes_metas.sql +114 -0
- package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
- package/entities/notes/notes.config.ts +61 -0
- package/entities/notes/notes.fields.ts +283 -0
- package/entities/notes/notes.service.ts +320 -0
- package/entities/notes/notes.types.ts +102 -0
- package/entities/opportunities/messages/en.json +107 -0
- package/entities/opportunities/messages/es.json +107 -0
- package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
- package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
- package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
- package/entities/opportunities/opportunities.config.ts +61 -0
- package/entities/opportunities/opportunities.fields.ts +416 -0
- package/entities/opportunities/opportunities.service.ts +525 -0
- package/entities/opportunities/opportunities.types.ts +135 -0
- package/entities/pipelines/messages/en.json +115 -0
- package/entities/pipelines/messages/es.json +115 -0
- package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
- package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
- package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
- package/entities/pipelines/pipelines.config.ts +62 -0
- package/entities/pipelines/pipelines.fields.ts +193 -0
- package/entities/pipelines/pipelines.service.ts +383 -0
- package/entities/pipelines/pipelines.types.ts +78 -0
- package/entities/products/messages/en.json +135 -0
- package/entities/products/messages/es.json +135 -0
- package/entities/products/migrations/001_products_table.sql +117 -0
- package/entities/products/migrations/002_products_metas.sql +114 -0
- package/entities/products/migrations/003_products_sample_data.sql +247 -0
- package/entities/products/products.config.ts +62 -0
- package/entities/products/products.fields.ts +361 -0
- package/entities/products/products.service.ts +437 -0
- package/entities/products/products.types.ts +125 -0
- package/lib/crm-constants.ts +77 -0
- package/lib/crm-utils.ts +185 -0
- package/lib/selectors.ts +333 -0
- package/messages/en.json +131 -0
- package/messages/es.json +131 -0
- package/migrations/999_theme_sample_data.sql +473 -0
- package/package.json +18 -0
- package/pendings.md +205 -0
- package/permissions-matrix.md +216 -0
- package/styles/components.css +414 -0
- package/styles/crm-theme.css +358 -0
- package/styles/globals.css +576 -0
- package/styles/variables.css +111 -0
- package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
- package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
- package/templates/dashboard/(main)/activities/page.tsx +297 -0
- package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
- package/templates/dashboard/(main)/companies/page.tsx +296 -0
- package/templates/dashboard/(main)/contacts/page.tsx +347 -0
- package/templates/dashboard/(main)/layout.tsx +98 -0
- package/templates/dashboard/(main)/leads/page.tsx +335 -0
- package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
- package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
- package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
- package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
- package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
- package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
- package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
- package/templates/dashboard/(main)/products/create/page.tsx +96 -0
- package/templates/dashboard/(main)/products/page.tsx +308 -0
- package/templates/shared/ActionButtons.tsx +41 -0
- package/templates/shared/CRMDashboard.tsx +519 -0
- package/templates/shared/CRMDataTable.tsx +441 -0
- package/templates/shared/CRMMetricCard.tsx +76 -0
- package/templates/shared/CRMMobileNav.tsx +172 -0
- package/templates/shared/CRMSidebar.tsx +346 -0
- package/templates/shared/CRMTopBar.tsx +265 -0
- package/templates/shared/DealCard.tsx +123 -0
- package/templates/shared/EntityCard.tsx +58 -0
- package/templates/shared/OpportunityForm.tsx +649 -0
- package/templates/shared/PipelineForm.tsx +367 -0
- package/templates/shared/PipelineKanban.tsx +194 -0
- package/templates/shared/QuickFilters.tsx +47 -0
- package/templates/shared/StageColumn.tsx +175 -0
- package/templates/shared/StageSelect.tsx +177 -0
- package/templates/shared/StagesRepeater.tsx +317 -0
- package/templates/shared/index.ts +9 -0
package/lib/crm-utils.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRM Theme Utilities
|
|
3
|
+
* Helper functions for the CRM theme
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format currency with proper symbol and decimals
|
|
8
|
+
*/
|
|
9
|
+
export function formatCurrency(
|
|
10
|
+
amount: number,
|
|
11
|
+
currency: string = 'USD',
|
|
12
|
+
locale: string = 'en-US'
|
|
13
|
+
): string {
|
|
14
|
+
return new Intl.NumberFormat(locale, {
|
|
15
|
+
style: 'currency',
|
|
16
|
+
currency,
|
|
17
|
+
minimumFractionDigits: 0,
|
|
18
|
+
maximumFractionDigits: 2,
|
|
19
|
+
}).format(amount)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format large numbers with K, M, B suffixes
|
|
24
|
+
*/
|
|
25
|
+
export function formatCompactNumber(num: number): string {
|
|
26
|
+
if (num >= 1_000_000_000) {
|
|
27
|
+
return `${(num / 1_000_000_000).toFixed(1)}B`
|
|
28
|
+
}
|
|
29
|
+
if (num >= 1_000_000) {
|
|
30
|
+
return `${(num / 1_000_000).toFixed(1)}M`
|
|
31
|
+
}
|
|
32
|
+
if (num >= 1_000) {
|
|
33
|
+
return `${(num / 1_000).toFixed(1)}K`
|
|
34
|
+
}
|
|
35
|
+
return num.toString()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Calculate percentage change between two values
|
|
40
|
+
*/
|
|
41
|
+
export function calculatePercentageChange(
|
|
42
|
+
current: number,
|
|
43
|
+
previous: number
|
|
44
|
+
): { value: number; isPositive: boolean } {
|
|
45
|
+
if (previous === 0) {
|
|
46
|
+
return { value: 100, isPositive: current > 0 }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const change = ((current - previous) / previous) * 100
|
|
50
|
+
return {
|
|
51
|
+
value: Math.abs(change),
|
|
52
|
+
isPositive: change >= 0,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get initials from a name
|
|
58
|
+
*/
|
|
59
|
+
export function getInitials(name: string): string {
|
|
60
|
+
return name
|
|
61
|
+
.split(' ')
|
|
62
|
+
.map(part => part[0])
|
|
63
|
+
.join('')
|
|
64
|
+
.toUpperCase()
|
|
65
|
+
.slice(0, 2)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calculate days between two dates
|
|
70
|
+
*/
|
|
71
|
+
export function daysBetween(date1: Date, date2: Date): number {
|
|
72
|
+
const diffTime = Math.abs(date2.getTime() - date1.getTime())
|
|
73
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a deal is rotten (stale) based on last update
|
|
78
|
+
*/
|
|
79
|
+
export function isDealRotten(
|
|
80
|
+
updatedAt: Date | string,
|
|
81
|
+
rottenDays: number = 30
|
|
82
|
+
): boolean {
|
|
83
|
+
const lastUpdate = typeof updatedAt === 'string' ? new Date(updatedAt) : updatedAt
|
|
84
|
+
const daysSinceUpdate = daysBetween(new Date(), lastUpdate)
|
|
85
|
+
return daysSinceUpdate > rottenDays
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format date relative to now (e.g., "2 hours ago", "yesterday")
|
|
90
|
+
*/
|
|
91
|
+
export function formatRelativeDate(date: Date | string | null | undefined): string {
|
|
92
|
+
if (!date) {
|
|
93
|
+
return '-'
|
|
94
|
+
}
|
|
95
|
+
const d = typeof date === 'string' ? new Date(date) : date
|
|
96
|
+
const now = new Date()
|
|
97
|
+
const diffInSeconds = Math.floor((now.getTime() - d.getTime()) / 1000)
|
|
98
|
+
|
|
99
|
+
if (diffInSeconds < 60) {
|
|
100
|
+
return 'just now'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const diffInMinutes = Math.floor(diffInSeconds / 60)
|
|
104
|
+
if (diffInMinutes < 60) {
|
|
105
|
+
return `${diffInMinutes}m ago`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const diffInHours = Math.floor(diffInMinutes / 60)
|
|
109
|
+
if (diffInHours < 24) {
|
|
110
|
+
return `${diffInHours}h ago`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const diffInDays = Math.floor(diffInHours / 24)
|
|
114
|
+
if (diffInDays === 1) {
|
|
115
|
+
return 'yesterday'
|
|
116
|
+
}
|
|
117
|
+
if (diffInDays < 7) {
|
|
118
|
+
return `${diffInDays}d ago`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return d.toLocaleDateString()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get badge variant based on status
|
|
126
|
+
*/
|
|
127
|
+
export function getStatusVariant(
|
|
128
|
+
status: string
|
|
129
|
+
): 'success' | 'warning' | 'danger' | 'info' | 'neutral' {
|
|
130
|
+
const lowerStatus = status.toLowerCase()
|
|
131
|
+
|
|
132
|
+
if (['won', 'completed', 'active', 'converted', 'success'].includes(lowerStatus)) {
|
|
133
|
+
return 'success'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (['pending', 'in_progress', 'scheduled', 'proposal', 'negotiation'].includes(lowerStatus)) {
|
|
137
|
+
return 'warning'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (['lost', 'cancelled', 'overdue', 'failed'].includes(lowerStatus)) {
|
|
141
|
+
return 'danger'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (['new', 'open', 'qualification'].includes(lowerStatus)) {
|
|
145
|
+
return 'info'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return 'neutral'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Calculate expected revenue from amount and probability
|
|
153
|
+
*/
|
|
154
|
+
export function calculateExpectedRevenue(amount: number, probability: number): number {
|
|
155
|
+
return (amount * probability) / 100
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Truncate text with ellipsis
|
|
160
|
+
*/
|
|
161
|
+
export function truncate(text: string, maxLength: number): string {
|
|
162
|
+
if (text.length <= maxLength) {
|
|
163
|
+
return text
|
|
164
|
+
}
|
|
165
|
+
return text.slice(0, maxLength) + '...'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format phone number to international format
|
|
170
|
+
*/
|
|
171
|
+
export function formatPhoneNumber(phone: string): string {
|
|
172
|
+
// Remove all non-numeric characters
|
|
173
|
+
const cleaned = phone.replace(/\D/g, '')
|
|
174
|
+
|
|
175
|
+
// Format based on length
|
|
176
|
+
if (cleaned.length === 10) {
|
|
177
|
+
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (cleaned.length === 11 && cleaned[0] === '1') {
|
|
181
|
+
return `+1 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return phone // Return original if format is unknown
|
|
185
|
+
}
|
package/lib/selectors.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRM Theme - Block Selectors
|
|
3
|
+
*
|
|
4
|
+
* This file defines selectors for block components in the theme.
|
|
5
|
+
* It's placed in lib/ instead of tests/ so TypeScript can resolve imports.
|
|
6
|
+
*
|
|
7
|
+
* Used by:
|
|
8
|
+
* - Block components (for data-cy attributes)
|
|
9
|
+
* - Cypress tests (via tests/cypress/src/selectors.ts)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createSelectorHelpers } from '@nextsparkjs/core/lib/test/selector-factory'
|
|
13
|
+
import { CORE_SELECTORS } from '@nextsparkjs/core/lib/test/core-selectors'
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// BLOCK SELECTORS
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Block-specific selectors for the CRM theme.
|
|
21
|
+
* Each block has at minimum a 'container' selector.
|
|
22
|
+
* Dynamic selectors use {index} placeholder.
|
|
23
|
+
*/
|
|
24
|
+
export const BLOCK_SELECTORS = {
|
|
25
|
+
// CRM theme currently has no custom blocks
|
|
26
|
+
// Add block selectors here when blocks are created
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// ENTITY SELECTORS
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Entity-specific selectors for the CRM theme.
|
|
35
|
+
*/
|
|
36
|
+
export const ENTITY_SELECTORS = {
|
|
37
|
+
leads: {
|
|
38
|
+
list: 'leads-list',
|
|
39
|
+
listItem: 'lead-item-{index}',
|
|
40
|
+
card: 'lead-card-{id}',
|
|
41
|
+
name: 'lead-name',
|
|
42
|
+
email: 'lead-email',
|
|
43
|
+
phone: 'lead-phone',
|
|
44
|
+
company: 'lead-company',
|
|
45
|
+
status: 'lead-status',
|
|
46
|
+
source: 'lead-source',
|
|
47
|
+
createButton: 'lead-create-button',
|
|
48
|
+
editButton: 'lead-edit-button',
|
|
49
|
+
deleteButton: 'lead-delete-button',
|
|
50
|
+
convertButton: 'lead-convert-button',
|
|
51
|
+
form: {
|
|
52
|
+
container: 'lead-form',
|
|
53
|
+
name: 'lead-form-name',
|
|
54
|
+
email: 'lead-form-email',
|
|
55
|
+
phone: 'lead-form-phone',
|
|
56
|
+
company: 'lead-form-company',
|
|
57
|
+
status: 'lead-form-status',
|
|
58
|
+
source: 'lead-form-source',
|
|
59
|
+
submit: 'lead-form-submit',
|
|
60
|
+
cancel: 'lead-form-cancel',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
contacts: {
|
|
64
|
+
list: 'contacts-list',
|
|
65
|
+
listItem: 'contact-item-{index}',
|
|
66
|
+
card: 'contact-card-{id}',
|
|
67
|
+
name: 'contact-name',
|
|
68
|
+
email: 'contact-email',
|
|
69
|
+
phone: 'contact-phone',
|
|
70
|
+
company: 'contact-company',
|
|
71
|
+
createButton: 'contact-create-button',
|
|
72
|
+
editButton: 'contact-edit-button',
|
|
73
|
+
deleteButton: 'contact-delete-button',
|
|
74
|
+
form: {
|
|
75
|
+
container: 'contact-form',
|
|
76
|
+
firstName: 'contact-form-first-name',
|
|
77
|
+
lastName: 'contact-form-last-name',
|
|
78
|
+
email: 'contact-form-email',
|
|
79
|
+
phone: 'contact-form-phone',
|
|
80
|
+
company: 'contact-form-company',
|
|
81
|
+
submit: 'contact-form-submit',
|
|
82
|
+
cancel: 'contact-form-cancel',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
companies: {
|
|
86
|
+
list: 'companies-list',
|
|
87
|
+
listItem: 'company-item-{index}',
|
|
88
|
+
card: 'company-card-{id}',
|
|
89
|
+
name: 'company-name',
|
|
90
|
+
website: 'company-website',
|
|
91
|
+
industry: 'company-industry',
|
|
92
|
+
size: 'company-size',
|
|
93
|
+
createButton: 'company-create-button',
|
|
94
|
+
editButton: 'company-edit-button',
|
|
95
|
+
deleteButton: 'company-delete-button',
|
|
96
|
+
form: {
|
|
97
|
+
container: 'company-form',
|
|
98
|
+
name: 'company-form-name',
|
|
99
|
+
website: 'company-form-website',
|
|
100
|
+
industry: 'company-form-industry',
|
|
101
|
+
size: 'company-form-size',
|
|
102
|
+
submit: 'company-form-submit',
|
|
103
|
+
cancel: 'company-form-cancel',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
opportunities: {
|
|
107
|
+
list: 'opportunities-list',
|
|
108
|
+
listItem: 'opportunity-item-{index}',
|
|
109
|
+
card: 'opportunity-card-{id}',
|
|
110
|
+
name: 'opportunity-name',
|
|
111
|
+
value: 'opportunity-value',
|
|
112
|
+
stage: 'opportunity-stage',
|
|
113
|
+
probability: 'opportunity-probability',
|
|
114
|
+
closeDate: 'opportunity-close-date',
|
|
115
|
+
createButton: 'opportunity-create-button',
|
|
116
|
+
editButton: 'opportunity-edit-button',
|
|
117
|
+
deleteButton: 'opportunity-delete-button',
|
|
118
|
+
form: {
|
|
119
|
+
container: 'opportunity-form',
|
|
120
|
+
name: 'opportunity-form-name',
|
|
121
|
+
value: 'opportunity-form-value',
|
|
122
|
+
stage: 'opportunity-form-stage',
|
|
123
|
+
probability: 'opportunity-form-probability',
|
|
124
|
+
closeDate: 'opportunity-form-close-date',
|
|
125
|
+
submit: 'opportunity-form-submit',
|
|
126
|
+
cancel: 'opportunity-form-cancel',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
activities: {
|
|
130
|
+
list: 'activities-list',
|
|
131
|
+
listItem: 'activity-item-{index}',
|
|
132
|
+
card: 'activity-card-{id}',
|
|
133
|
+
type: 'activity-type',
|
|
134
|
+
subject: 'activity-subject',
|
|
135
|
+
dueDate: 'activity-due-date',
|
|
136
|
+
status: 'activity-status',
|
|
137
|
+
createButton: 'activity-create-button',
|
|
138
|
+
editButton: 'activity-edit-button',
|
|
139
|
+
deleteButton: 'activity-delete-button',
|
|
140
|
+
completeButton: 'activity-complete-button',
|
|
141
|
+
form: {
|
|
142
|
+
container: 'activity-form',
|
|
143
|
+
type: 'activity-form-type',
|
|
144
|
+
subject: 'activity-form-subject',
|
|
145
|
+
description: 'activity-form-description',
|
|
146
|
+
dueDate: 'activity-form-due-date',
|
|
147
|
+
submit: 'activity-form-submit',
|
|
148
|
+
cancel: 'activity-form-cancel',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
notes: {
|
|
152
|
+
list: 'notes-list',
|
|
153
|
+
listItem: 'note-item-{index}',
|
|
154
|
+
card: 'note-card-{id}',
|
|
155
|
+
title: 'note-title',
|
|
156
|
+
content: 'note-content',
|
|
157
|
+
createButton: 'note-create-button',
|
|
158
|
+
editButton: 'note-edit-button',
|
|
159
|
+
deleteButton: 'note-delete-button',
|
|
160
|
+
form: {
|
|
161
|
+
container: 'note-form',
|
|
162
|
+
title: 'note-form-title',
|
|
163
|
+
content: 'note-form-content',
|
|
164
|
+
submit: 'note-form-submit',
|
|
165
|
+
cancel: 'note-form-cancel',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
campaigns: {
|
|
169
|
+
list: 'campaigns-list',
|
|
170
|
+
listItem: 'campaign-item-{index}',
|
|
171
|
+
card: 'campaign-card-{id}',
|
|
172
|
+
name: 'campaign-name',
|
|
173
|
+
type: 'campaign-type',
|
|
174
|
+
status: 'campaign-status',
|
|
175
|
+
budget: 'campaign-budget',
|
|
176
|
+
createButton: 'campaign-create-button',
|
|
177
|
+
editButton: 'campaign-edit-button',
|
|
178
|
+
deleteButton: 'campaign-delete-button',
|
|
179
|
+
form: {
|
|
180
|
+
container: 'campaign-form',
|
|
181
|
+
name: 'campaign-form-name',
|
|
182
|
+
type: 'campaign-form-type',
|
|
183
|
+
status: 'campaign-form-status',
|
|
184
|
+
budget: 'campaign-form-budget',
|
|
185
|
+
startDate: 'campaign-form-start-date',
|
|
186
|
+
endDate: 'campaign-form-end-date',
|
|
187
|
+
submit: 'campaign-form-submit',
|
|
188
|
+
cancel: 'campaign-form-cancel',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
products: {
|
|
192
|
+
list: 'products-list',
|
|
193
|
+
listItem: 'product-item-{index}',
|
|
194
|
+
card: 'product-card-{id}',
|
|
195
|
+
name: 'product-name',
|
|
196
|
+
sku: 'product-sku',
|
|
197
|
+
price: 'product-price',
|
|
198
|
+
category: 'product-category',
|
|
199
|
+
createButton: 'product-create-button',
|
|
200
|
+
editButton: 'product-edit-button',
|
|
201
|
+
deleteButton: 'product-delete-button',
|
|
202
|
+
form: {
|
|
203
|
+
container: 'product-form',
|
|
204
|
+
name: 'product-form-name',
|
|
205
|
+
sku: 'product-form-sku',
|
|
206
|
+
price: 'product-form-price',
|
|
207
|
+
description: 'product-form-description',
|
|
208
|
+
category: 'product-form-category',
|
|
209
|
+
submit: 'product-form-submit',
|
|
210
|
+
cancel: 'product-form-cancel',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
pipelines: {
|
|
214
|
+
list: 'pipelines-list',
|
|
215
|
+
listItem: 'pipeline-item-{index}',
|
|
216
|
+
card: 'pipeline-card-{id}',
|
|
217
|
+
name: 'pipeline-name',
|
|
218
|
+
stages: 'pipeline-stages',
|
|
219
|
+
createButton: 'pipeline-create-button',
|
|
220
|
+
editButton: 'pipeline-edit-button',
|
|
221
|
+
deleteButton: 'pipeline-delete-button',
|
|
222
|
+
form: {
|
|
223
|
+
container: 'pipeline-form',
|
|
224
|
+
name: 'pipeline-form-name',
|
|
225
|
+
description: 'pipeline-form-description',
|
|
226
|
+
stages: 'pipeline-form-stages',
|
|
227
|
+
submit: 'pipeline-form-submit',
|
|
228
|
+
cancel: 'pipeline-form-cancel',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
} as const
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// CRM-SPECIFIC SELECTORS
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* CRM-specific UI selectors.
|
|
239
|
+
*/
|
|
240
|
+
export const CRM_SELECTORS = {
|
|
241
|
+
dashboard: {
|
|
242
|
+
container: 'crm-dashboard',
|
|
243
|
+
statsCards: 'dashboard-stats-cards',
|
|
244
|
+
recentActivities: 'dashboard-recent-activities',
|
|
245
|
+
pipeline: 'dashboard-pipeline',
|
|
246
|
+
charts: 'dashboard-charts',
|
|
247
|
+
},
|
|
248
|
+
pipelineView: {
|
|
249
|
+
container: 'pipeline-view',
|
|
250
|
+
stage: 'pipeline-stage-{index}',
|
|
251
|
+
stageHeader: 'pipeline-stage-header-{index}',
|
|
252
|
+
opportunityCard: 'pipeline-opportunity-{id}',
|
|
253
|
+
dropZone: 'pipeline-drop-zone-{index}',
|
|
254
|
+
},
|
|
255
|
+
reporting: {
|
|
256
|
+
container: 'crm-reporting',
|
|
257
|
+
dateRange: 'report-date-range',
|
|
258
|
+
filters: 'report-filters',
|
|
259
|
+
exportButton: 'report-export-button',
|
|
260
|
+
chart: 'report-chart',
|
|
261
|
+
},
|
|
262
|
+
} as const
|
|
263
|
+
|
|
264
|
+
// =============================================================================
|
|
265
|
+
// THEME SELECTORS (CORE + BLOCKS + ENTITIES + CRM)
|
|
266
|
+
// =============================================================================
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Complete theme selectors merging core, blocks, and entities.
|
|
270
|
+
*/
|
|
271
|
+
export const THEME_SELECTORS = {
|
|
272
|
+
...CORE_SELECTORS,
|
|
273
|
+
blocks: BLOCK_SELECTORS,
|
|
274
|
+
entities: ENTITY_SELECTORS,
|
|
275
|
+
crm: CRM_SELECTORS,
|
|
276
|
+
} as const
|
|
277
|
+
|
|
278
|
+
// =============================================================================
|
|
279
|
+
// EXPORTS
|
|
280
|
+
// =============================================================================
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create helpers bound to theme selectors
|
|
284
|
+
*/
|
|
285
|
+
const helpers = createSelectorHelpers(THEME_SELECTORS)
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Full selectors object (core + theme extensions)
|
|
289
|
+
*/
|
|
290
|
+
export const SELECTORS = helpers.SELECTORS
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get a selector value by path
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* sel('auth.login.form') // 'login-form'
|
|
297
|
+
* sel('entities.leads.list') // 'leads-list'
|
|
298
|
+
* sel('entities.leads.listItem', { index: '0' }) // 'lead-item-0'
|
|
299
|
+
*/
|
|
300
|
+
export const sel = helpers.sel
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Alias for sel
|
|
304
|
+
*/
|
|
305
|
+
export const s = helpers.s
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get selector only in dev/test environments
|
|
309
|
+
*/
|
|
310
|
+
export const selDev = helpers.selDev
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get Cypress selector string [data-cy="..."]
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* cySelector('entities.leads.list') // '[data-cy="leads-list"]'
|
|
317
|
+
*/
|
|
318
|
+
export const cySelector = helpers.cySelector
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create entity-specific selector helpers
|
|
322
|
+
*/
|
|
323
|
+
export const entitySelectors = helpers.entitySelectors
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Type exports
|
|
327
|
+
*/
|
|
328
|
+
export type ThemeSelectorsType = typeof THEME_SELECTORS
|
|
329
|
+
export type BlockSelectorsType = typeof BLOCK_SELECTORS
|
|
330
|
+
export type EntitySelectorsType = typeof ENTITY_SELECTORS
|
|
331
|
+
export type CRMSelectorsType = typeof CRM_SELECTORS
|
|
332
|
+
export type { Replacements } from '@nextsparkjs/core/lib/test/selector-factory'
|
|
333
|
+
export { CORE_SELECTORS }
|
package/messages/en.json
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"crm": {
|
|
3
|
+
"name": "CRM Enterprise",
|
|
4
|
+
"description": "Complete CRM solution for sales and marketing teams",
|
|
5
|
+
"tagline": "Close more deals, grow your business",
|
|
6
|
+
|
|
7
|
+
"navigation": {
|
|
8
|
+
"leads": "Leads",
|
|
9
|
+
"contacts": "Contacts",
|
|
10
|
+
"companies": "Companies",
|
|
11
|
+
"opportunities": "Opportunities",
|
|
12
|
+
"activities": "Activities",
|
|
13
|
+
"campaigns": "Campaigns",
|
|
14
|
+
"reports": "Reports",
|
|
15
|
+
"settings": "Settings"
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
"quickActions": {
|
|
19
|
+
"newLead": "New Lead",
|
|
20
|
+
"newContact": "New Contact",
|
|
21
|
+
"newCompany": "New Company",
|
|
22
|
+
"newOpportunity": "New Opportunity",
|
|
23
|
+
"scheduleActivity": "Schedule Activity",
|
|
24
|
+
"createCampaign": "Create Campaign"
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
"dashboard": {
|
|
28
|
+
"welcome": "Welcome back!",
|
|
29
|
+
"welcomeWithName": "Welcome back, {name}!",
|
|
30
|
+
"overview": "Sales Overview",
|
|
31
|
+
"pipelineValue": "Pipeline Value",
|
|
32
|
+
"openDeals": "Open Deals",
|
|
33
|
+
"wonThisMonth": "Won This Month",
|
|
34
|
+
"conversionRate": "Conversion Rate",
|
|
35
|
+
"upcomingActivities": "Upcoming Activities",
|
|
36
|
+
"recentLeads": "Recent Leads",
|
|
37
|
+
"topOpportunities": "Top Opportunities"
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
"pipeline": {
|
|
41
|
+
"title": "Sales Pipeline",
|
|
42
|
+
"totalValue": "Total Value",
|
|
43
|
+
"dealCount": "Deals",
|
|
44
|
+
"stages": {
|
|
45
|
+
"qualification": "Qualification",
|
|
46
|
+
"needsAnalysis": "Needs Analysis",
|
|
47
|
+
"proposal": "Proposal",
|
|
48
|
+
"negotiation": "Negotiation",
|
|
49
|
+
"closedWon": "Closed Won",
|
|
50
|
+
"closedLost": "Closed Lost"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
"reports": {
|
|
55
|
+
"title": "Reports",
|
|
56
|
+
"sales": "Sales Report",
|
|
57
|
+
"marketing": "Marketing Report",
|
|
58
|
+
"pipeline": "Pipeline Report",
|
|
59
|
+
"activity": "Activity Report",
|
|
60
|
+
"forecast": "Sales Forecast"
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"features": {
|
|
64
|
+
"convert": {
|
|
65
|
+
"title": "Convert Lead",
|
|
66
|
+
"description": "Convert this lead to a contact and company",
|
|
67
|
+
"createContact": "Create Contact",
|
|
68
|
+
"createCompany": "Create Company",
|
|
69
|
+
"success": "Lead converted successfully",
|
|
70
|
+
"alreadyConverted": "This lead has already been converted"
|
|
71
|
+
},
|
|
72
|
+
"bulkImport": {
|
|
73
|
+
"title": "Import Data",
|
|
74
|
+
"description": "Import data from CSV or Excel file",
|
|
75
|
+
"selectFile": "Select file",
|
|
76
|
+
"mapping": "Map columns",
|
|
77
|
+
"preview": "Preview",
|
|
78
|
+
"import": "Import"
|
|
79
|
+
},
|
|
80
|
+
"bulkExport": {
|
|
81
|
+
"title": "Export Data",
|
|
82
|
+
"description": "Export data to CSV or Excel file",
|
|
83
|
+
"allRecords": "All records",
|
|
84
|
+
"selectedRecords": "Selected records",
|
|
85
|
+
"filteredRecords": "Filtered records"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
"roles": {
|
|
90
|
+
"owner": "Administrator",
|
|
91
|
+
"admin": "Manager",
|
|
92
|
+
"member": "Representative",
|
|
93
|
+
"viewer": "Viewer",
|
|
94
|
+
"descriptions": {
|
|
95
|
+
"owner": "Full access to all CRM features and settings",
|
|
96
|
+
"admin": "Can manage team, view reports, and perform bulk operations",
|
|
97
|
+
"member": "Can manage leads, contacts, opportunities and activities",
|
|
98
|
+
"viewer": "Read-only access to CRM data"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
"stats": {
|
|
103
|
+
"totalLeads": "Total Leads",
|
|
104
|
+
"qualifiedLeads": "Qualified Leads",
|
|
105
|
+
"totalOpportunities": "Total Opportunities",
|
|
106
|
+
"wonOpportunities": "Won Opportunities",
|
|
107
|
+
"pipelineValue": "Pipeline Value",
|
|
108
|
+
"avgDealSize": "Avg Deal Size",
|
|
109
|
+
"winRate": "Win Rate",
|
|
110
|
+
"activeCampaigns": "Active Campaigns"
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
"empty": {
|
|
114
|
+
"noLeads": {
|
|
115
|
+
"title": "No leads yet",
|
|
116
|
+
"description": "Start capturing leads to grow your pipeline.",
|
|
117
|
+
"action": "Add your first lead"
|
|
118
|
+
},
|
|
119
|
+
"noOpportunities": {
|
|
120
|
+
"title": "No opportunities yet",
|
|
121
|
+
"description": "Create opportunities from qualified leads.",
|
|
122
|
+
"action": "Create opportunity"
|
|
123
|
+
},
|
|
124
|
+
"noActivities": {
|
|
125
|
+
"title": "No activities scheduled",
|
|
126
|
+
"description": "Schedule calls, meetings, and tasks.",
|
|
127
|
+
"action": "Schedule activity"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|