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,168 @@
1
+ import Link from 'next/link';
2
+ import { notFound } from 'next/navigation';
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { getInvoice } from '@/lib/invoices/data';
6
+ import {
7
+ INVOICE_STATUS_LABEL,
8
+ INVOICE_STATUS_VARIANT,
9
+ PAYMENT_METHOD_LABEL,
10
+ } from '@/lib/invoices/types';
11
+ import { PayInvoiceButton } from '@/components/invoices/pay-invoice-button';
12
+ import { SendInvoiceEmailButton } from '@/components/invoices/send-invoice-email-button';
13
+ import { formatCurrency, formatDate } from '@/lib/utils';
14
+
15
+ interface PageProps { params: Promise<{ id: string }>; }
16
+
17
+ export default async function InvoiceDetailPage({ params }: PageProps) {
18
+ const { id } = await params;
19
+ const invoice = await getInvoice(id);
20
+ if (!invoice) notFound();
21
+
22
+ const overdueDays =
23
+ invoice.dueDate && invoice.amountDue > 0 && new Date(invoice.dueDate) < new Date()
24
+ ? Math.floor((Date.now() - new Date(invoice.dueDate).getTime()) / 86_400_000)
25
+ : 0;
26
+
27
+ return (
28
+ <div className="space-y-6">
29
+ <div>
30
+ <Link href="/invoices" className="text-muted-foreground text-sm hover:underline">← Invoices</Link>
31
+ <div className="mt-1 flex items-center gap-3">
32
+ <h1 className="text-3xl font-bold tracking-tight">{invoice.invoiceNumber}</h1>
33
+ <Badge variant={INVOICE_STATUS_VARIANT[invoice.status]}>
34
+ {INVOICE_STATUS_LABEL[invoice.status]}
35
+ </Badge>
36
+ <div className="ml-auto flex items-center gap-2">
37
+ <SendInvoiceEmailButton invoiceId={invoice.id} />
38
+ {invoice.amountDue > 0 && (
39
+ <PayInvoiceButton invoiceId={invoice.id} amountDue={invoice.amountDue} />
40
+ )}
41
+ </div>
42
+ </div>
43
+ <Link href={`/customers/${invoice.customerId}`} className="text-muted-foreground hover:underline">
44
+ {invoice.customerName}
45
+ </Link>
46
+ {invoice.jobId && (
47
+ <span className="text-muted-foreground ml-2 text-sm">
48
+ · from <Link href={`/jobs/${invoice.jobId}`} className="underline">job #{invoice.jobId.slice(-6)}</Link>
49
+ </span>
50
+ )}
51
+ </div>
52
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
53
+ <Card>
54
+ <CardHeader><CardTitle className="text-sm font-medium">Total</CardTitle></CardHeader>
55
+ <CardContent className="text-2xl font-bold">{formatCurrency(invoice.total)}</CardContent>
56
+ </Card>
57
+ <Card>
58
+ <CardHeader><CardTitle className="text-sm font-medium">Paid</CardTitle></CardHeader>
59
+ <CardContent>
60
+ <div className="text-2xl font-bold text-emerald-600">{formatCurrency(invoice.amountPaid)}</div>
61
+ <div className="text-muted-foreground mt-1 text-xs">
62
+ {invoice.total > 0
63
+ ? Math.round((invoice.amountPaid / invoice.total) * 100)
64
+ : 0}% of total
65
+ </div>
66
+ </CardContent>
67
+ </Card>
68
+ <Card>
69
+ <CardHeader><CardTitle className="text-sm font-medium">Due now</CardTitle></CardHeader>
70
+ <CardContent>
71
+ <div className={`text-2xl font-bold ${invoice.amountDue > 0 && invoice.status === 'overdue' ? 'text-destructive' : ''}`}>
72
+ {formatCurrency(invoice.amountDue)}
73
+ </div>
74
+ {overdueDays > 0 && (
75
+ <div className="text-destructive mt-1 text-xs font-bold">
76
+ {overdueDays} day{overdueDays === 1 ? '' : 's'} overdue
77
+ </div>
78
+ )}
79
+ </CardContent>
80
+ </Card>
81
+ <Card>
82
+ <CardHeader><CardTitle className="text-sm font-medium">Due date</CardTitle></CardHeader>
83
+ <CardContent className="text-sm">
84
+ {invoice.dueDate ? (
85
+ <div className="font-medium">{formatDate(invoice.dueDate)}</div>
86
+ ) : (
87
+ <span className="text-muted-foreground">No due date</span>
88
+ )}
89
+ {invoice.sentAt && (
90
+ <div className="text-muted-foreground mt-1 text-xs">Sent {formatDate(invoice.sentAt)}</div>
91
+ )}
92
+ {invoice.paidAt && (
93
+ <div className="text-emerald-600 mt-1 text-xs">Paid {formatDate(invoice.paidAt)}</div>
94
+ )}
95
+ </CardContent>
96
+ </Card>
97
+ </div>
98
+ <Card>
99
+ <CardHeader><CardTitle>Line items</CardTitle></CardHeader>
100
+ <CardContent className="p-0">
101
+ <table className="w-full text-sm">
102
+ <thead className="text-muted-foreground bg-muted/50 text-xs uppercase">
103
+ <tr>
104
+ <th className="px-4 py-3 text-left font-medium">Description</th>
105
+ <th className="px-4 py-3 text-right font-medium">Qty</th>
106
+ <th className="px-4 py-3 text-right font-medium">Unit price</th>
107
+ <th className="px-4 py-3 text-right font-medium">Subtotal</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody className="divide-border divide-y">
111
+ {invoice.lineItems.map((li, i) => (
112
+ <tr key={i}>
113
+ <td className="px-4 py-3">{li.description}</td>
114
+ <td className="px-4 py-3 text-right">{li.qty}</td>
115
+ <td className="px-4 py-3 text-right">{formatCurrency(li.unitPrice)}</td>
116
+ <td className="px-4 py-3 text-right font-medium">{formatCurrency(li.qty * li.unitPrice)}</td>
117
+ </tr>
118
+ ))}
119
+ </tbody>
120
+ <tfoot className="bg-muted/30">
121
+ <tr>
122
+ <td colSpan={3} className="px-4 py-3 text-right text-muted-foreground">Total</td>
123
+ <td className="px-4 py-3 text-right font-bold">{formatCurrency(invoice.total)}</td>
124
+ </tr>
125
+ </tfoot>
126
+ </table>
127
+ </CardContent>
128
+ </Card>
129
+ <Card>
130
+ <CardHeader>
131
+ <CardTitle>Payments ({invoice.payments.length})</CardTitle>
132
+ </CardHeader>
133
+ <CardContent>
134
+ {invoice.payments.length === 0 ? (
135
+ <p className="text-muted-foreground text-sm">No payments yet.</p>
136
+ ) : (
137
+ <table className="w-full text-sm">
138
+ <thead className="text-muted-foreground text-xs uppercase">
139
+ <tr>
140
+ <th className="py-2 text-left font-medium">Date</th>
141
+ <th className="py-2 text-left font-medium">Method</th>
142
+ <th className="py-2 text-left font-medium">Note</th>
143
+ <th className="py-2 text-right font-medium">Amount</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody className="divide-border divide-y">
147
+ {invoice.payments.map((p) => (
148
+ <tr key={p.id}>
149
+ <td className="text-muted-foreground py-2">{formatDate(p.recordedAt)}</td>
150
+ <td className="py-2">{PAYMENT_METHOD_LABEL[p.method]}</td>
151
+ <td className="text-muted-foreground py-2 text-xs">{p.note ?? '—'}</td>
152
+ <td className="py-2 text-right font-medium">{formatCurrency(p.amount)}</td>
153
+ </tr>
154
+ ))}
155
+ </tbody>
156
+ </table>
157
+ )}
158
+ </CardContent>
159
+ </Card>
160
+ {invoice.notes && (
161
+ <Card>
162
+ <CardHeader><CardTitle>Notes</CardTitle></CardHeader>
163
+ <CardContent className="text-sm whitespace-pre-wrap">{invoice.notes}</CardContent>
164
+ </Card>
165
+ )}
166
+ </div>
167
+ );
168
+ }
@@ -0,0 +1,100 @@
1
+ import Link from 'next/link';
2
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { getInvoices } from '@/lib/invoices/data';
5
+ import { INVOICE_STATUS_LABEL, INVOICE_STATUS_VARIANT } from '@/lib/invoices/types';
6
+ import { formatCurrency, formatDate } from '@/lib/utils';
7
+
8
+ export default async function InvoicesPage() {
9
+ const invoices = await getInvoices();
10
+ const ar = invoices
11
+ .filter((i) => i.status !== 'paid' && i.status !== 'void' && i.status !== 'draft')
12
+ .reduce((acc, i) => acc + i.amountDue, 0);
13
+ const overdue = invoices
14
+ .filter((i) => i.status === 'overdue')
15
+ .reduce((acc, i) => acc + i.amountDue, 0);
16
+ const collected = invoices.reduce((acc, i) => acc + i.amountPaid, 0);
17
+
18
+ return (
19
+ <div className="space-y-6">
20
+ <div className="flex items-center justify-between">
21
+ <div>
22
+ <h1 className="text-3xl font-bold tracking-tight">Invoices</h1>
23
+ <p className="text-muted-foreground mt-1 text-sm">{invoices.length} total</p>
24
+ </div>
25
+ </div>
26
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
27
+ <Card>
28
+ <CardHeader><CardTitle className="text-base">Open A/R</CardTitle></CardHeader>
29
+ <CardContent>
30
+ <div className="text-3xl font-bold">{formatCurrency(ar)}</div>
31
+ <p className="text-muted-foreground mt-1 text-xs">Sent + partially-paid + overdue</p>
32
+ </CardContent>
33
+ </Card>
34
+ <Card>
35
+ <CardHeader><CardTitle className="text-base">Overdue</CardTitle></CardHeader>
36
+ <CardContent>
37
+ <div className={`text-3xl font-bold ${overdue > 0 ? 'text-destructive' : ''}`}>
38
+ {formatCurrency(overdue)}
39
+ </div>
40
+ <p className="text-muted-foreground mt-1 text-xs">Past due date</p>
41
+ </CardContent>
42
+ </Card>
43
+ <Card>
44
+ <CardHeader><CardTitle className="text-base">Collected</CardTitle></CardHeader>
45
+ <CardContent>
46
+ <div className="text-3xl font-bold">{formatCurrency(collected)}</div>
47
+ <p className="text-muted-foreground mt-1 text-xs">All-time across all invoices</p>
48
+ </CardContent>
49
+ </Card>
50
+ </div>
51
+ <Card>
52
+ <CardHeader><CardTitle>All invoices</CardTitle></CardHeader>
53
+ <CardContent className="p-0">
54
+ <table className="w-full text-sm">
55
+ <thead className="bg-muted/50 text-muted-foreground text-xs uppercase">
56
+ <tr>
57
+ <th className="px-4 py-3 text-left font-medium">Invoice #</th>
58
+ <th className="px-4 py-3 text-left font-medium">Customer</th>
59
+ <th className="px-4 py-3 text-left font-medium">Status</th>
60
+ <th className="px-4 py-3 text-left font-medium">Due</th>
61
+ <th className="px-4 py-3 text-right font-medium">Total</th>
62
+ <th className="px-4 py-3 text-right font-medium">Due now</th>
63
+ </tr>
64
+ </thead>
65
+ <tbody className="divide-border divide-y">
66
+ {invoices.map((i) => (
67
+ <tr key={i.id} className="hover:bg-muted/30">
68
+ <td className="px-4 py-3 font-mono text-xs">
69
+ <Link href={`/invoices/${i.id}`} className="hover:underline font-medium">
70
+ {i.invoiceNumber}
71
+ </Link>
72
+ </td>
73
+ <td className="px-4 py-3">{i.customerName}</td>
74
+ <td className="px-4 py-3">
75
+ <Badge variant={INVOICE_STATUS_VARIANT[i.status]}>
76
+ {INVOICE_STATUS_LABEL[i.status]}
77
+ </Badge>
78
+ </td>
79
+ <td className="text-muted-foreground px-4 py-3">
80
+ {i.dueDate ? formatDate(i.dueDate) : '—'}
81
+ </td>
82
+ <td className="px-4 py-3 text-right">{formatCurrency(i.total)}</td>
83
+ <td className="px-4 py-3 text-right">
84
+ {i.amountDue > 0 ? (
85
+ <span className={i.status === 'overdue' ? 'text-destructive font-bold' : 'font-medium'}>
86
+ {formatCurrency(i.amountDue)}
87
+ </span>
88
+ ) : (
89
+ <span className="text-muted-foreground">—</span>
90
+ )}
91
+ </td>
92
+ </tr>
93
+ ))}
94
+ </tbody>
95
+ </table>
96
+ </CardContent>
97
+ </Card>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,14 @@
1
+ import { Button } from '@/components/ui/button';
2
+
3
+ /**
4
+ * Default stub for Convex / sample-data. Disabled. Drizzle's installer
5
+ * overwrites this with a working server-action-backed version.
6
+ */
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ export function ConvertToJobButton({ estimateId }: { estimateId: string }) {
9
+ return (
10
+ <Button size="sm" variant="outline" disabled title="Wire to your stack to enable">
11
+ Convert to job
12
+ </Button>
13
+ );
14
+ }
@@ -0,0 +1,15 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { formatCurrency } from '@/lib/utils';
3
+
4
+ /**
5
+ * Default stub for Convex / sample-data. Disabled. Drizzle's installer
6
+ * overwrites this with a Stripe-Checkout-backed version.
7
+ */
8
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
+ export function PayInvoiceButton({ invoiceId, amountDue }: { invoiceId: string; amountDue: number }) {
10
+ return (
11
+ <Button size="sm" variant="outline" disabled title="Wire to your stack to enable">
12
+ Pay {formatCurrency(amountDue)}
13
+ </Button>
14
+ );
15
+ }
@@ -0,0 +1,14 @@
1
+ import { Button } from '@/components/ui/button';
2
+
3
+ /**
4
+ * Default stub for Convex / sample-data. Disabled. Drizzle's installer
5
+ * overwrites this with a Resend-backed working version.
6
+ */
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ export function SendInvoiceEmailButton({ invoiceId }: { invoiceId: string }) {
9
+ return (
10
+ <Button size="sm" variant="outline" disabled title="Wire to your stack to enable">
11
+ Email invoice
12
+ </Button>
13
+ );
14
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Default data source — returns sample estimates. Stack-specific installers
3
+ * (Drizzle / Convex) OVERWRITE this file with real DB queries.
4
+ */
5
+ import { sampleEstimates } from './sample-data';
6
+ import type { Estimate } from './types';
7
+
8
+ export async function getEstimates(): Promise<Estimate[]> {
9
+ return sampleEstimates;
10
+ }
11
+
12
+ export async function getEstimate(id: string): Promise<Estimate | null> {
13
+ return sampleEstimates.find((e) => e.id === id) ?? null;
14
+ }
@@ -0,0 +1,74 @@
1
+ import type { Estimate } from './types';
2
+
3
+ export const sampleEstimates: Estimate[] = [
4
+ {
5
+ id: 'est_001',
6
+ customerId: 'cus_001',
7
+ customerName: 'Acme Property Management',
8
+ status: 'sent',
9
+ lineItems: [
10
+ { description: 'AC condenser replacement (3 ton)', qty: 1, unitPrice: 320000, unitCost: 195000, taxable: true },
11
+ { description: 'Refrigerant (R-410A, 6 lb)', qty: 6, unitPrice: 10750, unitCost: 4500, taxable: true },
12
+ ],
13
+ subtotal: 384500,
14
+ total: 384500,
15
+ totalCost: 222000,
16
+ margin: 162500,
17
+ marginPct: 42,
18
+ sentAt: '2026-05-22T10:00:00Z',
19
+ validUntil: '2026-06-21T23:59:59Z',
20
+ createdAt: '2026-05-22T09:30:00Z',
21
+ },
22
+ {
23
+ id: 'est_002',
24
+ customerId: 'cus_004',
25
+ customerName: 'Priya Patel',
26
+ status: 'draft',
27
+ lineItems: [
28
+ { description: 'Tankless water heater install', qty: 1, unitPrice: 142000, unitCost: 91000, taxable: true },
29
+ ],
30
+ subtotal: 142000,
31
+ total: 142000,
32
+ totalCost: 91000,
33
+ margin: 51000,
34
+ marginPct: 36,
35
+ createdAt: '2026-05-23T14:00:00Z',
36
+ },
37
+ {
38
+ id: 'est_003',
39
+ customerId: 'cus_003',
40
+ customerName: 'Mason Hardware Co.',
41
+ status: 'approved',
42
+ lineItems: [
43
+ { description: 'Walk-in cooler — full install', qty: 1, unitPrice: 950000, unitCost: 612000, taxable: true },
44
+ { description: 'Permit + inspection', qty: 1, unitPrice: 75000, unitCost: 18000, taxable: false },
45
+ { description: 'Annual service plan (yr 1)', qty: 1, unitPrice: 262000, unitCost: 80000, taxable: false },
46
+ ],
47
+ subtotal: 1287000,
48
+ total: 1287000,
49
+ totalCost: 710000,
50
+ margin: 577000,
51
+ marginPct: 45,
52
+ sentAt: '2026-05-15T11:00:00Z',
53
+ approvedAt: '2026-05-20T16:42:00Z',
54
+ validUntil: '2026-06-15T23:59:59Z',
55
+ createdAt: '2026-05-15T10:30:00Z',
56
+ },
57
+ {
58
+ id: 'est_004',
59
+ customerId: 'cus_002',
60
+ customerName: 'Jamie Rodriguez',
61
+ status: 'expired',
62
+ lineItems: [
63
+ { description: 'Capacitor + recharge', qty: 1, unitPrice: 62500, unitCost: 28000, taxable: true },
64
+ ],
65
+ subtotal: 62500,
66
+ total: 62500,
67
+ totalCost: 28000,
68
+ margin: 34500,
69
+ marginPct: 55,
70
+ sentAt: '2026-04-15T09:00:00Z',
71
+ validUntil: '2026-05-15T23:59:59Z',
72
+ createdAt: '2026-04-15T08:30:00Z',
73
+ },
74
+ ];
@@ -0,0 +1,60 @@
1
+ export const ESTIMATE_STATUSES = [
2
+ 'draft',
3
+ 'sent',
4
+ 'viewed',
5
+ 'approved',
6
+ 'declined',
7
+ 'expired',
8
+ 'converted',
9
+ ] as const;
10
+ export type EstimateStatus = (typeof ESTIMATE_STATUSES)[number];
11
+
12
+ export const ESTIMATE_STATUS_LABEL: Record<EstimateStatus, string> = {
13
+ draft: 'Draft',
14
+ sent: 'Sent',
15
+ viewed: 'Viewed',
16
+ approved: 'Approved',
17
+ declined: 'Declined',
18
+ expired: 'Expired',
19
+ converted: 'Converted to job',
20
+ };
21
+
22
+ export const ESTIMATE_STATUS_VARIANT: Record<
23
+ EstimateStatus,
24
+ 'default' | 'secondary' | 'destructive' | 'outline'
25
+ > = {
26
+ draft: 'outline',
27
+ sent: 'secondary',
28
+ viewed: 'secondary',
29
+ approved: 'default',
30
+ declined: 'destructive',
31
+ expired: 'destructive',
32
+ converted: 'default',
33
+ };
34
+
35
+ export interface EstimateLineItem {
36
+ description: string;
37
+ qty: number;
38
+ unitPrice: number; // cents
39
+ unitCost?: number; // cents — used for live margin display
40
+ taxable?: boolean;
41
+ }
42
+
43
+ export interface Estimate {
44
+ id: string;
45
+ customerId: string;
46
+ customerName: string;
47
+ status: EstimateStatus;
48
+ lineItems: EstimateLineItem[];
49
+ subtotal: number; // cents (sum of qty × unitPrice)
50
+ total: number; // cents (subtotal + tax, less discount)
51
+ totalCost: number; // cents (sum of qty × unitCost)
52
+ margin: number; // cents (total - totalCost)
53
+ marginPct: number; // 0-100 (margin / total × 100, 0 if total is 0)
54
+ notes?: string;
55
+ validUntil?: string; // ISO
56
+ sentAt?: string; // ISO
57
+ approvedAt?: string; // ISO
58
+ convertedJobId?: string;
59
+ createdAt: string; // ISO
60
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Default data source — returns sample invoices. Stack-specific installers
3
+ * (Drizzle / Convex) OVERWRITE this file with real DB queries.
4
+ */
5
+ import { sampleInvoices } from './sample-data';
6
+ import type { Invoice } from './types';
7
+
8
+ export async function getInvoices(): Promise<Invoice[]> {
9
+ return sampleInvoices;
10
+ }
11
+
12
+ export async function getInvoice(id: string): Promise<Invoice | null> {
13
+ return sampleInvoices.find((i) => i.id === id) ?? null;
14
+ }
@@ -0,0 +1,83 @@
1
+ import type { Invoice } from './types';
2
+
3
+ export const sampleInvoices: Invoice[] = [
4
+ {
5
+ id: 'inv_001',
6
+ invoiceNumber: 'INV-2026-0001',
7
+ customerId: 'cus_001',
8
+ customerName: 'Acme Property Management',
9
+ status: 'paid',
10
+ lineItems: [
11
+ { description: 'Quarterly HVAC maintenance (4 units)', qty: 4, unitPrice: 12500, taxable: true },
12
+ ],
13
+ subtotal: 50000,
14
+ total: 50000,
15
+ amountPaid: 50000,
16
+ amountDue: 0,
17
+ dueDate: '2026-05-15T23:59:59Z',
18
+ sentAt: '2026-05-01T10:00:00Z',
19
+ paidAt: '2026-05-14T15:23:00Z',
20
+ payments: [
21
+ { id: 'pay_001', amount: 50000, method: 'card', recordedAt: '2026-05-14T15:23:00Z' },
22
+ ],
23
+ createdAt: '2026-05-01T09:00:00Z',
24
+ },
25
+ {
26
+ id: 'inv_002',
27
+ invoiceNumber: 'INV-2026-0002',
28
+ customerId: 'cus_002',
29
+ customerName: 'Jamie Rodriguez',
30
+ status: 'sent',
31
+ lineItems: [
32
+ { description: 'Diagnostic fee', qty: 1, unitPrice: 8900, taxable: true },
33
+ { description: 'Capacitor replacement', qty: 1, unitPrice: 18500, taxable: true },
34
+ ],
35
+ subtotal: 27400,
36
+ total: 27400,
37
+ amountPaid: 0,
38
+ amountDue: 27400,
39
+ dueDate: '2026-06-08T23:59:59Z',
40
+ sentAt: '2026-05-25T16:00:00Z',
41
+ payments: [],
42
+ createdAt: '2026-05-25T15:30:00Z',
43
+ },
44
+ {
45
+ id: 'inv_003',
46
+ invoiceNumber: 'INV-2026-0003',
47
+ customerId: 'cus_003',
48
+ customerName: 'Mason Hardware Co.',
49
+ status: 'overdue',
50
+ lineItems: [
51
+ { description: 'Walk-in cooler service (50% progress invoice)', qty: 1, unitPrice: 152000, taxable: true },
52
+ ],
53
+ subtotal: 152000,
54
+ total: 152000,
55
+ amountPaid: 0,
56
+ amountDue: 152000,
57
+ dueDate: '2026-05-01T23:59:59Z',
58
+ sentAt: '2026-04-15T10:00:00Z',
59
+ payments: [],
60
+ createdAt: '2026-04-15T09:30:00Z',
61
+ },
62
+ {
63
+ id: 'inv_004',
64
+ invoiceNumber: 'INV-2026-0004',
65
+ customerId: 'cus_001',
66
+ customerName: 'Acme Property Management',
67
+ status: 'partial',
68
+ lineItems: [
69
+ { description: 'Walk-in cooler — full install (deposit)', qty: 1, unitPrice: 200000, taxable: true },
70
+ { description: 'Walk-in cooler — labor', qty: 1, unitPrice: 380000, taxable: true },
71
+ ],
72
+ subtotal: 580000,
73
+ total: 580000,
74
+ amountPaid: 200000,
75
+ amountDue: 380000,
76
+ dueDate: '2026-06-15T23:59:59Z',
77
+ sentAt: '2026-05-20T11:00:00Z',
78
+ payments: [
79
+ { id: 'pay_002', amount: 200000, method: 'ach', recordedAt: '2026-05-22T09:15:00Z', note: 'Deposit' },
80
+ ],
81
+ createdAt: '2026-05-20T10:30:00Z',
82
+ },
83
+ ];
@@ -0,0 +1,78 @@
1
+ export const INVOICE_STATUSES = [
2
+ 'draft',
3
+ 'sent',
4
+ 'partial',
5
+ 'paid',
6
+ 'overdue',
7
+ 'void',
8
+ ] as const;
9
+ export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
10
+
11
+ export const INVOICE_STATUS_LABEL: Record<InvoiceStatus, string> = {
12
+ draft: 'Draft',
13
+ sent: 'Sent',
14
+ partial: 'Partially paid',
15
+ paid: 'Paid',
16
+ overdue: 'Overdue',
17
+ void: 'Void',
18
+ };
19
+
20
+ export const INVOICE_STATUS_VARIANT: Record<
21
+ InvoiceStatus,
22
+ 'default' | 'secondary' | 'destructive' | 'outline'
23
+ > = {
24
+ draft: 'outline',
25
+ sent: 'secondary',
26
+ partial: 'secondary',
27
+ paid: 'default',
28
+ overdue: 'destructive',
29
+ void: 'outline',
30
+ };
31
+
32
+ export const PAYMENT_METHODS = ['card', 'ach', 'cash', 'check', 'manual', 'stripe'] as const;
33
+ export type PaymentMethod = (typeof PAYMENT_METHODS)[number];
34
+
35
+ export const PAYMENT_METHOD_LABEL: Record<PaymentMethod, string> = {
36
+ card: 'Card',
37
+ ach: 'ACH',
38
+ cash: 'Cash',
39
+ check: 'Check',
40
+ manual: 'Manual',
41
+ stripe: 'Stripe',
42
+ };
43
+
44
+ export interface InvoiceLineItem {
45
+ description: string;
46
+ qty: number;
47
+ unitPrice: number; // cents
48
+ taxable?: boolean;
49
+ }
50
+
51
+ export interface InvoicePayment {
52
+ id: string;
53
+ amount: number; // cents
54
+ method: PaymentMethod;
55
+ recordedAt: string; // ISO
56
+ note?: string;
57
+ }
58
+
59
+ export interface Invoice {
60
+ id: string;
61
+ invoiceNumber: string;
62
+ customerId: string;
63
+ customerName: string;
64
+ jobId?: string;
65
+ estimateId?: string;
66
+ status: InvoiceStatus;
67
+ lineItems: InvoiceLineItem[];
68
+ subtotal: number;
69
+ total: number;
70
+ amountPaid: number;
71
+ amountDue: number;
72
+ dueDate?: string;
73
+ sentAt?: string;
74
+ paidAt?: string;
75
+ notes?: string;
76
+ payments: InvoicePayment[];
77
+ createdAt: string;
78
+ }
@@ -0,0 +1,18 @@
1
+ import { asc } from 'drizzle-orm';
2
+ import { db } from '@/db/client';
3
+ import { customers } from '@/db/schema';
4
+ import { NewEstimateForm } from '@/components/estimates/new-estimate-form';
5
+
6
+ export default async function NewEstimatePage() {
7
+ const customerRows = await db
8
+ .select({ id: customers.id, name: customers.name })
9
+ .from(customers)
10
+ .orderBy(asc(customers.name));
11
+
12
+ return (
13
+ <div className="mx-auto max-w-3xl space-y-6">
14
+ <h1 className="text-3xl font-bold tracking-tight">New estimate</h1>
15
+ <NewEstimateForm customers={customerRows} />
16
+ </div>
17
+ );
18
+ }