@nextsparkjs/theme-default 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/about/business.md +49 -0
- package/about/features.json +302 -0
- package/about/team.md +79 -0
- package/api/ai/chat/stream/route.ts +212 -0
- package/api/ai/orchestrator/route.ts +226 -0
- package/api/ai/single-agent/route.ts +291 -0
- package/api/ai/usage/route.ts +122 -0
- package/blocks/benefits/component.tsx +100 -0
- package/blocks/benefits/config.ts +11 -0
- package/blocks/benefits/examples.ts +85 -0
- package/blocks/benefits/fields.ts +156 -0
- package/blocks/benefits/schema.ts +33 -0
- package/blocks/cta-section/component.tsx +100 -0
- package/blocks/cta-section/config.ts +11 -0
- package/blocks/cta-section/examples.ts +41 -0
- package/blocks/cta-section/fields.ts +89 -0
- package/blocks/cta-section/index.ts +6 -0
- package/blocks/cta-section/schema.ts +32 -0
- package/blocks/cta-section/thumbnail.png +1 -0
- package/blocks/faq-accordion/component.tsx +156 -0
- package/blocks/faq-accordion/config.ts +11 -0
- package/blocks/faq-accordion/examples.ts +77 -0
- package/blocks/faq-accordion/fields.ts +119 -0
- package/blocks/faq-accordion/index.ts +6 -0
- package/blocks/faq-accordion/schema.ts +45 -0
- package/blocks/features-grid/component.tsx +112 -0
- package/blocks/features-grid/config.ts +11 -0
- package/blocks/features-grid/examples.ts +63 -0
- package/blocks/features-grid/fields.ts +97 -0
- package/blocks/features-grid/index.ts +6 -0
- package/blocks/features-grid/schema.ts +40 -0
- package/blocks/features-grid/thumbnail.png +1 -0
- package/blocks/hero/component.tsx +100 -0
- package/blocks/hero/config.ts +11 -0
- package/blocks/hero/examples.ts +35 -0
- package/blocks/hero/fields.ts +60 -0
- package/blocks/hero/index.ts +6 -0
- package/blocks/hero/schema.ts +32 -0
- package/blocks/hero/thumbnail.png +1 -0
- package/blocks/hero/thumbnail.png.txt +6 -0
- package/blocks/hero-with-form/component.tsx +232 -0
- package/blocks/hero-with-form/config.ts +11 -0
- package/blocks/hero-with-form/examples.ts +16 -0
- package/blocks/hero-with-form/fields.ts +207 -0
- package/blocks/hero-with-form/index.ts +6 -0
- package/blocks/hero-with-form/schema.ts +54 -0
- package/blocks/jumbotron/component.tsx +136 -0
- package/blocks/jumbotron/config.ts +11 -0
- package/blocks/jumbotron/examples.ts +36 -0
- package/blocks/jumbotron/fields.ts +202 -0
- package/blocks/jumbotron/index.ts +6 -0
- package/blocks/jumbotron/schema.ts +55 -0
- package/blocks/logo-cloud/component.tsx +154 -0
- package/blocks/logo-cloud/config.ts +11 -0
- package/blocks/logo-cloud/examples.ts +34 -0
- package/blocks/logo-cloud/fields.ts +133 -0
- package/blocks/logo-cloud/index.ts +6 -0
- package/blocks/logo-cloud/schema.ts +46 -0
- package/blocks/post-content/component.tsx +197 -0
- package/blocks/post-content/config.ts +11 -0
- package/blocks/post-content/examples.ts +33 -0
- package/blocks/post-content/fields.ts +165 -0
- package/blocks/post-content/index.ts +4 -0
- package/blocks/post-content/schema.ts +46 -0
- package/blocks/pricing-table/component.tsx +154 -0
- package/blocks/pricing-table/config.ts +11 -0
- package/blocks/pricing-table/examples.ts +96 -0
- package/blocks/pricing-table/fields.ts +161 -0
- package/blocks/pricing-table/index.ts +4 -0
- package/blocks/pricing-table/schema.ts +50 -0
- package/blocks/split-content/component.tsx +135 -0
- package/blocks/split-content/config.ts +11 -0
- package/blocks/split-content/examples.ts +38 -0
- package/blocks/split-content/fields.ts +198 -0
- package/blocks/split-content/index.ts +6 -0
- package/blocks/split-content/schema.ts +67 -0
- package/blocks/stats-counter/component.tsx +124 -0
- package/blocks/stats-counter/config.ts +11 -0
- package/blocks/stats-counter/examples.ts +61 -0
- package/blocks/stats-counter/fields.ts +134 -0
- package/blocks/stats-counter/index.ts +6 -0
- package/blocks/stats-counter/schema.ts +47 -0
- package/blocks/testimonials/component.tsx +114 -0
- package/blocks/testimonials/config.ts +11 -0
- package/blocks/testimonials/examples.ts +65 -0
- package/blocks/testimonials/fields.ts +105 -0
- package/blocks/testimonials/index.ts +6 -0
- package/blocks/testimonials/schema.ts +41 -0
- package/blocks/testimonials/thumbnail.png +1 -0
- package/blocks/text-content/component.tsx +97 -0
- package/blocks/text-content/config.ts +11 -0
- package/blocks/text-content/examples.ts +30 -0
- package/blocks/text-content/fields.ts +88 -0
- package/blocks/text-content/index.ts +6 -0
- package/blocks/text-content/schema.ts +30 -0
- package/blocks/text-content/thumbnail.png +1 -0
- package/blocks/timeline/component.tsx +267 -0
- package/blocks/timeline/config.ts +11 -0
- package/blocks/timeline/examples.ts +68 -0
- package/blocks/timeline/fields.ts +147 -0
- package/blocks/timeline/index.ts +6 -0
- package/blocks/timeline/schema.ts +49 -0
- package/blocks/video-hero/component.tsx +270 -0
- package/blocks/video-hero/config.ts +11 -0
- package/blocks/video-hero/examples.ts +24 -0
- package/blocks/video-hero/fields.ts +98 -0
- package/blocks/video-hero/index.ts +6 -0
- package/blocks/video-hero/schema.ts +39 -0
- package/components/ai-chat/ChatPanel.tsx +575 -0
- package/components/ai-chat/ConversationItem.tsx +266 -0
- package/components/ai-chat/ConversationSidebar.tsx +99 -0
- package/components/ai-chat/MarkdownRenderer.tsx +15 -0
- package/components/ai-chat/Message.tsx +42 -0
- package/components/ai-chat/MessageInput.tsx +49 -0
- package/components/ai-chat/MessageList.tsx +46 -0
- package/components/ai-chat/TypingIndicator.tsx +11 -0
- package/config/app.config.ts +367 -0
- package/config/billing.config.ts +349 -0
- package/config/dashboard.config.ts +506 -0
- package/config/dev.config.ts +104 -0
- package/config/features.config.ts +203 -0
- package/config/flows.config.ts +129 -0
- package/config/permissions.config.ts +245 -0
- package/config/theme.config.ts +74 -0
- package/docs/01-overview/01-introduction.md +335 -0
- package/docs/01-overview/02-customization.md +671 -0
- package/docs/02-features/01-components.md +155 -0
- package/docs/02-features/02-styling.md +139 -0
- package/docs/02-features/03-tasks-entity.md +407 -0
- package/docs/03-ai/01-overview.md +211 -0
- package/docs/03-ai/02-customization.md +436 -0
- package/entities/customers/customers.config.ts +75 -0
- package/entities/customers/customers.fields.ts +165 -0
- package/entities/customers/customers.service.ts +516 -0
- package/entities/customers/customers.types.ts +83 -0
- package/entities/customers/messages/en.json +66 -0
- package/entities/customers/messages/es.json +66 -0
- package/entities/customers/migrations/001_customers_table.sql +102 -0
- package/entities/customers/migrations/002_customers_metas.sql +92 -0
- package/entities/pages/messages/en.json +41 -0
- package/entities/pages/messages/es.json +41 -0
- package/entities/pages/migrations/001_pages_table.sql +112 -0
- package/entities/pages/migrations/002_pages_metas.sql +56 -0
- package/entities/pages/migrations/003_add_status.sql +50 -0
- package/entities/pages/pages-management.service.ts +610 -0
- package/entities/pages/pages.config.ts +94 -0
- package/entities/pages/pages.fields.ts +101 -0
- package/entities/pages/pages.service.ts +290 -0
- package/entities/pages/pages.types.ts +124 -0
- package/entities/posts/components/post-header.tsx +97 -0
- package/entities/posts/messages/en.json +55 -0
- package/entities/posts/messages/es.json +55 -0
- package/entities/posts/migrations/001_posts_table.sql +115 -0
- package/entities/posts/migrations/003_add_status.sql +44 -0
- package/entities/posts/migrations/004_entity_taxonomy_relations.sql +129 -0
- package/entities/posts/migrations/006_posts_metas.sql +56 -0
- package/entities/posts/posts.config.ts +101 -0
- package/entities/posts/posts.fields.ts +116 -0
- package/entities/posts/posts.service.ts +376 -0
- package/entities/posts/posts.types.ts +74 -0
- package/entities/tasks/messages/en.json +204 -0
- package/entities/tasks/messages/es.json +204 -0
- package/entities/tasks/migrations/001_tasks_table.sql +105 -0
- package/entities/tasks/migrations/002_task_metas.sql +85 -0
- package/entities/tasks/migrations/sample_data.json +77 -0
- package/entities/tasks/tasks.config.ts +79 -0
- package/entities/tasks/tasks.fields.ts +196 -0
- package/entities/tasks/tasks.service.ts +541 -0
- package/entities/tasks/tasks.types.ts +56 -0
- package/lib/hooks/useAiChat.ts +114 -0
- package/lib/hooks/useConversations.ts +376 -0
- package/lib/hooks/useOrchestratorChat.ts +122 -0
- package/lib/hooks/usePersistentChat.ts +315 -0
- package/lib/hooks/useStreamingChat.ts +127 -0
- package/lib/hooks/useTokenUsage.ts +63 -0
- package/lib/langchain/agents/customer-assistant.md +69 -0
- package/lib/langchain/agents/index.ts +61 -0
- package/lib/langchain/agents/orchestrator.md +59 -0
- package/lib/langchain/agents/page-assistant.md +85 -0
- package/lib/langchain/agents/single-agent.md +46 -0
- package/lib/langchain/agents/task-assistant.md +55 -0
- package/lib/langchain/config.ts +45 -0
- package/lib/langchain/handlers/customer-handler.ts +338 -0
- package/lib/langchain/handlers/page-handler.ts +232 -0
- package/lib/langchain/handlers/task-handler.ts +323 -0
- package/lib/langchain/langchain.config.ts +223 -0
- package/lib/langchain/observability.config.ts +30 -0
- package/lib/langchain/orchestrator.ts +562 -0
- package/lib/langchain/tools/customers.ts +176 -0
- package/lib/langchain/tools/index.ts +10 -0
- package/lib/langchain/tools/orchestrator.ts +92 -0
- package/lib/langchain/tools/pages.ts +289 -0
- package/lib/langchain/tools/tasks.ts +167 -0
- package/lib/scheduled-actions/billing.ts +149 -0
- package/lib/scheduled-actions/index.ts +170 -0
- package/lib/scheduled-actions/webhook.ts +231 -0
- package/lib/selectors.ts +197 -0
- package/messages/de/admin.json +219 -0
- package/messages/de/aiUsage.json +36 -0
- package/messages/de/buttons.json +19 -0
- package/messages/de/categories.json +35 -0
- package/messages/de/common.json +16 -0
- package/messages/de/dev.json +101 -0
- package/messages/de/docs.json +27 -0
- package/messages/de/entities.json +7 -0
- package/messages/de/features.json +119 -0
- package/messages/de/footer.json +22 -0
- package/messages/de/home.json +57 -0
- package/messages/de/index.ts +39 -0
- package/messages/de/mobileNav.json +13 -0
- package/messages/de/navigation.json +8 -0
- package/messages/de/observability.json +74 -0
- package/messages/de/posts.json +54 -0
- package/messages/de/pricing.json +102 -0
- package/messages/de/support.json +9 -0
- package/messages/de/teams.json +8 -0
- package/messages/en/admin.json +219 -0
- package/messages/en/aiUsage.json +36 -0
- package/messages/en/buttons.json +19 -0
- package/messages/en/categories.json +35 -0
- package/messages/en/common.json +16 -0
- package/messages/en/dev.json +106 -0
- package/messages/en/docs.json +27 -0
- package/messages/en/entities.json +7 -0
- package/messages/en/features.json +119 -0
- package/messages/en/footer.json +22 -0
- package/messages/en/home.json +57 -0
- package/messages/en/index.ts +39 -0
- package/messages/en/mobileNav.json +13 -0
- package/messages/en/navigation.json +8 -0
- package/messages/en/observability.json +74 -0
- package/messages/en/posts.json +54 -0
- package/messages/en/pricing.json +102 -0
- package/messages/en/support.json +9 -0
- package/messages/en/teams.json +8 -0
- package/messages/es/admin.json +219 -0
- package/messages/es/aiUsage.json +36 -0
- package/messages/es/buttons.json +19 -0
- package/messages/es/categories.json +35 -0
- package/messages/es/common.json +16 -0
- package/messages/es/dev.json +101 -0
- package/messages/es/docs.json +27 -0
- package/messages/es/entities.json +7 -0
- package/messages/es/features.json +119 -0
- package/messages/es/footer.json +22 -0
- package/messages/es/home.json +57 -0
- package/messages/es/index.ts +39 -0
- package/messages/es/mobileNav.json +13 -0
- package/messages/es/navigation.json +8 -0
- package/messages/es/observability.json +74 -0
- package/messages/es/posts.json +54 -0
- package/messages/es/pricing.json +102 -0
- package/messages/es/support.json +9 -0
- package/messages/es/teams.json +8 -0
- package/messages/fr/admin.json +219 -0
- package/messages/fr/aiUsage.json +36 -0
- package/messages/fr/buttons.json +19 -0
- package/messages/fr/categories.json +35 -0
- package/messages/fr/common.json +16 -0
- package/messages/fr/dev.json +101 -0
- package/messages/fr/docs.json +27 -0
- package/messages/fr/entities.json +7 -0
- package/messages/fr/features.json +119 -0
- package/messages/fr/footer.json +22 -0
- package/messages/fr/home.json +57 -0
- package/messages/fr/index.ts +39 -0
- package/messages/fr/mobileNav.json +13 -0
- package/messages/fr/navigation.json +8 -0
- package/messages/fr/observability.json +74 -0
- package/messages/fr/posts.json +54 -0
- package/messages/fr/pricing.json +102 -0
- package/messages/fr/support.json +9 -0
- package/messages/fr/teams.json +8 -0
- package/messages/it/admin.json +219 -0
- package/messages/it/aiUsage.json +36 -0
- package/messages/it/buttons.json +19 -0
- package/messages/it/categories.json +35 -0
- package/messages/it/common.json +16 -0
- package/messages/it/dev.json +101 -0
- package/messages/it/docs.json +27 -0
- package/messages/it/entities.json +7 -0
- package/messages/it/features.json +119 -0
- package/messages/it/footer.json +22 -0
- package/messages/it/home.json +57 -0
- package/messages/it/index.ts +39 -0
- package/messages/it/mobileNav.json +13 -0
- package/messages/it/navigation.json +8 -0
- package/messages/it/observability.json +74 -0
- package/messages/it/posts.json +54 -0
- package/messages/it/pricing.json +102 -0
- package/messages/it/support.json +9 -0
- package/messages/it/teams.json +8 -0
- package/messages/pt/admin.json +219 -0
- package/messages/pt/aiUsage.json +36 -0
- package/messages/pt/buttons.json +19 -0
- package/messages/pt/categories.json +35 -0
- package/messages/pt/common.json +16 -0
- package/messages/pt/dev.json +101 -0
- package/messages/pt/docs.json +27 -0
- package/messages/pt/entities.json +7 -0
- package/messages/pt/features.json +119 -0
- package/messages/pt/footer.json +22 -0
- package/messages/pt/home.json +57 -0
- package/messages/pt/index.ts +39 -0
- package/messages/pt/mobileNav.json +13 -0
- package/messages/pt/navigation.json +8 -0
- package/messages/pt/observability.json +74 -0
- package/messages/pt/posts.json +54 -0
- package/messages/pt/pricing.json +102 -0
- package/messages/pt/support.json +9 -0
- package/messages/pt/teams.json +8 -0
- package/migrations/089_add_editor_team_role.sql +39 -0
- package/migrations/090_demo_users_teams.sql +540 -0
- package/migrations/091_greek_teams_billing.sql +523 -0
- package/migrations/092_billing_sample_data.sql +774 -0
- package/migrations/093_pages_sample_data.sql +1158 -0
- package/migrations/094_posts_sample_data.sql +278 -0
- package/migrations/095_tasks_sample_data.sql +440 -0
- package/migrations/096_customers_sample_data.sql +358 -0
- package/migrations/097_scheduled_actions_sample_data.sql +111 -0
- package/package.json +22 -0
- package/public/docs/desktop-layout-example.png +0 -0
- package/styles/components.css +11 -0
- package/styles/globals.css +179 -0
- package/templates/(public)/blog/[slug]/page.tsx +65 -0
- package/templates/(public)/layout.tsx +25 -0
- package/templates/(public)/page.tsx +200 -0
- package/templates/(public)/support/page.tsx +321 -0
- package/templates/dashboard/(main)/agent-multi/page.tsx +63 -0
- package/templates/dashboard/(main)/agent-single/page.tsx +142 -0
- package/templates/dashboard/(main)/settings/ai-usage/page.tsx +157 -0
- package/templates/superadmin/ai-observability/[traceId]/page.tsx +27 -0
- package/templates/superadmin/ai-observability/page.tsx +17 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customers Entity Fields Configuration
|
|
3
|
+
*
|
|
4
|
+
* Separated from main config according to new refactoring plan.
|
|
5
|
+
* Contains all field definitions for the customers entity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
|
|
9
|
+
|
|
10
|
+
export const customersFields: EntityField[] = [
|
|
11
|
+
{
|
|
12
|
+
name: 'name',
|
|
13
|
+
type: 'text',
|
|
14
|
+
required: true,
|
|
15
|
+
display: {
|
|
16
|
+
label: 'Name',
|
|
17
|
+
description: 'Customer name',
|
|
18
|
+
placeholder: 'Enter customer name...',
|
|
19
|
+
showInList: true,
|
|
20
|
+
showInDetail: true,
|
|
21
|
+
showInForm: true,
|
|
22
|
+
order: 1,
|
|
23
|
+
columnWidth: 6,
|
|
24
|
+
},
|
|
25
|
+
api: {
|
|
26
|
+
searchable: true,
|
|
27
|
+
sortable: true,
|
|
28
|
+
readOnly: false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'account',
|
|
33
|
+
type: 'number',
|
|
34
|
+
required: true,
|
|
35
|
+
display: {
|
|
36
|
+
label: 'Account Number',
|
|
37
|
+
description: 'Unique customer account number (used for identification and external integrations)',
|
|
38
|
+
placeholder: 'Enter account number...',
|
|
39
|
+
showInList: true,
|
|
40
|
+
showInDetail: true,
|
|
41
|
+
showInForm: true,
|
|
42
|
+
order: 2,
|
|
43
|
+
columnWidth: 3,
|
|
44
|
+
},
|
|
45
|
+
api: {
|
|
46
|
+
searchable: true,
|
|
47
|
+
sortable: true,
|
|
48
|
+
readOnly: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'office',
|
|
53
|
+
type: 'text',
|
|
54
|
+
required: true,
|
|
55
|
+
display: {
|
|
56
|
+
label: 'Office',
|
|
57
|
+
description: 'Customer office/branch',
|
|
58
|
+
placeholder: 'Enter office...',
|
|
59
|
+
showInList: true,
|
|
60
|
+
showInDetail: true,
|
|
61
|
+
showInForm: true,
|
|
62
|
+
order: 3,
|
|
63
|
+
columnWidth: 3,
|
|
64
|
+
},
|
|
65
|
+
api: {
|
|
66
|
+
searchable: true,
|
|
67
|
+
sortable: true,
|
|
68
|
+
readOnly: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'phone',
|
|
73
|
+
type: 'text',
|
|
74
|
+
required: false,
|
|
75
|
+
display: {
|
|
76
|
+
label: 'Phone',
|
|
77
|
+
description: 'Customer phone number',
|
|
78
|
+
placeholder: 'Enter phone number...',
|
|
79
|
+
showInList: true,
|
|
80
|
+
showInDetail: true,
|
|
81
|
+
showInForm: true,
|
|
82
|
+
order: 4,
|
|
83
|
+
columnWidth: 4,
|
|
84
|
+
},
|
|
85
|
+
api: {
|
|
86
|
+
searchable: true,
|
|
87
|
+
sortable: false,
|
|
88
|
+
readOnly: false,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'salesRep',
|
|
93
|
+
type: 'text',
|
|
94
|
+
required: false,
|
|
95
|
+
display: {
|
|
96
|
+
label: 'Sales Representative',
|
|
97
|
+
description: 'Assigned sales representative',
|
|
98
|
+
placeholder: 'Enter sales rep...',
|
|
99
|
+
showInList: true,
|
|
100
|
+
showInDetail: true,
|
|
101
|
+
showInForm: true,
|
|
102
|
+
order: 5,
|
|
103
|
+
columnWidth: 4,
|
|
104
|
+
},
|
|
105
|
+
api: {
|
|
106
|
+
searchable: true,
|
|
107
|
+
sortable: true,
|
|
108
|
+
readOnly: false,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'visitDays',
|
|
113
|
+
type: 'multiselect',
|
|
114
|
+
required: false,
|
|
115
|
+
options: [
|
|
116
|
+
{ value: 'lun', label: 'Lunes' },
|
|
117
|
+
{ value: 'mar', label: 'Martes' },
|
|
118
|
+
{ value: 'mie', label: 'Miércoles' },
|
|
119
|
+
{ value: 'jue', label: 'Jueves' },
|
|
120
|
+
{ value: 'vie', label: 'Viernes' },
|
|
121
|
+
],
|
|
122
|
+
display: {
|
|
123
|
+
label: 'Visit Days',
|
|
124
|
+
description: 'Scheduled visit days (Monday to Friday)',
|
|
125
|
+
placeholder: 'Select visit days...',
|
|
126
|
+
showInList: false,
|
|
127
|
+
showInDetail: true,
|
|
128
|
+
showInForm: true,
|
|
129
|
+
order: 6,
|
|
130
|
+
columnWidth: 4,
|
|
131
|
+
},
|
|
132
|
+
api: {
|
|
133
|
+
searchable: false,
|
|
134
|
+
sortable: false,
|
|
135
|
+
readOnly: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'contactDays',
|
|
140
|
+
type: 'multiselect',
|
|
141
|
+
required: false,
|
|
142
|
+
options: [
|
|
143
|
+
{ value: 'lun', label: 'Lunes' },
|
|
144
|
+
{ value: 'mar', label: 'Martes' },
|
|
145
|
+
{ value: 'mie', label: 'Miércoles' },
|
|
146
|
+
{ value: 'jue', label: 'Jueves' },
|
|
147
|
+
{ value: 'vie', label: 'Viernes' },
|
|
148
|
+
],
|
|
149
|
+
display: {
|
|
150
|
+
label: 'Contact Days',
|
|
151
|
+
description: 'Preferred contact days (Monday to Friday)',
|
|
152
|
+
placeholder: 'Select contact days...',
|
|
153
|
+
showInList: false,
|
|
154
|
+
showInDetail: true,
|
|
155
|
+
showInForm: true,
|
|
156
|
+
order: 7,
|
|
157
|
+
columnWidth: 4,
|
|
158
|
+
},
|
|
159
|
+
api: {
|
|
160
|
+
searchable: false,
|
|
161
|
+
sortable: false,
|
|
162
|
+
readOnly: false,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
]
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customers Service
|
|
3
|
+
*
|
|
4
|
+
* Provides data access methods for customers.
|
|
5
|
+
* Customers is a private entity with shared: true - all authenticated
|
|
6
|
+
* users can access all records (no userId filter needed).
|
|
7
|
+
*
|
|
8
|
+
* All methods require authentication (use RLS).
|
|
9
|
+
*
|
|
10
|
+
* @module CustomersService
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
14
|
+
import type {
|
|
15
|
+
Customer,
|
|
16
|
+
CustomerListOptions,
|
|
17
|
+
CustomerListResult,
|
|
18
|
+
CustomerSearchOptions,
|
|
19
|
+
CustomerCreateData,
|
|
20
|
+
CustomerUpdateData,
|
|
21
|
+
DayOfWeek,
|
|
22
|
+
} from './customers.types'
|
|
23
|
+
|
|
24
|
+
// Database row type for customer
|
|
25
|
+
interface DbCustomer {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
account: number
|
|
29
|
+
office: string
|
|
30
|
+
phone: string | null
|
|
31
|
+
salesRep: string | null
|
|
32
|
+
visitDays: DayOfWeek[] | null
|
|
33
|
+
contactDays: DayOfWeek[] | null
|
|
34
|
+
createdAt: string
|
|
35
|
+
updatedAt: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class CustomersService {
|
|
39
|
+
// ============================================
|
|
40
|
+
// AUTHENTICATED METHODS (con RLS)
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get a customer by ID
|
|
45
|
+
*
|
|
46
|
+
* Respects RLS policies. Since customers has shared: true,
|
|
47
|
+
* any authenticated user can access all records.
|
|
48
|
+
*
|
|
49
|
+
* @param id - Customer ID
|
|
50
|
+
* @param userId - Current user ID for RLS
|
|
51
|
+
* @returns Customer data or null if not found
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* const customer = await CustomersService.getById('customer-uuid', currentUserId)
|
|
55
|
+
*/
|
|
56
|
+
static async getById(
|
|
57
|
+
id: string,
|
|
58
|
+
userId: string
|
|
59
|
+
): Promise<Customer | null> {
|
|
60
|
+
try {
|
|
61
|
+
if (!id || id.trim() === '') {
|
|
62
|
+
throw new Error('Customer ID is required')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!userId || userId.trim() === '') {
|
|
66
|
+
throw new Error('User ID is required for authentication')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const customer = await queryOneWithRLS<DbCustomer>(
|
|
70
|
+
`
|
|
71
|
+
SELECT
|
|
72
|
+
id,
|
|
73
|
+
name,
|
|
74
|
+
account,
|
|
75
|
+
office,
|
|
76
|
+
phone,
|
|
77
|
+
"salesRep",
|
|
78
|
+
"visitDays",
|
|
79
|
+
"contactDays",
|
|
80
|
+
"createdAt",
|
|
81
|
+
"updatedAt"
|
|
82
|
+
FROM customers
|
|
83
|
+
WHERE id = $1
|
|
84
|
+
`,
|
|
85
|
+
[id],
|
|
86
|
+
userId
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (!customer) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
id: customer.id,
|
|
95
|
+
name: customer.name,
|
|
96
|
+
account: customer.account,
|
|
97
|
+
office: customer.office,
|
|
98
|
+
phone: customer.phone ?? undefined,
|
|
99
|
+
salesRep: customer.salesRep ?? undefined,
|
|
100
|
+
visitDays: customer.visitDays ?? undefined,
|
|
101
|
+
contactDays: customer.contactDays ?? undefined,
|
|
102
|
+
createdAt: customer.createdAt,
|
|
103
|
+
updatedAt: customer.updatedAt,
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('CustomersService.getById error:', error)
|
|
107
|
+
throw new Error(
|
|
108
|
+
error instanceof Error ? error.message : 'Failed to fetch customer'
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List customers with pagination
|
|
115
|
+
*
|
|
116
|
+
* @param userId - Current user ID for RLS
|
|
117
|
+
* @param options - List options (limit, offset, orderBy, orderDir)
|
|
118
|
+
* @returns Object with customers array and total count
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const { customers, total } = await CustomersService.list(currentUserId, { limit: 10 })
|
|
122
|
+
*/
|
|
123
|
+
static async list(
|
|
124
|
+
userId: string,
|
|
125
|
+
options: CustomerListOptions = {}
|
|
126
|
+
): Promise<CustomerListResult> {
|
|
127
|
+
try {
|
|
128
|
+
if (!userId || userId.trim() === '') {
|
|
129
|
+
throw new Error('User ID is required for authentication')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const {
|
|
133
|
+
limit = 10,
|
|
134
|
+
offset = 0,
|
|
135
|
+
orderBy = 'name',
|
|
136
|
+
orderDir = 'asc',
|
|
137
|
+
} = options
|
|
138
|
+
|
|
139
|
+
// Validate orderBy to prevent SQL injection
|
|
140
|
+
const validOrderBy = ['name', 'account', 'office', 'salesRep', 'createdAt'].includes(orderBy)
|
|
141
|
+
? orderBy
|
|
142
|
+
: 'name'
|
|
143
|
+
const validOrderDir = orderDir === 'desc' ? 'DESC' : 'ASC'
|
|
144
|
+
|
|
145
|
+
// Map field names to database columns
|
|
146
|
+
const orderColumnMap: Record<string, string> = {
|
|
147
|
+
name: 'name',
|
|
148
|
+
account: 'account',
|
|
149
|
+
office: 'office',
|
|
150
|
+
salesRep: '"salesRep"',
|
|
151
|
+
createdAt: '"createdAt"',
|
|
152
|
+
}
|
|
153
|
+
const orderColumn = orderColumnMap[validOrderBy] || 'name'
|
|
154
|
+
|
|
155
|
+
// Get total count
|
|
156
|
+
const countResult = await queryWithRLS<{ count: string }>(
|
|
157
|
+
`SELECT COUNT(*)::text as count FROM customers`,
|
|
158
|
+
[],
|
|
159
|
+
userId
|
|
160
|
+
)
|
|
161
|
+
const total = parseInt(countResult[0]?.count || '0', 10)
|
|
162
|
+
|
|
163
|
+
// Get customers
|
|
164
|
+
const customers = await queryWithRLS<DbCustomer>(
|
|
165
|
+
`
|
|
166
|
+
SELECT
|
|
167
|
+
id,
|
|
168
|
+
name,
|
|
169
|
+
account,
|
|
170
|
+
office,
|
|
171
|
+
phone,
|
|
172
|
+
"salesRep",
|
|
173
|
+
"visitDays",
|
|
174
|
+
"contactDays",
|
|
175
|
+
"createdAt",
|
|
176
|
+
"updatedAt"
|
|
177
|
+
FROM customers
|
|
178
|
+
ORDER BY ${orderColumn} ${validOrderDir}
|
|
179
|
+
LIMIT $1 OFFSET $2
|
|
180
|
+
`,
|
|
181
|
+
[limit, offset],
|
|
182
|
+
userId
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
customers: customers.map((customer) => ({
|
|
187
|
+
id: customer.id,
|
|
188
|
+
name: customer.name,
|
|
189
|
+
account: customer.account,
|
|
190
|
+
office: customer.office,
|
|
191
|
+
phone: customer.phone ?? undefined,
|
|
192
|
+
salesRep: customer.salesRep ?? undefined,
|
|
193
|
+
visitDays: customer.visitDays ?? undefined,
|
|
194
|
+
contactDays: customer.contactDays ?? undefined,
|
|
195
|
+
createdAt: customer.createdAt,
|
|
196
|
+
updatedAt: customer.updatedAt,
|
|
197
|
+
})),
|
|
198
|
+
total,
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error('CustomersService.list error:', error)
|
|
202
|
+
throw new Error(
|
|
203
|
+
error instanceof Error ? error.message : 'Failed to list customers'
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Search customers by name, account, office, or salesRep
|
|
210
|
+
*
|
|
211
|
+
* @param userId - Current user ID for RLS
|
|
212
|
+
* @param options - Search options (query, limit)
|
|
213
|
+
* @returns Array of matching customers
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const results = await CustomersService.search(currentUserId, { query: 'acme', limit: 5 })
|
|
217
|
+
*/
|
|
218
|
+
static async search(
|
|
219
|
+
userId: string,
|
|
220
|
+
options: CustomerSearchOptions
|
|
221
|
+
): Promise<Customer[]> {
|
|
222
|
+
try {
|
|
223
|
+
if (!userId || userId.trim() === '') {
|
|
224
|
+
throw new Error('User ID is required for authentication')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { query, limit = 10 } = options
|
|
228
|
+
|
|
229
|
+
if (!query || query.trim() === '') {
|
|
230
|
+
return []
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const searchTerm = `%${query.trim()}%`
|
|
234
|
+
|
|
235
|
+
const customers = await queryWithRLS<DbCustomer>(
|
|
236
|
+
`
|
|
237
|
+
SELECT
|
|
238
|
+
id,
|
|
239
|
+
name,
|
|
240
|
+
account,
|
|
241
|
+
office,
|
|
242
|
+
phone,
|
|
243
|
+
"salesRep",
|
|
244
|
+
"visitDays",
|
|
245
|
+
"contactDays",
|
|
246
|
+
"createdAt",
|
|
247
|
+
"updatedAt"
|
|
248
|
+
FROM customers
|
|
249
|
+
WHERE
|
|
250
|
+
name ILIKE $1
|
|
251
|
+
OR office ILIKE $1
|
|
252
|
+
OR "salesRep" ILIKE $1
|
|
253
|
+
OR account::text ILIKE $1
|
|
254
|
+
ORDER BY name ASC
|
|
255
|
+
LIMIT $2
|
|
256
|
+
`,
|
|
257
|
+
[searchTerm, limit],
|
|
258
|
+
userId
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return customers.map((customer) => ({
|
|
262
|
+
id: customer.id,
|
|
263
|
+
name: customer.name,
|
|
264
|
+
account: customer.account,
|
|
265
|
+
office: customer.office,
|
|
266
|
+
phone: customer.phone ?? undefined,
|
|
267
|
+
salesRep: customer.salesRep ?? undefined,
|
|
268
|
+
visitDays: customer.visitDays ?? undefined,
|
|
269
|
+
contactDays: customer.contactDays ?? undefined,
|
|
270
|
+
createdAt: customer.createdAt,
|
|
271
|
+
updatedAt: customer.updatedAt,
|
|
272
|
+
}))
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error('CustomersService.search error:', error)
|
|
275
|
+
throw new Error(
|
|
276
|
+
error instanceof Error ? error.message : 'Failed to search customers'
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================
|
|
282
|
+
// WRITE METHODS (con RLS)
|
|
283
|
+
// ============================================
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Create a new customer
|
|
287
|
+
*
|
|
288
|
+
* @param userId - Current user ID for RLS
|
|
289
|
+
* @param data - Customer data to create
|
|
290
|
+
* @returns Created customer
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* const customer = await CustomersService.create(currentUserId, {
|
|
294
|
+
* name: 'Acme Corp',
|
|
295
|
+
* account: 12345,
|
|
296
|
+
* office: 'Central',
|
|
297
|
+
* teamId: 'team-123'
|
|
298
|
+
* })
|
|
299
|
+
*/
|
|
300
|
+
static async create(
|
|
301
|
+
userId: string,
|
|
302
|
+
data: CustomerCreateData
|
|
303
|
+
): Promise<Customer> {
|
|
304
|
+
try {
|
|
305
|
+
if (!userId || userId.trim() === '') {
|
|
306
|
+
throw new Error('User ID is required for authentication')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!data.name || !data.office || data.account === undefined) {
|
|
310
|
+
throw new Error('Name, account, and office are required')
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const id = crypto.randomUUID()
|
|
314
|
+
const now = new Date().toISOString()
|
|
315
|
+
|
|
316
|
+
const result = await mutateWithRLS<DbCustomer>(
|
|
317
|
+
`
|
|
318
|
+
INSERT INTO customers (
|
|
319
|
+
id, "userId", "teamId", name, account, office,
|
|
320
|
+
phone, "salesRep", "visitDays", "contactDays",
|
|
321
|
+
"createdAt", "updatedAt"
|
|
322
|
+
)
|
|
323
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
324
|
+
RETURNING
|
|
325
|
+
id, name, account, office, phone, "salesRep",
|
|
326
|
+
"visitDays", "contactDays", "createdAt", "updatedAt"
|
|
327
|
+
`,
|
|
328
|
+
[
|
|
329
|
+
id,
|
|
330
|
+
userId,
|
|
331
|
+
data.teamId,
|
|
332
|
+
data.name,
|
|
333
|
+
data.account,
|
|
334
|
+
data.office,
|
|
335
|
+
data.phone || null,
|
|
336
|
+
data.salesRep || null,
|
|
337
|
+
data.visitDays ? JSON.stringify(data.visitDays) : null,
|
|
338
|
+
data.contactDays ? JSON.stringify(data.contactDays) : null,
|
|
339
|
+
now,
|
|
340
|
+
now,
|
|
341
|
+
],
|
|
342
|
+
userId
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if (!result.rows[0]) {
|
|
346
|
+
throw new Error('Failed to create customer')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const customer = result.rows[0]
|
|
350
|
+
return {
|
|
351
|
+
id: customer.id,
|
|
352
|
+
name: customer.name,
|
|
353
|
+
account: customer.account,
|
|
354
|
+
office: customer.office,
|
|
355
|
+
phone: customer.phone ?? undefined,
|
|
356
|
+
salesRep: customer.salesRep ?? undefined,
|
|
357
|
+
visitDays: customer.visitDays ?? undefined,
|
|
358
|
+
contactDays: customer.contactDays ?? undefined,
|
|
359
|
+
createdAt: customer.createdAt,
|
|
360
|
+
updatedAt: customer.updatedAt,
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error('CustomersService.create error:', error)
|
|
364
|
+
throw new Error(
|
|
365
|
+
error instanceof Error ? error.message : 'Failed to create customer'
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Update an existing customer
|
|
372
|
+
*
|
|
373
|
+
* @param userId - Current user ID for RLS
|
|
374
|
+
* @param id - Customer ID to update
|
|
375
|
+
* @param data - Fields to update
|
|
376
|
+
* @returns Updated customer
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* const customer = await CustomersService.update(currentUserId, 'customer-123', {
|
|
380
|
+
* phone: '555-1234',
|
|
381
|
+
* salesRep: 'John Doe'
|
|
382
|
+
* })
|
|
383
|
+
*/
|
|
384
|
+
static async update(
|
|
385
|
+
userId: string,
|
|
386
|
+
id: string,
|
|
387
|
+
data: CustomerUpdateData
|
|
388
|
+
): Promise<Customer> {
|
|
389
|
+
try {
|
|
390
|
+
if (!userId || userId.trim() === '') {
|
|
391
|
+
throw new Error('User ID is required for authentication')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!id || id.trim() === '') {
|
|
395
|
+
throw new Error('Customer ID is required')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Build dynamic update query
|
|
399
|
+
const updates: string[] = []
|
|
400
|
+
const values: unknown[] = []
|
|
401
|
+
let paramIndex = 1
|
|
402
|
+
|
|
403
|
+
if (data.name !== undefined) {
|
|
404
|
+
updates.push(`name = $${paramIndex++}`)
|
|
405
|
+
values.push(data.name)
|
|
406
|
+
}
|
|
407
|
+
if (data.account !== undefined) {
|
|
408
|
+
updates.push(`account = $${paramIndex++}`)
|
|
409
|
+
values.push(data.account)
|
|
410
|
+
}
|
|
411
|
+
if (data.office !== undefined) {
|
|
412
|
+
updates.push(`office = $${paramIndex++}`)
|
|
413
|
+
values.push(data.office)
|
|
414
|
+
}
|
|
415
|
+
if (data.phone !== undefined) {
|
|
416
|
+
updates.push(`phone = $${paramIndex++}`)
|
|
417
|
+
values.push(data.phone || null)
|
|
418
|
+
}
|
|
419
|
+
if (data.salesRep !== undefined) {
|
|
420
|
+
updates.push(`"salesRep" = $${paramIndex++}`)
|
|
421
|
+
values.push(data.salesRep || null)
|
|
422
|
+
}
|
|
423
|
+
if (data.visitDays !== undefined) {
|
|
424
|
+
updates.push(`"visitDays" = $${paramIndex++}`)
|
|
425
|
+
values.push(data.visitDays ? JSON.stringify(data.visitDays) : null)
|
|
426
|
+
}
|
|
427
|
+
if (data.contactDays !== undefined) {
|
|
428
|
+
updates.push(`"contactDays" = $${paramIndex++}`)
|
|
429
|
+
values.push(data.contactDays ? JSON.stringify(data.contactDays) : null)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (updates.length === 0) {
|
|
433
|
+
throw new Error('No fields to update')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
updates.push(`"updatedAt" = $${paramIndex++}`)
|
|
437
|
+
values.push(new Date().toISOString())
|
|
438
|
+
|
|
439
|
+
values.push(id)
|
|
440
|
+
|
|
441
|
+
const result = await mutateWithRLS<DbCustomer>(
|
|
442
|
+
`
|
|
443
|
+
UPDATE customers
|
|
444
|
+
SET ${updates.join(', ')}
|
|
445
|
+
WHERE id = $${paramIndex}
|
|
446
|
+
RETURNING
|
|
447
|
+
id, name, account, office, phone, "salesRep",
|
|
448
|
+
"visitDays", "contactDays", "createdAt", "updatedAt"
|
|
449
|
+
`,
|
|
450
|
+
values,
|
|
451
|
+
userId
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
if (!result.rows[0]) {
|
|
455
|
+
throw new Error('Customer not found or update failed')
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const customer = result.rows[0]
|
|
459
|
+
return {
|
|
460
|
+
id: customer.id,
|
|
461
|
+
name: customer.name,
|
|
462
|
+
account: customer.account,
|
|
463
|
+
office: customer.office,
|
|
464
|
+
phone: customer.phone ?? undefined,
|
|
465
|
+
salesRep: customer.salesRep ?? undefined,
|
|
466
|
+
visitDays: customer.visitDays ?? undefined,
|
|
467
|
+
contactDays: customer.contactDays ?? undefined,
|
|
468
|
+
createdAt: customer.createdAt,
|
|
469
|
+
updatedAt: customer.updatedAt,
|
|
470
|
+
}
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error('CustomersService.update error:', error)
|
|
473
|
+
throw new Error(
|
|
474
|
+
error instanceof Error ? error.message : 'Failed to update customer'
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Delete a customer
|
|
481
|
+
*
|
|
482
|
+
* @param userId - Current user ID for RLS
|
|
483
|
+
* @param id - Customer ID to delete
|
|
484
|
+
* @returns true if deleted successfully
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* const success = await CustomersService.delete(currentUserId, 'customer-123')
|
|
488
|
+
*/
|
|
489
|
+
static async delete(
|
|
490
|
+
userId: string,
|
|
491
|
+
id: string
|
|
492
|
+
): Promise<boolean> {
|
|
493
|
+
try {
|
|
494
|
+
if (!userId || userId.trim() === '') {
|
|
495
|
+
throw new Error('User ID is required for authentication')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!id || id.trim() === '') {
|
|
499
|
+
throw new Error('Customer ID is required')
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const result = await mutateWithRLS(
|
|
503
|
+
`DELETE FROM customers WHERE id = $1`,
|
|
504
|
+
[id],
|
|
505
|
+
userId
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
return result.rowCount > 0
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error('CustomersService.delete error:', error)
|
|
511
|
+
throw new Error(
|
|
512
|
+
error instanceof Error ? error.message : 'Failed to delete customer'
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|