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,65 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Label } from '@/components/ui/label';
10
+ import { signIn } from '@/lib/auth-client';
11
+
12
+ export default function SignInPage() {
13
+ const router = useRouter();
14
+ const [email, setEmail] = useState('');
15
+ const [password, setPassword] = useState('');
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [loading, setLoading] = useState(false);
18
+
19
+ async function onSubmit(e: React.FormEvent) {
20
+ e.preventDefault();
21
+ setError(null);
22
+ setLoading(true);
23
+ const result = await signIn.email({ email, password });
24
+ setLoading(false);
25
+ if (result.error) {
26
+ setError(result.error.message ?? 'Sign-in failed');
27
+ return;
28
+ }
29
+ router.push('/dashboard');
30
+ router.refresh();
31
+ }
32
+
33
+ return (
34
+ <Card>
35
+ <CardHeader>
36
+ <CardTitle>Sign in</CardTitle>
37
+ <CardDescription>Welcome back. Enter your credentials.</CardDescription>
38
+ </CardHeader>
39
+ <form onSubmit={onSubmit}>
40
+ <CardContent className="space-y-4">
41
+ <div className="space-y-2">
42
+ <Label htmlFor="email">Email</Label>
43
+ <Input id="email" type="email" required value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" />
44
+ </div>
45
+ <div className="space-y-2">
46
+ <Label htmlFor="password">Password</Label>
47
+ <Input id="password" type="password" required value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" />
48
+ </div>
49
+ {error && <p className="text-destructive text-sm">{error}</p>}
50
+ </CardContent>
51
+ <CardFooter className="flex flex-col gap-3">
52
+ <Button type="submit" className="w-full" disabled={loading}>
53
+ {loading ? 'Signing in...' : 'Sign in'}
54
+ </Button>
55
+ <p className="text-muted-foreground text-center text-sm">
56
+ No account?{' '}
57
+ <Link href="/sign-up" className="text-brand underline">
58
+ Sign up
59
+ </Link>
60
+ </p>
61
+ </CardFooter>
62
+ </form>
63
+ </Card>
64
+ );
65
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Label } from '@/components/ui/label';
10
+ import { signUp } from '@/lib/auth-client';
11
+
12
+ export default function SignUpPage() {
13
+ const router = useRouter();
14
+ const [name, setName] = useState('');
15
+ const [email, setEmail] = useState('');
16
+ const [password, setPassword] = useState('');
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [loading, setLoading] = useState(false);
19
+
20
+ async function onSubmit(e: React.FormEvent) {
21
+ e.preventDefault();
22
+ setError(null);
23
+ setLoading(true);
24
+ const result = await signUp.email({ email, password, name });
25
+ setLoading(false);
26
+ if (result.error) {
27
+ setError(result.error.message ?? 'Sign-up failed');
28
+ return;
29
+ }
30
+ router.push('/dashboard');
31
+ router.refresh();
32
+ }
33
+
34
+ return (
35
+ <Card>
36
+ <CardHeader>
37
+ <CardTitle>Create your account</CardTitle>
38
+ <CardDescription>Get started with your CRM.</CardDescription>
39
+ </CardHeader>
40
+ <form onSubmit={onSubmit}>
41
+ <CardContent className="space-y-4">
42
+ <div className="space-y-2">
43
+ <Label htmlFor="name">Name</Label>
44
+ <Input id="name" required value={name} onChange={(e) => setName(e.target.value)} autoComplete="name" />
45
+ </div>
46
+ <div className="space-y-2">
47
+ <Label htmlFor="email">Email</Label>
48
+ <Input id="email" type="email" required value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" />
49
+ </div>
50
+ <div className="space-y-2">
51
+ <Label htmlFor="password">Password (min 8 chars)</Label>
52
+ <Input id="password" type="password" required minLength={8} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password" />
53
+ </div>
54
+ {error && <p className="text-destructive text-sm">{error}</p>}
55
+ </CardContent>
56
+ <CardFooter className="flex flex-col gap-3">
57
+ <Button type="submit" className="w-full" disabled={loading}>
58
+ {loading ? 'Creating account...' : 'Create account'}
59
+ </Button>
60
+ <p className="text-muted-foreground text-center text-sm">
61
+ Have an account?{' '}
62
+ <Link href="/sign-in" className="text-brand underline">
63
+ Sign in
64
+ </Link>
65
+ </p>
66
+ </CardFooter>
67
+ </form>
68
+ </Card>
69
+ );
70
+ }
@@ -0,0 +1,68 @@
1
+ import { asc } from 'drizzle-orm';
2
+ import { db } from '@/db/client';
3
+ import { user as userTable } from '@/db/schema';
4
+ import { requireRole } from '@/lib/auth-server';
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { formatDate } from '@/lib/utils';
8
+
9
+ export default async function AdminPage() {
10
+ // Gate: only admins reach this page (requireRole redirects others).
11
+ await requireRole('admin');
12
+
13
+ const members = await db
14
+ .select({
15
+ id: userTable.id,
16
+ name: userTable.name,
17
+ email: userTable.email,
18
+ role: userTable.role,
19
+ createdAt: userTable.createdAt,
20
+ })
21
+ .from(userTable)
22
+ .orderBy(asc(userTable.createdAt));
23
+
24
+ return (
25
+ <div className="space-y-6">
26
+ <div>
27
+ <h1 className="text-3xl font-bold tracking-tight">Admin</h1>
28
+ <p className="text-muted-foreground mt-1 text-sm">
29
+ Team members and their roles. {members.length} user{members.length === 1 ? '' : 's'}.
30
+ </p>
31
+ </div>
32
+ <Card>
33
+ <CardHeader>
34
+ <CardTitle>Team members</CardTitle>
35
+ <CardDescription>
36
+ Roles control access. The first user to sign up (or the owner email set at setup)
37
+ is the admin. To change a role today, update the user&apos;s <code>role</code> column in
38
+ your database; an in-app role editor is a good next addition.
39
+ </CardDescription>
40
+ </CardHeader>
41
+ <CardContent className="p-0">
42
+ <table className="w-full text-sm">
43
+ <thead className="text-muted-foreground bg-muted/50 text-xs uppercase">
44
+ <tr>
45
+ <th className="px-4 py-3 text-left font-medium">Name</th>
46
+ <th className="px-4 py-3 text-left font-medium">Email</th>
47
+ <th className="px-4 py-3 text-left font-medium">Role</th>
48
+ <th className="px-4 py-3 text-left font-medium">Joined</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody className="divide-border divide-y">
52
+ {members.map((m) => (
53
+ <tr key={m.id}>
54
+ <td className="px-4 py-3 font-medium">{m.name}</td>
55
+ <td className="text-muted-foreground px-4 py-3">{m.email}</td>
56
+ <td className="px-4 py-3">
57
+ <Badge variant={m.role === 'admin' ? 'default' : 'secondary'}>{m.role}</Badge>
58
+ </td>
59
+ <td className="text-muted-foreground px-4 py-3">{formatDate(m.createdAt)}</td>
60
+ </tr>
61
+ ))}
62
+ </tbody>
63
+ </table>
64
+ </CardContent>
65
+ </Card>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,42 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
2
+ import { requireUser } from '@/lib/auth-server';
3
+
4
+ export default async function DashboardPage() {
5
+ const user = await requireUser();
6
+ return (
7
+ <div className="space-y-6">
8
+ <div>
9
+ <h1 className="text-3xl font-bold tracking-tight">
10
+ Welcome back{user.name ? `, ${user.name}` : ''}.
11
+ </h1>
12
+ <p className="text-muted-foreground mt-1">
13
+ Your CRM dashboard. Add modules with{' '}
14
+ <code className="bg-muted rounded px-1.5 py-0.5">npx create-crm-starter add &lt;module&gt;</code>.
15
+ </p>
16
+ </div>
17
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
18
+ <Card>
19
+ <CardHeader>
20
+ <CardDescription>Open jobs</CardDescription>
21
+ <CardTitle className="text-4xl">0</CardTitle>
22
+ </CardHeader>
23
+ <CardContent className="text-muted-foreground text-xs">No data yet.</CardContent>
24
+ </Card>
25
+ <Card>
26
+ <CardHeader>
27
+ <CardDescription>Revenue this month</CardDescription>
28
+ <CardTitle className="text-4xl">$0</CardTitle>
29
+ </CardHeader>
30
+ <CardContent className="text-muted-foreground text-xs">No data yet.</CardContent>
31
+ </Card>
32
+ <Card>
33
+ <CardHeader>
34
+ <CardDescription>AR outstanding</CardDescription>
35
+ <CardTitle className="text-4xl">$0</CardTitle>
36
+ </CardHeader>
37
+ <CardContent className="text-muted-foreground text-xs">No data yet.</CardContent>
38
+ </Card>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,41 @@
1
+ import Link from 'next/link';
2
+ import { requireUser } from '@/lib/auth-server';
3
+ import { SignOutButton } from '@/components/sign-out-button';
4
+
5
+ export default async function DashboardLayout({
6
+ children,
7
+ }: {
8
+ children: React.ReactNode;
9
+ }) {
10
+ const user = await requireUser();
11
+ return (
12
+ <div className="grid min-h-screen grid-cols-[14rem_1fr]">
13
+ <aside className="border-border bg-muted/30 border-r p-4">
14
+ <Link href="/dashboard" className="mb-6 block text-lg font-semibold">
15
+ CRM
16
+ </Link>
17
+ <nav className="flex flex-col gap-1 text-sm">
18
+ <Link href="/dashboard" className="hover:bg-accent rounded-md px-3 py-2">Dashboard</Link>
19
+ <Link href="/customers" className="hover:bg-accent rounded-md px-3 py-2">Customers</Link>
20
+ <Link href="/jobs" className="hover:bg-accent rounded-md px-3 py-2">Jobs</Link>
21
+ <Link href="/calendar" className="hover:bg-accent rounded-md px-3 py-2">Calendar</Link>
22
+ <Link href="/estimates" className="hover:bg-accent rounded-md px-3 py-2">Estimates</Link>
23
+ <Link href="/invoices" className="hover:bg-accent rounded-md px-3 py-2">Invoices</Link>
24
+ <Link href="/price-book" className="hover:bg-accent rounded-md px-3 py-2">Price Book</Link>
25
+ {user.role === 'admin' && (
26
+ <Link href="/admin" className="hover:bg-accent rounded-md px-3 py-2">Admin</Link>
27
+ )}
28
+ </nav>
29
+ </aside>
30
+ <div className="flex flex-col">
31
+ <header className="border-border flex h-14 items-center justify-between border-b px-6">
32
+ <div className="text-muted-foreground text-sm">
33
+ Signed in as <span className="font-medium text-foreground">{user.email}</span> ({user.role})
34
+ </div>
35
+ <SignOutButton />
36
+ </header>
37
+ <main className="flex-1 p-6">{children}</main>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,4 @@
1
+ import { toNextJsHandler } from 'better-auth/next-js';
2
+ import { auth } from '@/lib/auth';
3
+
4
+ export const { POST, GET } = toNextJsHandler(auth);
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+
3
+ import { useRouter } from 'next/navigation';
4
+ import { Button } from '@/components/ui/button';
5
+ import { signOut } from '@/lib/auth-client';
6
+
7
+ export function SignOutButton() {
8
+ const router = useRouter();
9
+ async function handle() {
10
+ await signOut();
11
+ router.push('/');
12
+ router.refresh();
13
+ }
14
+ return (
15
+ <Button variant="outline" size="sm" onClick={handle}>
16
+ Sign out
17
+ </Button>
18
+ );
19
+ }
@@ -0,0 +1,7 @@
1
+ import { createAuthClient } from 'better-auth/react';
2
+
3
+ export const authClient = createAuthClient({
4
+ baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL ?? 'http://localhost:3000',
5
+ });
6
+
7
+ export const { signIn, signUp, signOut, useSession } = authClient;
@@ -0,0 +1,51 @@
1
+ import { headers } from 'next/headers';
2
+ import { redirect } from 'next/navigation';
3
+ import { auth } from './auth';
4
+
5
+ export type AppRole = 'admin' | 'dispatcher' | 'technician' | 'csr' | 'sales' | 'accountant';
6
+
7
+ export interface AppUser {
8
+ id: string;
9
+ email: string;
10
+ name: string | null;
11
+ role: AppRole;
12
+ }
13
+
14
+ function normalizeRole(role: string | null | undefined): AppRole {
15
+ switch (role) {
16
+ case 'admin':
17
+ case 'dispatcher':
18
+ case 'technician':
19
+ case 'csr':
20
+ case 'sales':
21
+ case 'accountant':
22
+ return role;
23
+ default:
24
+ return 'csr';
25
+ }
26
+ }
27
+
28
+ export async function getCurrentUser(): Promise<AppUser | null> {
29
+ const session = await auth.api.getSession({ headers: await headers() });
30
+ if (!session?.user) return null;
31
+ const u = session.user as { id: string; email: string; name: string | null; role?: string | null };
32
+ return {
33
+ id: u.id,
34
+ email: u.email,
35
+ name: u.name,
36
+ role: normalizeRole(u.role),
37
+ };
38
+ }
39
+
40
+ export async function requireUser(): Promise<AppUser> {
41
+ const u = await getCurrentUser();
42
+ if (!u) redirect('/sign-in');
43
+ return u;
44
+ }
45
+
46
+ export async function requireRole(role: AppRole | AppRole[]): Promise<AppUser> {
47
+ const u = await requireUser();
48
+ const allowed = Array.isArray(role) ? role : [role];
49
+ if (u.role === 'admin' || allowed.includes(u.role)) return u;
50
+ redirect('/dashboard');
51
+ }
@@ -0,0 +1,76 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
3
+ import { nextCookies } from 'better-auth/next-js';
4
+ import { sql } from 'drizzle-orm';
5
+ import { db } from '@/db/client';
6
+ import * as schema from '@/db/schema';
7
+
8
+ /**
9
+ * Decides the role a newly-created user should get. The business owner must
10
+ * land as 'admin' on first sign-up, otherwise they can't reach admin UI.
11
+ * Two triggers, in order:
12
+ * 1. Their email matches ADMIN_BOOTSTRAP_EMAIL (set at scaffold time from
13
+ * the owner email you entered).
14
+ * 2. They're the very first user in the database (robust fallback).
15
+ * Everyone else defaults to 'csr'.
16
+ */
17
+ async function resolveSignupRole(email: string): Promise<'admin' | 'csr'> {
18
+ const bootstrapEmail = process.env.ADMIN_BOOTSTRAP_EMAIL?.trim().toLowerCase();
19
+ if (bootstrapEmail && email.trim().toLowerCase() === bootstrapEmail) {
20
+ return 'admin';
21
+ }
22
+ try {
23
+ const [{ count }] = await db
24
+ .select({ count: sql<number>`count(*)::int` })
25
+ .from(schema.user);
26
+ if (count === 0) return 'admin';
27
+ } catch {
28
+ /* table may not exist yet during setup — fall through to csr */
29
+ }
30
+ return 'csr';
31
+ }
32
+
33
+ export const auth = betterAuth({
34
+ database: drizzleAdapter(db, {
35
+ provider: 'pg',
36
+ schema: {
37
+ user: schema.user,
38
+ session: schema.session,
39
+ account: schema.account,
40
+ verification: schema.verification,
41
+ },
42
+ }),
43
+ emailAndPassword: {
44
+ enabled: true,
45
+ autoSignIn: true,
46
+ },
47
+ user: {
48
+ additionalFields: {
49
+ role: {
50
+ type: 'string',
51
+ defaultValue: 'csr',
52
+ required: false,
53
+ },
54
+ },
55
+ },
56
+ databaseHooks: {
57
+ user: {
58
+ create: {
59
+ before: async (user) => {
60
+ const role = await resolveSignupRole(user.email);
61
+ return { data: { ...user, role } };
62
+ },
63
+ },
64
+ },
65
+ },
66
+ session: {
67
+ expiresIn: 60 * 60 * 24 * 7, // 7 days
68
+ updateAge: 60 * 60 * 24, // 1 day
69
+ },
70
+ // The mobile-api installer injects `trustedOrigins` + bearer()/expo()
71
+ // plugins here when you scaffold the field-tech iPhone app. nextCookies()
72
+ // must remain LAST in the plugins array.
73
+ plugins: [nextCookies()], // <crm-starter:auth-plugins>
74
+ });
75
+
76
+ export type Session = typeof auth.$Infer.Session;
@@ -0,0 +1,25 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getSessionCookie } from 'better-auth/cookies';
3
+
4
+ const PROTECTED = [/^\/dashboard(\/.*)?$/, /^\/admin(\/.*)?$/, /^\/customers(\/.*)?$/, /^\/jobs(\/.*)?$/, /^\/calendar(\/.*)?$/, /^\/estimates(\/.*)?$/, /^\/invoices(\/.*)?$/];
5
+
6
+ export async function middleware(req: NextRequest) {
7
+ const { pathname } = req.nextUrl;
8
+ const needsAuth = PROTECTED.some((re) => re.test(pathname));
9
+ if (!needsAuth) return NextResponse.next();
10
+
11
+ const session = getSessionCookie(req);
12
+ if (!session) {
13
+ const url = req.nextUrl.clone();
14
+ url.pathname = '/sign-in';
15
+ url.searchParams.set('redirect', pathname);
16
+ return NextResponse.redirect(url);
17
+ }
18
+ return NextResponse.next();
19
+ }
20
+
21
+ export const config = {
22
+ matcher: [
23
+ '/((?!_next|api/auth|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|webmanifest)).*)',
24
+ ],
25
+ };
@@ -0,0 +1,7 @@
1
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
2
+ return (
3
+ <main className="bg-muted/30 flex min-h-screen items-center justify-center p-6">
4
+ {children}
5
+ </main>
6
+ );
7
+ }
@@ -0,0 +1,5 @@
1
+ import { SignIn } from '@clerk/nextjs';
2
+
3
+ export default function SignInPage() {
4
+ return <SignIn />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { SignUp } from '@clerk/nextjs';
2
+
3
+ export default function SignUpPage() {
4
+ return <SignUp />;
5
+ }
@@ -0,0 +1,37 @@
1
+ import { requireRole } from '@/lib/auth';
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
3
+
4
+ export default async function AdminPage() {
5
+ // Gate: only admins reach this page (requireRole redirects others).
6
+ const user = await requireRole('admin');
7
+
8
+ return (
9
+ <div className="space-y-6">
10
+ <div>
11
+ <h1 className="text-3xl font-bold tracking-tight">Admin</h1>
12
+ <p className="text-muted-foreground mt-1 text-sm">Signed in as {user.email}</p>
13
+ </div>
14
+ <Card>
15
+ <CardHeader>
16
+ <CardTitle>Team & roles</CardTitle>
17
+ <CardDescription>
18
+ On the Clerk stack, user roles live in each user&apos;s{' '}
19
+ <code>publicMetadata.role</code>. Manage team members in the Clerk
20
+ dashboard, and set <code>{'{ "role": "admin" }'}</code> (or dispatcher,
21
+ technician, csr, sales, accountant) in their public metadata.
22
+ </CardDescription>
23
+ </CardHeader>
24
+ <CardContent className="text-muted-foreground space-y-2 text-sm">
25
+ <p>
26
+ To make yourself admin: open the Clerk dashboard → Users → your user →
27
+ Metadata → Public → add <code>{'{ "role": "admin" }'}</code>.
28
+ </p>
29
+ <p>
30
+ A full in-app team manager (listing users + editing roles via the Clerk
31
+ API) is a good next addition.
32
+ </p>
33
+ </CardContent>
34
+ </Card>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,42 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
2
+ import { requireUser } from '@/lib/auth';
3
+
4
+ export default async function DashboardPage() {
5
+ const user = await requireUser();
6
+ return (
7
+ <div className="space-y-6">
8
+ <div>
9
+ <h1 className="text-3xl font-bold tracking-tight">
10
+ Welcome back{user.firstName ? `, ${user.firstName}` : ''}.
11
+ </h1>
12
+ <p className="text-muted-foreground mt-1">
13
+ Your CRM dashboard. Add modules with{' '}
14
+ <code className="bg-muted rounded px-1.5 py-0.5">npx create-crm-starter add &lt;module&gt;</code>.
15
+ </p>
16
+ </div>
17
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
18
+ <Card>
19
+ <CardHeader>
20
+ <CardDescription>Open jobs</CardDescription>
21
+ <CardTitle className="text-4xl">0</CardTitle>
22
+ </CardHeader>
23
+ <CardContent className="text-muted-foreground text-xs">No data yet.</CardContent>
24
+ </Card>
25
+ <Card>
26
+ <CardHeader>
27
+ <CardDescription>Revenue this month</CardDescription>
28
+ <CardTitle className="text-4xl">$0</CardTitle>
29
+ </CardHeader>
30
+ <CardContent className="text-muted-foreground text-xs">No data yet.</CardContent>
31
+ </Card>
32
+ <Card>
33
+ <CardHeader>
34
+ <CardDescription>AR outstanding</CardDescription>
35
+ <CardTitle className="text-4xl">$0</CardTitle>
36
+ </CardHeader>
37
+ <CardContent className="text-muted-foreground text-xs">No data yet.</CardContent>
38
+ </Card>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,57 @@
1
+ import Link from 'next/link';
2
+ import { UserButton } from '@clerk/nextjs';
3
+ import { requireUser } from '@/lib/auth';
4
+
5
+ export default async function DashboardLayout({
6
+ children,
7
+ }: {
8
+ children: React.ReactNode;
9
+ }) {
10
+ const user = await requireUser();
11
+ return (
12
+ <div className="grid min-h-screen grid-cols-[14rem_1fr]">
13
+ <aside className="border-border bg-muted/30 border-r p-4">
14
+ <Link href="/dashboard" className="mb-6 block text-lg font-semibold">
15
+ CRM
16
+ </Link>
17
+ <nav className="flex flex-col gap-1 text-sm">
18
+ <Link href="/dashboard" className="hover:bg-accent rounded-md px-3 py-2">
19
+ Dashboard
20
+ </Link>
21
+ <Link href="/customers" className="hover:bg-accent rounded-md px-3 py-2">
22
+ Customers
23
+ </Link>
24
+ <Link href="/jobs" className="hover:bg-accent rounded-md px-3 py-2">
25
+ Jobs
26
+ </Link>
27
+ <Link href="/calendar" className="hover:bg-accent rounded-md px-3 py-2">
28
+ Calendar
29
+ </Link>
30
+ <Link href="/estimates" className="hover:bg-accent rounded-md px-3 py-2">
31
+ Estimates
32
+ </Link>
33
+ <Link href="/invoices" className="hover:bg-accent rounded-md px-3 py-2">
34
+ Invoices
35
+ </Link>
36
+ <Link href="/price-book" className="hover:bg-accent rounded-md px-3 py-2">
37
+ Price Book
38
+ </Link>
39
+ {user.role === 'admin' && (
40
+ <Link href="/admin" className="hover:bg-accent rounded-md px-3 py-2">
41
+ Admin
42
+ </Link>
43
+ )}
44
+ </nav>
45
+ </aside>
46
+ <div className="flex flex-col">
47
+ <header className="border-border flex h-14 items-center justify-between border-b px-6">
48
+ <div className="text-muted-foreground text-sm">
49
+ Signed in as <span className="font-medium text-foreground">{user.email}</span> ({user.role})
50
+ </div>
51
+ <UserButton afterSignOutUrl="/" />
52
+ </header>
53
+ <main className="flex-1 p-6">{children}</main>
54
+ </div>
55
+ </div>
56
+ );
57
+ }