create-crm-starter 0.1.0

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 (261) hide show
  1. package/README.md +53 -0
  2. package/dist/index.js +2341 -0
  3. package/package.json +51 -0
  4. package/template/base/_dot_gitignore +11 -0
  5. package/template/base/eslint.config.mjs +7 -0
  6. package/template/base/next.config.ts +17 -0
  7. package/template/base/package.json +35 -0
  8. package/template/base/postcss.config.mjs +7 -0
  9. package/template/base/scripts/setup.mjs +146 -0
  10. package/template/base/src/app/globals.css +68 -0
  11. package/template/base/src/app/layout.tsx +19 -0
  12. package/template/base/src/app/page.tsx +37 -0
  13. package/template/base/src/components/ui/badge.tsx +29 -0
  14. package/template/base/src/components/ui/button.tsx +49 -0
  15. package/template/base/src/components/ui/card.tsx +54 -0
  16. package/template/base/src/components/ui/input.tsx +19 -0
  17. package/template/base/src/components/ui/label.tsx +19 -0
  18. package/template/base/src/components/ui/separator.tsx +25 -0
  19. package/template/base/src/components/ui/skeleton.tsx +7 -0
  20. package/template/base/src/lib/utils.ts +17 -0
  21. package/template/base/tsconfig.json +21 -0
  22. package/template/extras/auth-better-auth/src/app/(auth)/layout.tsx +7 -0
  23. package/template/extras/auth-better-auth/src/app/(auth)/sign-in/page.tsx +65 -0
  24. package/template/extras/auth-better-auth/src/app/(auth)/sign-up/page.tsx +70 -0
  25. package/template/extras/auth-better-auth/src/app/(dashboard)/admin/page.tsx +68 -0
  26. package/template/extras/auth-better-auth/src/app/(dashboard)/dashboard/page.tsx +42 -0
  27. package/template/extras/auth-better-auth/src/app/(dashboard)/layout.tsx +41 -0
  28. package/template/extras/auth-better-auth/src/app/api/auth/[...all]/route.ts +4 -0
  29. package/template/extras/auth-better-auth/src/components/sign-out-button.tsx +19 -0
  30. package/template/extras/auth-better-auth/src/lib/auth-client.ts +7 -0
  31. package/template/extras/auth-better-auth/src/lib/auth-server.ts +51 -0
  32. package/template/extras/auth-better-auth/src/lib/auth.ts +76 -0
  33. package/template/extras/auth-better-auth/src/middleware.ts +25 -0
  34. package/template/extras/auth-clerk/src/app/(auth)/layout.tsx +7 -0
  35. package/template/extras/auth-clerk/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +5 -0
  36. package/template/extras/auth-clerk/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +5 -0
  37. package/template/extras/auth-clerk/src/app/(dashboard)/admin/page.tsx +37 -0
  38. package/template/extras/auth-clerk/src/app/(dashboard)/dashboard/page.tsx +42 -0
  39. package/template/extras/auth-clerk/src/app/(dashboard)/layout.tsx +57 -0
  40. package/template/extras/auth-clerk/src/lib/auth.ts +55 -0
  41. package/template/extras/auth-clerk/src/middleware.ts +17 -0
  42. package/template/extras/calendar-dispatch/_shared/src/app/(dashboard)/calendar/page.tsx +39 -0
  43. package/template/extras/calendar-dispatch/drizzle/src/app/(dashboard)/calendar/page.tsx +21 -0
  44. package/template/extras/calendar-dispatch/drizzle/src/components/calendar/calendar-board.tsx +195 -0
  45. package/template/extras/calendar-dispatch/drizzle/src/lib/calendar/actions.ts +35 -0
  46. package/template/extras/calendar-dispatch/drizzle/src/lib/calendar/data.ts +74 -0
  47. package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/[id]/page.tsx +48 -0
  48. package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/new/page.tsx +15 -0
  49. package/template/extras/checklists/_shared/src/app/(dashboard)/checklists/page.tsx +83 -0
  50. package/template/extras/checklists/_shared/src/components/jobs/job-checklists-section.tsx +18 -0
  51. package/template/extras/checklists/_shared/src/lib/checklists/data.ts +17 -0
  52. package/template/extras/checklists/_shared/src/lib/checklists/sample-data.ts +56 -0
  53. package/template/extras/checklists/_shared/src/lib/checklists/types.ts +47 -0
  54. package/template/extras/checklists/drizzle/src/app/(dashboard)/checklists/new/page.tsx +10 -0
  55. package/template/extras/checklists/drizzle/src/components/checklists/new-template-form.tsx +158 -0
  56. package/template/extras/checklists/drizzle/src/components/jobs/job-checklists-client.tsx +202 -0
  57. package/template/extras/checklists/drizzle/src/components/jobs/job-checklists-section.tsx +24 -0
  58. package/template/extras/checklists/drizzle/src/db/schema/checklists.ts +52 -0
  59. package/template/extras/checklists/drizzle/src/lib/checklists/actions.ts +112 -0
  60. package/template/extras/checklists/drizzle/src/lib/checklists/data.ts +80 -0
  61. package/template/extras/comms-email/src/app/(dashboard)/email/page.tsx +32 -0
  62. package/template/extras/comms-sms/_shared/src/app/(dashboard)/sms/new/page.tsx +35 -0
  63. package/template/extras/comms-sms/_shared/src/app/(dashboard)/sms/page.tsx +55 -0
  64. package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/[customerId]/page.tsx +102 -0
  65. package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/new/page.tsx +120 -0
  66. package/template/extras/comms-sms/drizzle/src/app/(dashboard)/sms/page.tsx +70 -0
  67. package/template/extras/comms-sms/drizzle/src/app/api/twilio/sms/route.ts +69 -0
  68. package/template/extras/comms-sms/drizzle/src/db/schema/sms-messages.ts +27 -0
  69. package/template/extras/comms-sms/drizzle/src/lib/sms/actions.ts +107 -0
  70. package/template/extras/comms-sms/drizzle/src/lib/sms/data.ts +111 -0
  71. package/template/extras/customer-portal/_shared/src/app/portal/[token]/layout.tsx +51 -0
  72. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/appointments/page.tsx +78 -0
  73. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/estimates/page.tsx +94 -0
  74. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/invoices/page.tsx +71 -0
  75. package/template/extras/customer-portal/drizzle/src/app/portal/[token]/page.tsx +118 -0
  76. package/template/extras/customer-portal/drizzle/src/components/portal/estimate-approval.tsx +141 -0
  77. package/template/extras/customer-portal/drizzle/src/components/portal/signature-pad.tsx +126 -0
  78. package/template/extras/customer-portal/drizzle/src/lib/portal/actions.ts +107 -0
  79. package/template/extras/customer-portal/drizzle/src/lib/portal/data.ts +158 -0
  80. package/template/extras/customers/_fragments/convex.txt +28 -0
  81. package/template/extras/customers/_shared/src/app/(dashboard)/customers/[id]/page.tsx +81 -0
  82. package/template/extras/customers/_shared/src/app/(dashboard)/customers/new/page.tsx +16 -0
  83. package/template/extras/customers/_shared/src/app/(dashboard)/customers/page.tsx +73 -0
  84. package/template/extras/customers/_shared/src/lib/customers/data.ts +15 -0
  85. package/template/extras/customers/_shared/src/lib/customers/sample-data.ts +67 -0
  86. package/template/extras/customers/_shared/src/lib/customers/types.ts +31 -0
  87. package/template/extras/customers/convex/convex/customers.ts +52 -0
  88. package/template/extras/customers/convex/src/lib/customers/data.ts +64 -0
  89. package/template/extras/customers/drizzle/src/app/(dashboard)/customers/new/page.tsx +82 -0
  90. package/template/extras/customers/drizzle/src/db/schema/customers.ts +34 -0
  91. package/template/extras/customers/drizzle/src/lib/customers/actions.ts +67 -0
  92. package/template/extras/customers/drizzle/src/lib/customers/data.ts +34 -0
  93. package/template/extras/db-convex/convex/_generated/README.md +13 -0
  94. package/template/extras/db-convex/convex/_generated/api.d.ts +8 -0
  95. package/template/extras/db-convex/convex/_generated/api.js +12 -0
  96. package/template/extras/db-convex/convex/_generated/dataModel.d.ts +9 -0
  97. package/template/extras/db-convex/convex/_generated/server.d.ts +18 -0
  98. package/template/extras/db-convex/convex/_generated/server.js +12 -0
  99. package/template/extras/db-convex/convex/auth.config.ts +17 -0
  100. package/template/extras/db-convex/convex/schema.ts +28 -0
  101. package/template/extras/db-convex/src/app/layout.tsx +20 -0
  102. package/template/extras/db-convex/src/components/providers.tsx +28 -0
  103. package/template/extras/db-convex/src/lib/convex.ts +6 -0
  104. package/template/extras/db-drizzle-pg/docker-compose.yml +21 -0
  105. package/template/extras/db-drizzle-pg/drizzle.config.ts +24 -0
  106. package/template/extras/db-drizzle-pg/src/app/layout.tsx +20 -0
  107. package/template/extras/db-drizzle-pg/src/components/providers.tsx +16 -0
  108. package/template/extras/db-drizzle-pg/src/db/client.ts +14 -0
  109. package/template/extras/db-drizzle-pg/src/db/schema/auth.ts +62 -0
  110. package/template/extras/db-drizzle-pg/src/db/schema/index.ts +3 -0
  111. package/template/extras/emergency-dispatch/src/app/(dashboard)/emergency/page.tsx +43 -0
  112. package/template/extras/equipment-tracking/src/app/(dashboard)/equipment/page.tsx +61 -0
  113. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/[id]/page.tsx +123 -0
  114. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/new/page.tsx +22 -0
  115. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/estimates/page.tsx +102 -0
  116. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/invoices/[id]/page.tsx +168 -0
  117. package/template/extras/estimates-invoices/_shared/src/app/(dashboard)/invoices/page.tsx +100 -0
  118. package/template/extras/estimates-invoices/_shared/src/components/estimates/convert-to-job-button.tsx +14 -0
  119. package/template/extras/estimates-invoices/_shared/src/components/invoices/pay-invoice-button.tsx +15 -0
  120. package/template/extras/estimates-invoices/_shared/src/components/invoices/send-invoice-email-button.tsx +14 -0
  121. package/template/extras/estimates-invoices/_shared/src/lib/estimates/data.ts +14 -0
  122. package/template/extras/estimates-invoices/_shared/src/lib/estimates/sample-data.ts +74 -0
  123. package/template/extras/estimates-invoices/_shared/src/lib/estimates/types.ts +60 -0
  124. package/template/extras/estimates-invoices/_shared/src/lib/invoices/data.ts +14 -0
  125. package/template/extras/estimates-invoices/_shared/src/lib/invoices/sample-data.ts +83 -0
  126. package/template/extras/estimates-invoices/_shared/src/lib/invoices/types.ts +78 -0
  127. package/template/extras/estimates-invoices/drizzle/src/app/(dashboard)/estimates/new/page.tsx +18 -0
  128. package/template/extras/estimates-invoices/drizzle/src/app/api/stripe/webhook/route.ts +87 -0
  129. package/template/extras/estimates-invoices/drizzle/src/app/i/[token]/page.tsx +148 -0
  130. package/template/extras/estimates-invoices/drizzle/src/components/estimates/convert-to-job-button.tsx +18 -0
  131. package/template/extras/estimates-invoices/drizzle/src/components/estimates/new-estimate-form.tsx +261 -0
  132. package/template/extras/estimates-invoices/drizzle/src/components/invoices/pay-invoice-button.tsx +19 -0
  133. package/template/extras/estimates-invoices/drizzle/src/components/invoices/public-pay-button.tsx +20 -0
  134. package/template/extras/estimates-invoices/drizzle/src/components/invoices/send-invoice-email-button.tsx +37 -0
  135. package/template/extras/estimates-invoices/drizzle/src/components/jobs/generate-invoice-button.tsx +23 -0
  136. package/template/extras/estimates-invoices/drizzle/src/db/schema/estimates.ts +41 -0
  137. package/template/extras/estimates-invoices/drizzle/src/db/schema/invoices.ts +59 -0
  138. package/template/extras/estimates-invoices/drizzle/src/lib/estimates/actions.ts +110 -0
  139. package/template/extras/estimates-invoices/drizzle/src/lib/estimates/data.ts +57 -0
  140. package/template/extras/estimates-invoices/drizzle/src/lib/invoices/actions.ts +199 -0
  141. package/template/extras/estimates-invoices/drizzle/src/lib/invoices/data.ts +99 -0
  142. package/template/extras/estimates-invoices/drizzle/src/lib/invoices/email-actions.ts +102 -0
  143. package/template/extras/inspection-checklists/src/app/(dashboard)/inspections/page.tsx +60 -0
  144. package/template/extras/jobs/_fragments/convex.txt +21 -0
  145. package/template/extras/jobs/_shared/src/app/(dashboard)/jobs/[id]/page.tsx +102 -0
  146. package/template/extras/jobs/_shared/src/app/(dashboard)/jobs/page.tsx +72 -0
  147. package/template/extras/jobs/_shared/src/components/jobs/advance-status-button.tsx +21 -0
  148. package/template/extras/jobs/_shared/src/components/jobs/generate-invoice-button.tsx +15 -0
  149. package/template/extras/jobs/_shared/src/components/jobs/photo-gallery.tsx +17 -0
  150. package/template/extras/jobs/_shared/src/components/jobs/photos-section.tsx +18 -0
  151. package/template/extras/jobs/_shared/src/lib/jobs/data.ts +14 -0
  152. package/template/extras/jobs/_shared/src/lib/jobs/sample-data.ts +50 -0
  153. package/template/extras/jobs/_shared/src/lib/jobs/types.ts +62 -0
  154. package/template/extras/jobs/convex/convex/jobs.ts +46 -0
  155. package/template/extras/jobs/convex/src/lib/jobs/data.ts +65 -0
  156. package/template/extras/jobs/drizzle/src/app/(dashboard)/jobs/new/page.tsx +18 -0
  157. package/template/extras/jobs/drizzle/src/components/jobs/advance-status-button.tsx +34 -0
  158. package/template/extras/jobs/drizzle/src/components/jobs/new-job-form.tsx +275 -0
  159. package/template/extras/jobs/drizzle/src/components/jobs/photo-gallery.tsx +130 -0
  160. package/template/extras/jobs/drizzle/src/components/jobs/photos-section.tsx +7 -0
  161. package/template/extras/jobs/drizzle/src/db/schema/job-attachments.ts +26 -0
  162. package/template/extras/jobs/drizzle/src/db/schema/jobs.ts +29 -0
  163. package/template/extras/jobs/drizzle/src/lib/jobs/actions.ts +71 -0
  164. package/template/extras/jobs/drizzle/src/lib/jobs/data.ts +48 -0
  165. package/template/extras/jobs/drizzle/src/lib/jobs/photos-actions.ts +121 -0
  166. package/template/extras/jobs/drizzle/src/lib/r2.ts +45 -0
  167. package/template/extras/landing-page/_shared/src/app/book/page.tsx +43 -0
  168. package/template/extras/landing-page/_shared/src/app/book/thanks/page.tsx +31 -0
  169. package/template/extras/landing-page/_shared/src/app/page.tsx +81 -0
  170. package/template/extras/landing-page/drizzle/src/app/book/page.tsx +105 -0
  171. package/template/extras/landing-page/drizzle/src/lib/booking/actions.ts +97 -0
  172. package/template/extras/maintenance-plans/src/app/(dashboard)/maintenance-plans/page.tsx +72 -0
  173. package/template/extras/mobile/mobile/README.md +67 -0
  174. package/template/extras/mobile/mobile/_dot_env.example +5 -0
  175. package/template/extras/mobile/mobile/_dot_gitignore +26 -0
  176. package/template/extras/mobile/mobile/app/(app)/_layout.tsx +37 -0
  177. package/template/extras/mobile/mobile/app/(app)/estimate.tsx +135 -0
  178. package/template/extras/mobile/mobile/app/(app)/inbox/[customerId].tsx +103 -0
  179. package/template/extras/mobile/mobile/app/(app)/inbox/index.tsx +70 -0
  180. package/template/extras/mobile/mobile/app/(app)/index.tsx +111 -0
  181. package/template/extras/mobile/mobile/app/(app)/job/[id]/checklist.tsx +99 -0
  182. package/template/extras/mobile/mobile/app/(app)/job/[id]/invoice.tsx +143 -0
  183. package/template/extras/mobile/mobile/app/(app)/job/[id].tsx +259 -0
  184. package/template/extras/mobile/mobile/app/_layout.tsx +14 -0
  185. package/template/extras/mobile/mobile/app/index.tsx +23 -0
  186. package/template/extras/mobile/mobile/app/sign-in.tsx +101 -0
  187. package/template/extras/mobile/mobile/app.brand.ts +14 -0
  188. package/template/extras/mobile/mobile/app.config.ts +40 -0
  189. package/template/extras/mobile/mobile/app.features.ts +11 -0
  190. package/template/extras/mobile/mobile/components/SignaturePad.tsx +60 -0
  191. package/template/extras/mobile/mobile/eas.json +22 -0
  192. package/template/extras/mobile/mobile/lib/api.ts +253 -0
  193. package/template/extras/mobile/mobile/lib/auth.ts +51 -0
  194. package/template/extras/mobile/mobile/lib/format.ts +23 -0
  195. package/template/extras/mobile/mobile/lib/push.ts +24 -0
  196. package/template/extras/mobile/mobile/lib/theme.ts +16 -0
  197. package/template/extras/mobile/mobile/package.json +34 -0
  198. package/template/extras/mobile/mobile/tsconfig.json +11 -0
  199. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/customers/[id]/route.ts +18 -0
  200. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/devices/route.ts +40 -0
  201. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/attachments/route.ts +59 -0
  202. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/checklists/item/route.ts +34 -0
  203. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/checklists/route.ts +15 -0
  204. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/route.ts +35 -0
  205. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/status/route.ts +28 -0
  206. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/clock-in/route.ts +35 -0
  207. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/clock-out/route.ts +27 -0
  208. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/[id]/time/route.ts +36 -0
  209. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/jobs/route.ts +30 -0
  210. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/me/route.ts +26 -0
  211. package/template/extras/mobile-api/drizzle/src/app/api/mobile/v1/uploads/sign/route.ts +46 -0
  212. package/template/extras/mobile-api/drizzle/src/db/schema/push-tokens.ts +21 -0
  213. package/template/extras/mobile-api/drizzle/src/db/schema/time-entries.ts +23 -0
  214. package/template/extras/mobile-api/drizzle/src/lib/mobile/cors.ts +30 -0
  215. package/template/extras/mobile-api/drizzle/src/lib/mobile/guard.ts +49 -0
  216. package/template/extras/mobile-api/drizzle/src/lib/push/send.ts +56 -0
  217. package/template/extras/mobile-api/estimates/src/app/api/mobile/v1/estimates/route.ts +52 -0
  218. package/template/extras/mobile-api/estimates/src/app/api/mobile/v1/price-book/route.ts +14 -0
  219. package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/invoices/[id]/payments/route.ts +32 -0
  220. package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/invoices/[id]/route.ts +28 -0
  221. package/template/extras/mobile-api/invoices/src/app/api/mobile/v1/jobs/[id]/invoice/route.ts +82 -0
  222. package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/send/route.ts +32 -0
  223. package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/threads/[customerId]/route.ts +15 -0
  224. package/template/extras/mobile-api/sms/src/app/api/mobile/v1/sms/threads/route.ts +14 -0
  225. package/template/extras/payments-stripe/src/app/(dashboard)/payments/page.tsx +56 -0
  226. package/template/extras/payments-stripe/src/app/api/stripe/webhook/route.ts +56 -0
  227. package/template/extras/payments-stripe/src/components/payments/demo-checkout-button.tsx +18 -0
  228. package/template/extras/payments-stripe/src/lib/payments/actions.ts +63 -0
  229. package/template/extras/payments-stripe/src/lib/stripe.ts +25 -0
  230. package/template/extras/permit-tracking/src/app/(dashboard)/permits/page.tsx +56 -0
  231. package/template/extras/price-book/_shared/src/app/(dashboard)/price-book/new/page.tsx +20 -0
  232. package/template/extras/price-book/_shared/src/app/(dashboard)/price-book/page.tsx +79 -0
  233. package/template/extras/price-book/_shared/src/components/price-book/item-picker.tsx +145 -0
  234. package/template/extras/price-book/_shared/src/lib/price-book/actions.ts +14 -0
  235. package/template/extras/price-book/_shared/src/lib/price-book/data.ts +28 -0
  236. package/template/extras/price-book/_shared/src/lib/price-book/sample-data.ts +62 -0
  237. package/template/extras/price-book/_shared/src/lib/price-book/types.ts +35 -0
  238. package/template/extras/price-book/drizzle/src/app/(dashboard)/price-book/new/page.tsx +18 -0
  239. package/template/extras/price-book/drizzle/src/components/price-book/new-item-form.tsx +254 -0
  240. package/template/extras/price-book/drizzle/src/db/schema/price-book.ts +33 -0
  241. package/template/extras/price-book/drizzle/src/lib/price-book/actions.ts +72 -0
  242. package/template/extras/price-book/drizzle/src/lib/price-book/data.ts +81 -0
  243. package/template/extras/reporting/src/app/(dashboard)/reports/page.tsx +66 -0
  244. package/template/extras/reviews/src/app/(dashboard)/reviews/page.tsx +58 -0
  245. package/template/extras/seed/_shared/scripts/seed.ts +35 -0
  246. package/template/extras/seed/drizzle/scripts/seed.ts +314 -0
  247. package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/[id]/page.tsx +114 -0
  248. package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/new/page.tsx +18 -0
  249. package/template/extras/service-plans/_shared/src/app/(dashboard)/service-plans/page.tsx +92 -0
  250. package/template/extras/service-plans/_shared/src/components/service-plans/subscribe-customer-section.tsx +17 -0
  251. package/template/extras/service-plans/_shared/src/lib/service-plans/data.ts +14 -0
  252. package/template/extras/service-plans/_shared/src/lib/service-plans/sample-data.ts +83 -0
  253. package/template/extras/service-plans/_shared/src/lib/service-plans/types.ts +57 -0
  254. package/template/extras/service-plans/drizzle/src/app/(dashboard)/service-plans/new/page.tsx +10 -0
  255. package/template/extras/service-plans/drizzle/src/app/api/stripe/webhook/route.ts +143 -0
  256. package/template/extras/service-plans/drizzle/src/components/service-plans/new-plan-form.tsx +126 -0
  257. package/template/extras/service-plans/drizzle/src/components/service-plans/subscribe-customer-form.tsx +88 -0
  258. package/template/extras/service-plans/drizzle/src/components/service-plans/subscribe-customer-section.tsx +12 -0
  259. package/template/extras/service-plans/drizzle/src/db/schema/service-plans.ts +46 -0
  260. package/template/extras/service-plans/drizzle/src/lib/service-plans/actions.ts +196 -0
  261. package/template/extras/service-plans/drizzle/src/lib/service-plans/data.ts +124 -0
