@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.
Files changed (333) hide show
  1. package/about/business.md +49 -0
  2. package/about/features.json +302 -0
  3. package/about/team.md +79 -0
  4. package/api/ai/chat/stream/route.ts +212 -0
  5. package/api/ai/orchestrator/route.ts +226 -0
  6. package/api/ai/single-agent/route.ts +291 -0
  7. package/api/ai/usage/route.ts +122 -0
  8. package/blocks/benefits/component.tsx +100 -0
  9. package/blocks/benefits/config.ts +11 -0
  10. package/blocks/benefits/examples.ts +85 -0
  11. package/blocks/benefits/fields.ts +156 -0
  12. package/blocks/benefits/schema.ts +33 -0
  13. package/blocks/cta-section/component.tsx +100 -0
  14. package/blocks/cta-section/config.ts +11 -0
  15. package/blocks/cta-section/examples.ts +41 -0
  16. package/blocks/cta-section/fields.ts +89 -0
  17. package/blocks/cta-section/index.ts +6 -0
  18. package/blocks/cta-section/schema.ts +32 -0
  19. package/blocks/cta-section/thumbnail.png +1 -0
  20. package/blocks/faq-accordion/component.tsx +156 -0
  21. package/blocks/faq-accordion/config.ts +11 -0
  22. package/blocks/faq-accordion/examples.ts +77 -0
  23. package/blocks/faq-accordion/fields.ts +119 -0
  24. package/blocks/faq-accordion/index.ts +6 -0
  25. package/blocks/faq-accordion/schema.ts +45 -0
  26. package/blocks/features-grid/component.tsx +112 -0
  27. package/blocks/features-grid/config.ts +11 -0
  28. package/blocks/features-grid/examples.ts +63 -0
  29. package/blocks/features-grid/fields.ts +97 -0
  30. package/blocks/features-grid/index.ts +6 -0
  31. package/blocks/features-grid/schema.ts +40 -0
  32. package/blocks/features-grid/thumbnail.png +1 -0
  33. package/blocks/hero/component.tsx +100 -0
  34. package/blocks/hero/config.ts +11 -0
  35. package/blocks/hero/examples.ts +35 -0
  36. package/blocks/hero/fields.ts +60 -0
  37. package/blocks/hero/index.ts +6 -0
  38. package/blocks/hero/schema.ts +32 -0
  39. package/blocks/hero/thumbnail.png +1 -0
  40. package/blocks/hero/thumbnail.png.txt +6 -0
  41. package/blocks/hero-with-form/component.tsx +232 -0
  42. package/blocks/hero-with-form/config.ts +11 -0
  43. package/blocks/hero-with-form/examples.ts +16 -0
  44. package/blocks/hero-with-form/fields.ts +207 -0
  45. package/blocks/hero-with-form/index.ts +6 -0
  46. package/blocks/hero-with-form/schema.ts +54 -0
  47. package/blocks/jumbotron/component.tsx +136 -0
  48. package/blocks/jumbotron/config.ts +11 -0
  49. package/blocks/jumbotron/examples.ts +36 -0
  50. package/blocks/jumbotron/fields.ts +202 -0
  51. package/blocks/jumbotron/index.ts +6 -0
  52. package/blocks/jumbotron/schema.ts +55 -0
  53. package/blocks/logo-cloud/component.tsx +154 -0
  54. package/blocks/logo-cloud/config.ts +11 -0
  55. package/blocks/logo-cloud/examples.ts +34 -0
  56. package/blocks/logo-cloud/fields.ts +133 -0
  57. package/blocks/logo-cloud/index.ts +6 -0
  58. package/blocks/logo-cloud/schema.ts +46 -0
  59. package/blocks/post-content/component.tsx +197 -0
  60. package/blocks/post-content/config.ts +11 -0
  61. package/blocks/post-content/examples.ts +33 -0
  62. package/blocks/post-content/fields.ts +165 -0
  63. package/blocks/post-content/index.ts +4 -0
  64. package/blocks/post-content/schema.ts +46 -0
  65. package/blocks/pricing-table/component.tsx +154 -0
  66. package/blocks/pricing-table/config.ts +11 -0
  67. package/blocks/pricing-table/examples.ts +96 -0
  68. package/blocks/pricing-table/fields.ts +161 -0
  69. package/blocks/pricing-table/index.ts +4 -0
  70. package/blocks/pricing-table/schema.ts +50 -0
  71. package/blocks/split-content/component.tsx +135 -0
  72. package/blocks/split-content/config.ts +11 -0
  73. package/blocks/split-content/examples.ts +38 -0
  74. package/blocks/split-content/fields.ts +198 -0
  75. package/blocks/split-content/index.ts +6 -0
  76. package/blocks/split-content/schema.ts +67 -0
  77. package/blocks/stats-counter/component.tsx +124 -0
  78. package/blocks/stats-counter/config.ts +11 -0
  79. package/blocks/stats-counter/examples.ts +61 -0
  80. package/blocks/stats-counter/fields.ts +134 -0
  81. package/blocks/stats-counter/index.ts +6 -0
  82. package/blocks/stats-counter/schema.ts +47 -0
  83. package/blocks/testimonials/component.tsx +114 -0
  84. package/blocks/testimonials/config.ts +11 -0
  85. package/blocks/testimonials/examples.ts +65 -0
  86. package/blocks/testimonials/fields.ts +105 -0
  87. package/blocks/testimonials/index.ts +6 -0
  88. package/blocks/testimonials/schema.ts +41 -0
  89. package/blocks/testimonials/thumbnail.png +1 -0
  90. package/blocks/text-content/component.tsx +97 -0
  91. package/blocks/text-content/config.ts +11 -0
  92. package/blocks/text-content/examples.ts +30 -0
  93. package/blocks/text-content/fields.ts +88 -0
  94. package/blocks/text-content/index.ts +6 -0
  95. package/blocks/text-content/schema.ts +30 -0
  96. package/blocks/text-content/thumbnail.png +1 -0
  97. package/blocks/timeline/component.tsx +267 -0
  98. package/blocks/timeline/config.ts +11 -0
  99. package/blocks/timeline/examples.ts +68 -0
  100. package/blocks/timeline/fields.ts +147 -0
  101. package/blocks/timeline/index.ts +6 -0
  102. package/blocks/timeline/schema.ts +49 -0
  103. package/blocks/video-hero/component.tsx +270 -0
  104. package/blocks/video-hero/config.ts +11 -0
  105. package/blocks/video-hero/examples.ts +24 -0
  106. package/blocks/video-hero/fields.ts +98 -0
  107. package/blocks/video-hero/index.ts +6 -0
  108. package/blocks/video-hero/schema.ts +39 -0
  109. package/components/ai-chat/ChatPanel.tsx +575 -0
  110. package/components/ai-chat/ConversationItem.tsx +266 -0
  111. package/components/ai-chat/ConversationSidebar.tsx +99 -0
  112. package/components/ai-chat/MarkdownRenderer.tsx +15 -0
  113. package/components/ai-chat/Message.tsx +42 -0
  114. package/components/ai-chat/MessageInput.tsx +49 -0
  115. package/components/ai-chat/MessageList.tsx +46 -0
  116. package/components/ai-chat/TypingIndicator.tsx +11 -0
  117. package/config/app.config.ts +367 -0
  118. package/config/billing.config.ts +349 -0
  119. package/config/dashboard.config.ts +506 -0
  120. package/config/dev.config.ts +104 -0
  121. package/config/features.config.ts +203 -0
  122. package/config/flows.config.ts +129 -0
  123. package/config/permissions.config.ts +245 -0
  124. package/config/theme.config.ts +74 -0
  125. package/docs/01-overview/01-introduction.md +335 -0
  126. package/docs/01-overview/02-customization.md +671 -0
  127. package/docs/02-features/01-components.md +155 -0
  128. package/docs/02-features/02-styling.md +139 -0
  129. package/docs/02-features/03-tasks-entity.md +407 -0
  130. package/docs/03-ai/01-overview.md +211 -0
  131. package/docs/03-ai/02-customization.md +436 -0
  132. package/entities/customers/customers.config.ts +75 -0
  133. package/entities/customers/customers.fields.ts +165 -0
  134. package/entities/customers/customers.service.ts +516 -0
  135. package/entities/customers/customers.types.ts +83 -0
  136. package/entities/customers/messages/en.json +66 -0
  137. package/entities/customers/messages/es.json +66 -0
  138. package/entities/customers/migrations/001_customers_table.sql +102 -0
  139. package/entities/customers/migrations/002_customers_metas.sql +92 -0
  140. package/entities/pages/messages/en.json +41 -0
  141. package/entities/pages/messages/es.json +41 -0
  142. package/entities/pages/migrations/001_pages_table.sql +112 -0
  143. package/entities/pages/migrations/002_pages_metas.sql +56 -0
  144. package/entities/pages/migrations/003_add_status.sql +50 -0
  145. package/entities/pages/pages-management.service.ts +610 -0
  146. package/entities/pages/pages.config.ts +94 -0
  147. package/entities/pages/pages.fields.ts +101 -0
  148. package/entities/pages/pages.service.ts +290 -0
  149. package/entities/pages/pages.types.ts +124 -0
  150. package/entities/posts/components/post-header.tsx +97 -0
  151. package/entities/posts/messages/en.json +55 -0
  152. package/entities/posts/messages/es.json +55 -0
  153. package/entities/posts/migrations/001_posts_table.sql +115 -0
  154. package/entities/posts/migrations/003_add_status.sql +44 -0
  155. package/entities/posts/migrations/004_entity_taxonomy_relations.sql +129 -0
  156. package/entities/posts/migrations/006_posts_metas.sql +56 -0
  157. package/entities/posts/posts.config.ts +101 -0
  158. package/entities/posts/posts.fields.ts +116 -0
  159. package/entities/posts/posts.service.ts +376 -0
  160. package/entities/posts/posts.types.ts +74 -0
  161. package/entities/tasks/messages/en.json +204 -0
  162. package/entities/tasks/messages/es.json +204 -0
  163. package/entities/tasks/migrations/001_tasks_table.sql +105 -0
  164. package/entities/tasks/migrations/002_task_metas.sql +85 -0
  165. package/entities/tasks/migrations/sample_data.json +77 -0
  166. package/entities/tasks/tasks.config.ts +79 -0
  167. package/entities/tasks/tasks.fields.ts +196 -0
  168. package/entities/tasks/tasks.service.ts +541 -0
  169. package/entities/tasks/tasks.types.ts +56 -0
  170. package/lib/hooks/useAiChat.ts +114 -0
  171. package/lib/hooks/useConversations.ts +376 -0
  172. package/lib/hooks/useOrchestratorChat.ts +122 -0
  173. package/lib/hooks/usePersistentChat.ts +315 -0
  174. package/lib/hooks/useStreamingChat.ts +127 -0
  175. package/lib/hooks/useTokenUsage.ts +63 -0
  176. package/lib/langchain/agents/customer-assistant.md +69 -0
  177. package/lib/langchain/agents/index.ts +61 -0
  178. package/lib/langchain/agents/orchestrator.md +59 -0
  179. package/lib/langchain/agents/page-assistant.md +85 -0
  180. package/lib/langchain/agents/single-agent.md +46 -0
  181. package/lib/langchain/agents/task-assistant.md +55 -0
  182. package/lib/langchain/config.ts +45 -0
  183. package/lib/langchain/handlers/customer-handler.ts +338 -0
  184. package/lib/langchain/handlers/page-handler.ts +232 -0
  185. package/lib/langchain/handlers/task-handler.ts +323 -0
  186. package/lib/langchain/langchain.config.ts +223 -0
  187. package/lib/langchain/observability.config.ts +30 -0
  188. package/lib/langchain/orchestrator.ts +562 -0
  189. package/lib/langchain/tools/customers.ts +176 -0
  190. package/lib/langchain/tools/index.ts +10 -0
  191. package/lib/langchain/tools/orchestrator.ts +92 -0
  192. package/lib/langchain/tools/pages.ts +289 -0
  193. package/lib/langchain/tools/tasks.ts +167 -0
  194. package/lib/scheduled-actions/billing.ts +149 -0
  195. package/lib/scheduled-actions/index.ts +170 -0
  196. package/lib/scheduled-actions/webhook.ts +231 -0
  197. package/lib/selectors.ts +197 -0
  198. package/messages/de/admin.json +219 -0
  199. package/messages/de/aiUsage.json +36 -0
  200. package/messages/de/buttons.json +19 -0
  201. package/messages/de/categories.json +35 -0
  202. package/messages/de/common.json +16 -0
  203. package/messages/de/dev.json +101 -0
  204. package/messages/de/docs.json +27 -0
  205. package/messages/de/entities.json +7 -0
  206. package/messages/de/features.json +119 -0
  207. package/messages/de/footer.json +22 -0
  208. package/messages/de/home.json +57 -0
  209. package/messages/de/index.ts +39 -0
  210. package/messages/de/mobileNav.json +13 -0
  211. package/messages/de/navigation.json +8 -0
  212. package/messages/de/observability.json +74 -0
  213. package/messages/de/posts.json +54 -0
  214. package/messages/de/pricing.json +102 -0
  215. package/messages/de/support.json +9 -0
  216. package/messages/de/teams.json +8 -0
  217. package/messages/en/admin.json +219 -0
  218. package/messages/en/aiUsage.json +36 -0
  219. package/messages/en/buttons.json +19 -0
  220. package/messages/en/categories.json +35 -0
  221. package/messages/en/common.json +16 -0
  222. package/messages/en/dev.json +106 -0
  223. package/messages/en/docs.json +27 -0
  224. package/messages/en/entities.json +7 -0
  225. package/messages/en/features.json +119 -0
  226. package/messages/en/footer.json +22 -0
  227. package/messages/en/home.json +57 -0
  228. package/messages/en/index.ts +39 -0
  229. package/messages/en/mobileNav.json +13 -0
  230. package/messages/en/navigation.json +8 -0
  231. package/messages/en/observability.json +74 -0
  232. package/messages/en/posts.json +54 -0
  233. package/messages/en/pricing.json +102 -0
  234. package/messages/en/support.json +9 -0
  235. package/messages/en/teams.json +8 -0
  236. package/messages/es/admin.json +219 -0
  237. package/messages/es/aiUsage.json +36 -0
  238. package/messages/es/buttons.json +19 -0
  239. package/messages/es/categories.json +35 -0
  240. package/messages/es/common.json +16 -0
  241. package/messages/es/dev.json +101 -0
  242. package/messages/es/docs.json +27 -0
  243. package/messages/es/entities.json +7 -0
  244. package/messages/es/features.json +119 -0
  245. package/messages/es/footer.json +22 -0
  246. package/messages/es/home.json +57 -0
  247. package/messages/es/index.ts +39 -0
  248. package/messages/es/mobileNav.json +13 -0
  249. package/messages/es/navigation.json +8 -0
  250. package/messages/es/observability.json +74 -0
  251. package/messages/es/posts.json +54 -0
  252. package/messages/es/pricing.json +102 -0
  253. package/messages/es/support.json +9 -0
  254. package/messages/es/teams.json +8 -0
  255. package/messages/fr/admin.json +219 -0
  256. package/messages/fr/aiUsage.json +36 -0
  257. package/messages/fr/buttons.json +19 -0
  258. package/messages/fr/categories.json +35 -0
  259. package/messages/fr/common.json +16 -0
  260. package/messages/fr/dev.json +101 -0
  261. package/messages/fr/docs.json +27 -0
  262. package/messages/fr/entities.json +7 -0
  263. package/messages/fr/features.json +119 -0
  264. package/messages/fr/footer.json +22 -0
  265. package/messages/fr/home.json +57 -0
  266. package/messages/fr/index.ts +39 -0
  267. package/messages/fr/mobileNav.json +13 -0
  268. package/messages/fr/navigation.json +8 -0
  269. package/messages/fr/observability.json +74 -0
  270. package/messages/fr/posts.json +54 -0
  271. package/messages/fr/pricing.json +102 -0
  272. package/messages/fr/support.json +9 -0
  273. package/messages/fr/teams.json +8 -0
  274. package/messages/it/admin.json +219 -0
  275. package/messages/it/aiUsage.json +36 -0
  276. package/messages/it/buttons.json +19 -0
  277. package/messages/it/categories.json +35 -0
  278. package/messages/it/common.json +16 -0
  279. package/messages/it/dev.json +101 -0
  280. package/messages/it/docs.json +27 -0
  281. package/messages/it/entities.json +7 -0
  282. package/messages/it/features.json +119 -0
  283. package/messages/it/footer.json +22 -0
  284. package/messages/it/home.json +57 -0
  285. package/messages/it/index.ts +39 -0
  286. package/messages/it/mobileNav.json +13 -0
  287. package/messages/it/navigation.json +8 -0
  288. package/messages/it/observability.json +74 -0
  289. package/messages/it/posts.json +54 -0
  290. package/messages/it/pricing.json +102 -0
  291. package/messages/it/support.json +9 -0
  292. package/messages/it/teams.json +8 -0
  293. package/messages/pt/admin.json +219 -0
  294. package/messages/pt/aiUsage.json +36 -0
  295. package/messages/pt/buttons.json +19 -0
  296. package/messages/pt/categories.json +35 -0
  297. package/messages/pt/common.json +16 -0
  298. package/messages/pt/dev.json +101 -0
  299. package/messages/pt/docs.json +27 -0
  300. package/messages/pt/entities.json +7 -0
  301. package/messages/pt/features.json +119 -0
  302. package/messages/pt/footer.json +22 -0
  303. package/messages/pt/home.json +57 -0
  304. package/messages/pt/index.ts +39 -0
  305. package/messages/pt/mobileNav.json +13 -0
  306. package/messages/pt/navigation.json +8 -0
  307. package/messages/pt/observability.json +74 -0
  308. package/messages/pt/posts.json +54 -0
  309. package/messages/pt/pricing.json +102 -0
  310. package/messages/pt/support.json +9 -0
  311. package/messages/pt/teams.json +8 -0
  312. package/migrations/089_add_editor_team_role.sql +39 -0
  313. package/migrations/090_demo_users_teams.sql +540 -0
  314. package/migrations/091_greek_teams_billing.sql +523 -0
  315. package/migrations/092_billing_sample_data.sql +774 -0
  316. package/migrations/093_pages_sample_data.sql +1158 -0
  317. package/migrations/094_posts_sample_data.sql +278 -0
  318. package/migrations/095_tasks_sample_data.sql +440 -0
  319. package/migrations/096_customers_sample_data.sql +358 -0
  320. package/migrations/097_scheduled_actions_sample_data.sql +111 -0
  321. package/package.json +22 -0
  322. package/public/docs/desktop-layout-example.png +0 -0
  323. package/styles/components.css +11 -0
  324. package/styles/globals.css +179 -0
  325. package/templates/(public)/blog/[slug]/page.tsx +65 -0
  326. package/templates/(public)/layout.tsx +25 -0
  327. package/templates/(public)/page.tsx +200 -0
  328. package/templates/(public)/support/page.tsx +321 -0
  329. package/templates/dashboard/(main)/agent-multi/page.tsx +63 -0
  330. package/templates/dashboard/(main)/agent-single/page.tsx +142 -0
  331. package/templates/dashboard/(main)/settings/ai-usage/page.tsx +157 -0
  332. package/templates/superadmin/ai-observability/[traceId]/page.tsx +27 -0
  333. 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
+ }