@@ -0,0 +1,73 @@
1
+ import Link from 'next/link';
2
+ import { Badge } from '@/components/ui/badge';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Card } from '@/components/ui/card';
5
+ import { getCustomers } from '@/lib/customers/data';
6
+ import { formatCurrency } from '@/lib/utils';
7
+
8
+ export default async function CustomersPage() {
9
+ const customers = await getCustomers();
10
+ return (
11
+ <div className="space-y-6">
12
+ <div className="flex items-center justify-between">
13
+ <div>
14
+ <h1 className="text-3xl font-bold tracking-tight">Customers</h1>
15
+ <p className="text-muted-foreground mt-1 text-sm">
16
+ {customers.length} customer{customers.length === 1 ? '' : 's'}
17
+ </p>
18
+ </div>
19
+ <Button asChild>
20
+ <Link href="/customers/new">New customer</Link>
21
+ </Button>
22
+ </div>
23
+ <Card className="overflow-hidden">
24
+ <table className="w-full text-sm">
25
+ <thead className="bg-muted/50 text-muted-foreground text-xs uppercase">
26
+ <tr>
27
+ <th className="px-4 py-3 text-left font-medium">Name</th>
28
+ <th className="px-4 py-3 text-left font-medium">Tags</th>
29
+ <th className="px-4 py-3 text-left font-medium">Contact</th>
30
+ <th className="px-4 py-3 text-right font-medium">LTV</th>
31
+ <th className="px-4 py-3 text-right font-medium">Balance</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody className="divide-border divide-y">
35
+ {customers.map((c) => (
36
+ <tr key={c.id} className="hover:bg-muted/30">
37
+ <td className="px-4 py-3">
38
+ <Link href={`/customers/${c.id}`} className="font-medium hover:underline">
39
+ {c.name}
40
+ </Link>
41
+ </td>
42
+ <td className="px-4 py-3">
43
+ <div className="flex flex-wrap gap-1">
44
+ {c.tags.map((t) => (
45
+ <Badge key={t} variant="secondary" className="text-[10px] uppercase">
46
+ {t}
47
+ </Badge>
48
+ ))}
49
+ </div>
50
+ </td>
51
+ <td className="text-muted-foreground px-4 py-3">
52
+ <div>{c.phones[0]}</div>
53
+ <div className="text-xs">{c.emails[0]}</div>
54
+ </td>
55
+ <td className="px-4 py-3 text-right font-medium">{formatCurrency(c.lifetimeValue)}</td>
56
+ <td className="px-4 py-3 text-right">
57
+ {c.balanceDue > 0 ? (
58
+ <span className="text-destructive font-medium">{formatCurrency(c.balanceDue)}</span>
59
+ ) : (
60
+ <span className="text-muted-foreground">—</span>
61
+ )}
62
+ </td>
63
+ </tr>
64
+ ))}
65
+ </tbody>
66
+ </table>
67
+ </Card>
68
+ <p className="text-muted-foreground text-xs">
69
+ Loaded via <code>src/lib/customers/data.ts</code>. Default is sample data; Drizzle/Convex installers swap in real queries.
70
+ </p>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Default data-source — returns hardcoded sample data so the UI works
3
+ * before any DB is connected. Stack-specific installers (Drizzle/Convex)
4
+ * OVERWRITE this file with real DB queries.
5
+ */
6
+ import { sampleCustomers } from './sample-data';
7
+ import type { Customer } from './types';
8
+
9
+ export async function getCustomers(): Promise<Customer[]> {
10
+ return sampleCustomers;
11
+ }
12
+
13
+ export async function getCustomer(id: string): Promise<Customer | null> {
14
+ return sampleCustomers.find((c) => c.id === id) ?? null;
15
+ }
@@ -0,0 +1,67 @@
1
+ import type { Customer } from './types';
2
+
3
+ // Placeholder data used until the seed module populates the real DB.
4
+ // Replace this import with a server-side query once your DB is wired up.
5
+ export const sampleCustomers: Customer[] = [
6
+ {
7
+ id: 'cus_001',
8
+ name: 'Acme Property Management',
9
+ phones: ['(555) 010-1100'],
10
+ emails: ['ops@acmepm.com'],
11
+ billingAddress: { line1: '120 Main St', city: 'Austin', state: 'TX', postalCode: '78701' },
12
+ serviceAddresses: [
13
+ { line1: '120 Main St', city: 'Austin', state: 'TX', postalCode: '78701' },
14
+ { line1: '845 Oak Ave', city: 'Austin', state: 'TX', postalCode: '78704' },
15
+ ],
16
+ tags: ['commercial', 'recurring'],
17
+ leadSource: 'Referral',
18
+ lifetimeValue: 1287400,
19
+ balanceDue: 0,
20
+ doNotContact: false,
21
+ notes: '6-month maintenance contract.',
22
+ createdAt: '2025-09-12T14:21:00Z',
23
+ },
24
+ {
25
+ id: 'cus_002',
26
+ name: 'Jamie Rodriguez',
27
+ phones: ['(555) 010-2200'],
28
+ emails: ['jamie.r@gmail.com'],
29
+ billingAddress: { line1: '32 Elm Ct', city: 'Round Rock', state: 'TX', postalCode: '78664' },
30
+ serviceAddresses: [{ line1: '32 Elm Ct', city: 'Round Rock', state: 'TX', postalCode: '78664' }],
31
+ tags: ['residential', 'vip'],
32
+ leadSource: 'Google',
33
+ lifetimeValue: 92500,
34
+ balanceDue: 24500,
35
+ doNotContact: false,
36
+ createdAt: '2026-01-08T10:02:00Z',
37
+ },
38
+ {
39
+ id: 'cus_003',
40
+ name: 'Mason Hardware Co.',
41
+ phones: ['(555) 010-3300', '(555) 010-3301'],
42
+ emails: ['accounts@masonhw.com'],
43
+ billingAddress: { line1: '2200 Industrial Blvd', city: 'Pflugerville', state: 'TX', postalCode: '78660' },
44
+ serviceAddresses: [],
45
+ tags: ['commercial'],
46
+ leadSource: 'Trade show',
47
+ lifetimeValue: 458200,
48
+ balanceDue: 152000,
49
+ doNotContact: false,
50
+ createdAt: '2025-11-22T09:30:00Z',
51
+ },
52
+ {
53
+ id: 'cus_004',
54
+ name: 'Priya Patel',
55
+ phones: ['(555) 010-4400'],
56
+ emails: ['priya.patel@example.com'],
57
+ billingAddress: { line1: '78 Cedar Ln', city: 'Cedar Park', state: 'TX', postalCode: '78613' },
58
+ serviceAddresses: [{ line1: '78 Cedar Ln', city: 'Cedar Park', state: 'TX', postalCode: '78613' }],
59
+ tags: ['residential', 'lead'],
60
+ leadSource: 'Yelp',
61
+ lifetimeValue: 0,
62
+ balanceDue: 0,
63
+ doNotContact: false,
64
+ notes: 'Requested estimate for water heater install.',
65
+ createdAt: '2026-05-01T16:11:00Z',
66
+ },
67
+ ];
@@ -0,0 +1,31 @@
1
+ export type CustomerTag = 'vip' | 'recurring' | 'commercial' | 'residential' | 'lead';
2
+
3
+ export interface Address {
4
+ line1: string;
5
+ line2?: string;
6
+ city: string;
7
+ state: string;
8
+ postalCode: string;
9
+ }
10
+
11
+ export interface Customer {
12
+ id: string;
13
+ name: string;
14
+ phones: string[];
15
+ emails: string[];
16
+ billingAddress: Address;
17
+ serviceAddresses: Address[];
18
+ tags: CustomerTag[];
19
+ leadSource?: string;
20
+ lifetimeValue: number;
21
+ balanceDue: number;
22
+ doNotContact: boolean;
23
+ notes?: string;
24
+ createdAt: string; // ISO
25
+ }
26
+
27
+ export function formatAddress(a: Address): string {
28
+ const parts = [a.line1, a.line2, `${a.city}, ${a.state} ${a.postalCode}`].filter(Boolean);
29
+ return parts.join(', ');
30
+ }
31
+
@@ -0,0 +1,52 @@
1
+ import { v } from 'convex/values';
2
+ import { mutation, query } from './_generated/server';
3
+
4
+ /**
5
+ * Convex queries + mutations for customers. After `npx convex dev` runs,
6
+ * these are exposed at `api.customers.list`, `api.customers.getById`,
7
+ * `api.customers.create`.
8
+ */
9
+
10
+ const addressShape = v.object({
11
+ line1: v.string(),
12
+ line2: v.optional(v.string()),
13
+ city: v.string(),
14
+ state: v.string(),
15
+ postalCode: v.string(),
16
+ });
17
+
18
+ export const list = query({
19
+ args: {},
20
+ handler: async (ctx: any) => {
21
+ return await ctx.db.query('customers').order('desc').take(100);
22
+ },
23
+ });
24
+
25
+ export const getById = query({
26
+ args: { id: v.id('customers') },
27
+ handler: async (ctx: any, args: { id: any }) => {
28
+ return await ctx.db.get(args.id);
29
+ },
30
+ });
31
+
32
+ export const create = mutation({
33
+ args: {
34
+ name: v.string(),
35
+ phones: v.array(v.string()),
36
+ emails: v.array(v.string()),
37
+ billingAddress: addressShape,
38
+ serviceAddresses: v.array(addressShape),
39
+ tags: v.array(v.string()),
40
+ leadSource: v.optional(v.string()),
41
+ notes: v.optional(v.string()),
42
+ },
43
+ handler: async (ctx: any, args: any) => {
44
+ return await ctx.db.insert('customers', {
45
+ ...args,
46
+ lifetimeValue: 0,
47
+ balanceDue: 0,
48
+ doNotContact: false,
49
+ createdAt: Date.now(),
50
+ });
51
+ },
52
+ });
@@ -0,0 +1,64 @@
1
+ import 'server-only';
2
+ import { fetchQuery } from 'convex/nextjs';
3
+ import { api } from '../../../convex/_generated/api';
4
+ import { sampleCustomers } from './sample-data';
5
+ import type { Customer, CustomerTag } from './types';
6
+
7
+ const convexConfigured = (() => {
8
+ const url = process.env.NEXT_PUBLIC_CONVEX_URL;
9
+ return typeof url === 'string' && url.startsWith('http') && !url.includes('placeholder');
10
+ })();
11
+
12
+ interface RawCustomer {
13
+ _id: string;
14
+ name: string;
15
+ phones: string[];
16
+ emails: string[];
17
+ billingAddress: Customer['billingAddress'];
18
+ serviceAddresses: Customer['serviceAddresses'];
19
+ tags: string[];
20
+ leadSource?: string;
21
+ lifetimeValue: number;
22
+ balanceDue: number;
23
+ doNotContact: boolean;
24
+ notes?: string;
25
+ createdAt: number;
26
+ }
27
+
28
+ function toCustomer(row: RawCustomer): Customer {
29
+ return {
30
+ id: row._id,
31
+ name: row.name,
32
+ phones: row.phones,
33
+ emails: row.emails,
34
+ billingAddress: row.billingAddress,
35
+ serviceAddresses: row.serviceAddresses,
36
+ tags: row.tags as CustomerTag[],
37
+ leadSource: row.leadSource,
38
+ lifetimeValue: row.lifetimeValue,
39
+ balanceDue: row.balanceDue,
40
+ doNotContact: row.doNotContact,
41
+ notes: row.notes,
42
+ createdAt: new Date(row.createdAt).toISOString(),
43
+ };
44
+ }
45
+
46
+ export async function getCustomers(): Promise<Customer[]> {
47
+ if (!convexConfigured) return sampleCustomers;
48
+ try {
49
+ const rows = (await fetchQuery(api.customers.list)) as RawCustomer[] | null;
50
+ return (rows ?? []).map(toCustomer);
51
+ } catch {
52
+ return sampleCustomers;
53
+ }
54
+ }
55
+
56
+ export async function getCustomer(id: string): Promise<Customer | null> {
57
+ if (!convexConfigured) return sampleCustomers.find((c) => c.id === id) ?? null;
58
+ try {
59
+ const row = (await fetchQuery(api.customers.getById, { id })) as RawCustomer | null;
60
+ return row ? toCustomer(row) : null;
61
+ } catch {
62
+ return sampleCustomers.find((c) => c.id === id) ?? null;
63
+ }
64
+ }
@@ -0,0 +1,82 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Input } from '@/components/ui/input';
4
+ import { Label } from '@/components/ui/label';
5
+ import { createCustomer } from '@/lib/customers/actions';
6
+
7
+ export default function NewCustomerPage() {
8
+ return (
9
+ <form action={createCustomer} className="mx-auto max-w-2xl space-y-6">
10
+ <h1 className="text-3xl font-bold tracking-tight">New customer</h1>
11
+ <Card>
12
+ <CardHeader>
13
+ <CardTitle>Contact</CardTitle>
14
+ <CardDescription>Required fields are marked with *</CardDescription>
15
+ </CardHeader>
16
+ <CardContent className="space-y-4">
17
+ <div className="space-y-2">
18
+ <Label htmlFor="name">Name *</Label>
19
+ <Input id="name" name="name" required />
20
+ </div>
21
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
22
+ <div className="space-y-2">
23
+ <Label htmlFor="phone">Phone *</Label>
24
+ <Input id="phone" name="phone" type="tel" required />
25
+ </div>
26
+ <div className="space-y-2">
27
+ <Label htmlFor="email">Email</Label>
28
+ <Input id="email" name="email" type="email" />
29
+ </div>
30
+ </div>
31
+ </CardContent>
32
+ </Card>
33
+ <Card>
34
+ <CardHeader>
35
+ <CardTitle>Billing address</CardTitle>
36
+ <CardDescription>Also used as the default service address.</CardDescription>
37
+ </CardHeader>
38
+ <CardContent className="space-y-4">
39
+ <div className="space-y-2">
40
+ <Label htmlFor="line1">Street *</Label>
41
+ <Input id="line1" name="line1" required />
42
+ </div>
43
+ <div className="grid grid-cols-3 gap-3">
44
+ <div className="space-y-2">
45
+ <Label htmlFor="city">City *</Label>
46
+ <Input id="city" name="city" required />
47
+ </div>
48
+ <div className="space-y-2">
49
+ <Label htmlFor="state">State *</Label>
50
+ <Input id="state" name="state" maxLength={2} required />
51
+ </div>
52
+ <div className="space-y-2">
53
+ <Label htmlFor="postalCode">ZIP *</Label>
54
+ <Input id="postalCode" name="postalCode" required />
55
+ </div>
56
+ </div>
57
+ </CardContent>
58
+ </Card>
59
+ <Card>
60
+ <CardHeader><CardTitle>Optional</CardTitle></CardHeader>
61
+ <CardContent className="space-y-4">
62
+ <div className="space-y-2">
63
+ <Label htmlFor="leadSource">Lead source</Label>
64
+ <Input id="leadSource" name="leadSource" placeholder="Google, Referral, Yelp..." />
65
+ </div>
66
+ <div className="space-y-2">
67
+ <Label htmlFor="notes">Notes</Label>
68
+ <textarea
69
+ id="notes"
70
+ name="notes"
71
+ rows={3}
72
+ className="border-input bg-background focus-visible:ring-ring flex w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2"
73
+ />
74
+ </div>
75
+ </CardContent>
76
+ </Card>
77
+ <div className="flex justify-end gap-3">
78
+ <Button type="submit">Create customer</Button>
79
+ </div>
80
+ </form>
81
+ );
82
+ }
@@ -0,0 +1,34 @@
1
+ import { boolean, integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
+
3
+ interface AddressJson {
4
+ line1: string;
5
+ line2?: string;
6
+ city: string;
7
+ state: string;
8
+ postalCode: string;
9
+ }
10
+
11
+ export const customers = pgTable('customers', {
12
+ id: uuid('id').primaryKey().defaultRandom(),
13
+ name: text('name').notNull(),
14
+ phones: jsonb('phones').$type<string[]>().notNull().default([]),
15
+ emails: jsonb('emails').$type<string[]>().notNull().default([]),
16
+ billingAddress: jsonb('billing_address').$type<AddressJson>().notNull(),
17
+ serviceAddresses: jsonb('service_addresses').$type<AddressJson[]>().notNull().default([]),
18
+ tags: jsonb('tags').$type<string[]>().notNull().default([]),
19
+ leadSource: text('lead_source'),
20
+ lifetimeValue: integer('lifetime_value').notNull().default(0), // cents
21
+ balanceDue: integer('balance_due').notNull().default(0), // cents
22
+ doNotContact: boolean('do_not_contact').notNull().default(false),
23
+ notes: text('notes'),
24
+ // Customer-portal magic-link token. Nullable for back-compat with rows
25
+ // seeded before the portal module existed; new rows get one via
26
+ // createCustomer (32 hex chars). The /portal/[token] route uses this
27
+ // as the sole credential — same security model as HCP magic links.
28
+ publicToken: text('public_token').unique(),
29
+ createdAt: timestamp('created_at').notNull().defaultNow(),
30
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
31
+ });
32
+
33
+ export type CustomerRow = typeof customers.$inferSelect;
34
+ export type NewCustomer = typeof customers.$inferInsert;
@@ -0,0 +1,67 @@
1
+ 'use server';
2
+
3
+ import crypto from 'node:crypto';
4
+ import { redirect } from 'next/navigation';
5
+ import { revalidatePath } from 'next/cache';
6
+ import { db } from '@/db/client';
7
+ import { customers } from '@/db/schema';
8
+
9
+ export interface CreateCustomerInput {
10
+ name: string;
11
+ phone: string;
12
+ email: string;
13
+ line1: string;
14
+ city: string;
15
+ state: string;
16
+ postalCode: string;
17
+ leadSource?: string;
18
+ notes?: string;
19
+ }
20
+
21
+ export type CreateCustomerResult =
22
+ | { ok: true; id: string }
23
+ | { ok: false; error: string };
24
+
25
+ /**
26
+ * Server action — creates a customer in Postgres and redirects to the
27
+ * detail page. Used by `app/(dashboard)/customers/new/page.tsx`.
28
+ */
29
+ export async function createCustomer(formData: FormData): Promise<void> {
30
+ const get = (key: string) => (formData.get(key) ?? '').toString().trim();
31
+
32
+ const name = get('name');
33
+ const phone = get('phone');
34
+ const email = get('email');
35
+ const line1 = get('line1');
36
+ const city = get('city');
37
+ const state = get('state');
38
+ const postalCode = get('postalCode');
39
+ const leadSource = get('leadSource') || undefined;
40
+ const notes = get('notes') || undefined;
41
+
42
+ if (!name || !phone || !line1 || !city || !state || !postalCode) {
43
+ throw new Error('Missing required fields');
44
+ }
45
+
46
+ const [row] = await db
47
+ .insert(customers)
48
+ .values({
49
+ name,
50
+ phones: [phone],
51
+ emails: email ? [email] : [],
52
+ billingAddress: { line1, city, state, postalCode },
53
+ serviceAddresses: [{ line1, city, state, postalCode }],
54
+ tags: [],
55
+ leadSource,
56
+ lifetimeValue: 0,
57
+ balanceDue: 0,
58
+ doNotContact: false,
59
+ notes,
60
+ // Magic-link token for /portal/[token] access.
61
+ publicToken: crypto.randomBytes(16).toString('hex'),
62
+ })
63
+ .returning({ id: customers.id });
64
+
65
+ revalidatePath('/customers');
66
+ redirect(`/customers/${row.id}`);
67
+ }
@@ -0,0 +1,34 @@
1
+ import { eq, desc } from 'drizzle-orm';
2
+ import { db } from '@/db/client';
3
+ import { customers as customersTable } from '@/db/schema';
4
+ import type { Customer, CustomerTag } from './types';
5
+
6
+ type Row = typeof customersTable.$inferSelect;
7
+
8
+ function toCustomer(row: Row): Customer {
9
+ return {
10
+ id: row.id,
11
+ name: row.name,
12
+ phones: row.phones,
13
+ emails: row.emails,
14
+ billingAddress: row.billingAddress,
15
+ serviceAddresses: row.serviceAddresses,
16
+ tags: row.tags as CustomerTag[],
17
+ leadSource: row.leadSource ?? undefined,
18
+ lifetimeValue: row.lifetimeValue,
19
+ balanceDue: row.balanceDue,
20
+ doNotContact: row.doNotContact,
21
+ notes: row.notes ?? undefined,
22
+ createdAt: row.createdAt.toISOString(),
23
+ };
24
+ }
25
+
26
+ export async function getCustomers(): Promise<Customer[]> {
27
+ const rows = await db.select().from(customersTable).orderBy(desc(customersTable.createdAt));
28
+ return rows.map(toCustomer);
29
+ }
30
+
31
+ export async function getCustomer(id: string): Promise<Customer | null> {
32
+ const [row] = await db.select().from(customersTable).where(eq(customersTable.id, id)).limit(1);
33
+ return row ? toCustomer(row) : null;
34
+ }
@@ -0,0 +1,13 @@
1
+ # Convex generated files
2
+
3
+ This directory is **auto-managed by Convex**. The files here ship as
4
+ permissive stubs so the project type-checks before you connect a
5
+ deployment. As soon as you run:
6
+
7
+ ```bash
8
+ pnpm convex:dev
9
+ ```
10
+
11
+ Convex regenerates real `.d.ts` + `.js` files derived from
12
+ `convex/schema.ts`. Don't edit these manually — your changes will be
13
+ overwritten on every `convex dev` cycle.
@@ -0,0 +1,8 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Placeholder stub. Replaced by real, schema-derived `api` and `internal`
4
+ * objects when you run `npx convex dev`. Permissive `any` here means user
5
+ * code compiles before the first deployment exists.
6
+ */
7
+ export declare const api: any;
8
+ export declare const internal: any;
@@ -0,0 +1,12 @@
1
+ /* eslint-disable */
2
+ // Placeholder runtime stub. Overwritten by `npx convex dev`.
3
+ // Using a Proxy so any chained access (api.customers.list) returns
4
+ // something callable that resolves to null at runtime — keeps server
5
+ // components from crashing before the deployment exists.
6
+ const makeProxy = () =>
7
+ new Proxy(() => null, {
8
+ get() { return makeProxy(); },
9
+ apply() { return Promise.resolve(null); },
10
+ });
11
+ export const api = makeProxy();
12
+ export const internal = makeProxy();
@@ -0,0 +1,9 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Placeholder stub. Replaced by schema-derived types when you run
4
+ * `npx convex dev`.
5
+ */
6
+ export type Doc<TableName extends string = string> = any;
7
+ export type Id<TableName extends string = string> = string & { __tableName?: TableName };
8
+ export type DataModel = any;
9
+ export type TableNames = string;
@@ -0,0 +1,18 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Placeholder stub. Replaced by real type declarations when you run
4
+ * `npx convex dev` for the first time. These permissive types just make
5
+ * `tsc --noEmit` pass before the deployment exists.
6
+ */
7
+ export declare const query: any;
8
+ export declare const mutation: any;
9
+ export declare const internalQuery: any;
10
+ export declare const internalMutation: any;
11
+ export declare const action: any;
12
+ export declare const internalAction: any;
13
+ export declare const httpAction: any;
14
+ export declare const httpRouter: any;
15
+ export declare const cronJobs: any;
16
+ export type QueryCtx = any;
17
+ export type MutationCtx = any;
18
+ export type ActionCtx = any;
@@ -0,0 +1,12 @@
1
+ /* eslint-disable */
2
+ // Placeholder runtime stub. Overwritten by `npx convex dev`.
3
+ const noop = () => ({ args: undefined, handler: undefined });
4
+ export const query = noop;
5
+ export const mutation = noop;
6
+ export const internalQuery = noop;
7
+ export const internalMutation = noop;
8
+ export const action = noop;
9
+ export const internalAction = noop;
10
+ export const httpAction = noop;
11
+ export const httpRouter = () => ({ route: () => {} });
12
+ export const cronJobs = () => ({ daily: () => {}, weekly: () => {}, monthly: () => {} });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Convex auth config — pulls the JWT issuer URL from your Clerk
3
+ * application. Find it at https://dashboard.clerk.com → JWT Templates →
4
+ * "convex" (create one if it doesn't exist), then set:
5
+ *
6
+ * CLERK_JWT_ISSUER_DOMAIN=https://<your-frontend-api>.clerk.accounts.dev
7
+ *
8
+ * in your Convex deployment (`npx convex env set ...`).
9
+ */
10
+ export default {
11
+ providers: [
12
+ {
13
+ domain: process.env.CLERK_JWT_ISSUER_DOMAIN ?? '',
14
+ applicationID: 'convex',
15
+ },
16
+ ],
17
+ };
@@ -0,0 +1,28 @@
1
+ import { defineSchema, defineTable } from 'convex/server';
2
+ import { v } from 'convex/values';
3
+
4
+ /**
5
+ * Top-level Convex schema. Each feature module adds its own tables
6
+ * via `convex/<module>.ts` files that re-export from this object.
7
+ *
8
+ * For Phase 2 we only define `users` (synced from Clerk on first call).
9
+ * Later phases extend this with customers, jobs, estimates, invoices, etc.
10
+ */
11
+ export default defineSchema({
12
+ users: defineTable({
13
+ clerkId: v.string(),
14
+ email: v.string(),
15
+ firstName: v.optional(v.string()),
16
+ lastName: v.optional(v.string()),
17
+ role: v.union(
18
+ v.literal('admin'),
19
+ v.literal('dispatcher'),
20
+ v.literal('technician'),
21
+ v.literal('csr'),
22
+ v.literal('sales'),
23
+ v.literal('accountant'),
24
+ ),
25
+ createdAt: v.number(),
26
+ }).index('by_clerk_id', ['clerkId']),
27
+ // <crm-starter:tables>
28
+ });