@pylonsync/create-pylon 0.3.343 → 0.3.345

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 (422) hide show
  1. package/bin/create-pylon.js +35 -0
  2. package/package.json +1 -1
  3. package/templates/agency/components/ui/badge.tsx +37 -0
  4. package/templates/agency/components/ui/dialog.tsx +110 -0
  5. package/templates/agency/components/ui/input.tsx +30 -0
  6. package/templates/agency/components/ui/label.tsx +21 -0
  7. package/templates/agency/components/ui/select.tsx +39 -0
  8. package/templates/agency/components/ui/table.tsx +124 -0
  9. package/templates/agency/components/ui/textarea.tsx +24 -0
  10. package/templates/agency/package.json +1 -0
  11. package/templates/ai-chat/components/ui/badge.tsx +37 -0
  12. package/templates/ai-chat/components/ui/button.tsx +56 -0
  13. package/templates/ai-chat/components/ui/card.tsx +90 -0
  14. package/templates/ai-chat/components/ui/dialog.tsx +110 -0
  15. package/templates/ai-chat/components/ui/input.tsx +30 -0
  16. package/templates/ai-chat/components/ui/label.tsx +21 -0
  17. package/templates/ai-chat/components/ui/select.tsx +39 -0
  18. package/templates/ai-chat/components/ui/table.tsx +124 -0
  19. package/templates/ai-chat/components/ui/textarea.tsx +24 -0
  20. package/templates/ai-chat/package.json +1 -0
  21. package/templates/ai-studio/components/ui/badge.tsx +37 -0
  22. package/templates/ai-studio/components/ui/button.tsx +56 -0
  23. package/templates/ai-studio/components/ui/card.tsx +90 -0
  24. package/templates/ai-studio/components/ui/dialog.tsx +110 -0
  25. package/templates/ai-studio/components/ui/input.tsx +30 -0
  26. package/templates/ai-studio/components/ui/label.tsx +21 -0
  27. package/templates/ai-studio/components/ui/select.tsx +39 -0
  28. package/templates/ai-studio/components/ui/table.tsx +124 -0
  29. package/templates/ai-studio/components/ui/textarea.tsx +24 -0
  30. package/templates/ai-studio/package.json +1 -0
  31. package/templates/barebones/components/ui/badge.tsx +37 -0
  32. package/templates/barebones/components/ui/dialog.tsx +110 -0
  33. package/templates/barebones/components/ui/input.tsx +30 -0
  34. package/templates/barebones/components/ui/label.tsx +21 -0
  35. package/templates/barebones/components/ui/select.tsx +39 -0
  36. package/templates/barebones/components/ui/table.tsx +124 -0
  37. package/templates/barebones/components/ui/textarea.tsx +24 -0
  38. package/templates/barebones/package.json +1 -0
  39. package/templates/chat/components/ui/badge.tsx +37 -0
  40. package/templates/chat/components/ui/dialog.tsx +110 -0
  41. package/templates/chat/components/ui/input.tsx +30 -0
  42. package/templates/chat/components/ui/label.tsx +21 -0
  43. package/templates/chat/components/ui/select.tsx +39 -0
  44. package/templates/chat/components/ui/table.tsx +124 -0
  45. package/templates/chat/components/ui/textarea.tsx +24 -0
  46. package/templates/chat/package.json +1 -0
  47. package/templates/consumer/components/ui/badge.tsx +37 -0
  48. package/templates/consumer/components/ui/dialog.tsx +110 -0
  49. package/templates/consumer/components/ui/input.tsx +30 -0
  50. package/templates/consumer/components/ui/label.tsx +21 -0
  51. package/templates/consumer/components/ui/select.tsx +39 -0
  52. package/templates/consumer/components/ui/table.tsx +124 -0
  53. package/templates/consumer/components/ui/textarea.tsx +24 -0
  54. package/templates/consumer/package.json +1 -0
  55. package/templates/creator/components/ui/badge.tsx +37 -0
  56. package/templates/creator/components/ui/dialog.tsx +110 -0
  57. package/templates/creator/components/ui/input.tsx +30 -0
  58. package/templates/creator/components/ui/label.tsx +21 -0
  59. package/templates/creator/components/ui/select.tsx +39 -0
  60. package/templates/creator/components/ui/table.tsx +124 -0
  61. package/templates/creator/components/ui/textarea.tsx +24 -0
  62. package/templates/creator/package.json +1 -0
  63. package/templates/crm/AGENTS.md +116 -0
  64. package/templates/crm/CLAUDE.md +7 -0
  65. package/templates/crm/README.md +96 -0
  66. package/templates/crm/app/companies/companies-view.tsx +129 -0
  67. package/templates/crm/app/companies/page.tsx +17 -0
  68. package/templates/crm/app/contacts/contacts-view.tsx +139 -0
  69. package/templates/crm/app/contacts/page.tsx +17 -0
  70. package/templates/crm/app/deals/[id]/deal-view.tsx +189 -0
  71. package/templates/crm/app/deals/[id]/page.tsx +23 -0
  72. package/templates/crm/app/error.tsx +43 -0
  73. package/templates/crm/app/globals.css +201 -0
  74. package/templates/crm/app/layout.tsx +31 -0
  75. package/templates/crm/app/login/auth-form.tsx +122 -0
  76. package/templates/crm/app/login/page.tsx +38 -0
  77. package/templates/crm/app/not-found.tsx +29 -0
  78. package/templates/crm/app/page.tsx +38 -0
  79. package/templates/crm/app/pipeline-view.tsx +99 -0
  80. package/templates/crm/app/robots.ts +12 -0
  81. package/templates/crm/app/sitemap.ts +27 -0
  82. package/templates/crm/app/workspace.tsx +231 -0
  83. package/templates/crm/app.ts +171 -0
  84. package/templates/crm/bunfig +4 -0
  85. package/templates/crm/components/activity-timeline.tsx +133 -0
  86. package/templates/crm/components/avatar.tsx +40 -0
  87. package/templates/crm/components/board-skeleton.tsx +44 -0
  88. package/templates/crm/components/command-palette.tsx +162 -0
  89. package/templates/crm/components/data-table.tsx +90 -0
  90. package/templates/crm/components/deal-card.tsx +91 -0
  91. package/templates/crm/components/deal-dialog.tsx +169 -0
  92. package/templates/crm/components/empty-state.tsx +43 -0
  93. package/templates/crm/components/kbd.tsx +23 -0
  94. package/templates/crm/components/metrics-bar.tsx +46 -0
  95. package/templates/crm/components/page-header.tsx +36 -0
  96. package/templates/crm/components/pipeline-board.tsx +105 -0
  97. package/templates/crm/components/record-dialog.tsx +126 -0
  98. package/templates/crm/components/sidebar.tsx +112 -0
  99. package/templates/crm/components/stage-badge.tsx +33 -0
  100. package/templates/crm/components/ui/badge.tsx +37 -0
  101. package/templates/crm/components/ui/button.tsx +56 -0
  102. package/templates/crm/components/ui/card.tsx +90 -0
  103. package/templates/crm/components/ui/dialog.tsx +110 -0
  104. package/templates/crm/components/ui/input.tsx +30 -0
  105. package/templates/crm/components/ui/label.tsx +21 -0
  106. package/templates/crm/components/ui/select.tsx +39 -0
  107. package/templates/crm/components/ui/table.tsx +124 -0
  108. package/templates/crm/components/ui/textarea.tsx +24 -0
  109. package/templates/crm/components.json +20 -0
  110. package/templates/crm/functions/createDeal.ts +59 -0
  111. package/templates/crm/functions/logActivity.ts +49 -0
  112. package/templates/crm/functions/moveDeal.ts +53 -0
  113. package/templates/crm/functions/seedWorkspace.ts +63 -0
  114. package/templates/crm/gitignore +10 -0
  115. package/templates/crm/lib/pipeline.ts +221 -0
  116. package/templates/crm/lib/search.ts +78 -0
  117. package/templates/crm/lib/seed.ts +130 -0
  118. package/templates/crm/lib/utils.ts +10 -0
  119. package/templates/crm/package.json +39 -0
  120. package/templates/crm/tests/components.test.tsx +245 -0
  121. package/templates/crm/tests/pipeline.test.ts +219 -0
  122. package/templates/crm/tests/search.test.ts +83 -0
  123. package/templates/crm/tests/setup.ts +5 -0
  124. package/templates/crm/tsconfig.json +33 -0
  125. package/templates/default/components/ui/badge.tsx +37 -0
  126. package/templates/default/components/ui/dialog.tsx +110 -0
  127. package/templates/default/components/ui/input.tsx +30 -0
  128. package/templates/default/components/ui/label.tsx +21 -0
  129. package/templates/default/components/ui/select.tsx +39 -0
  130. package/templates/default/components/ui/table.tsx +124 -0
  131. package/templates/default/components/ui/textarea.tsx +24 -0
  132. package/templates/default/package.json +1 -0
  133. package/templates/directory/components/ui/badge.tsx +37 -0
  134. package/templates/directory/components/ui/dialog.tsx +110 -0
  135. package/templates/directory/components/ui/input.tsx +30 -0
  136. package/templates/directory/components/ui/label.tsx +21 -0
  137. package/templates/directory/components/ui/select.tsx +39 -0
  138. package/templates/directory/components/ui/table.tsx +124 -0
  139. package/templates/directory/components/ui/textarea.tsx +24 -0
  140. package/templates/directory/package.json +1 -0
  141. package/templates/helpdesk/AGENTS.md +116 -0
  142. package/templates/helpdesk/CLAUDE.md +7 -0
  143. package/templates/helpdesk/README.md +90 -0
  144. package/templates/helpdesk/app/customers/customers-view.tsx +140 -0
  145. package/templates/helpdesk/app/customers/page.tsx +17 -0
  146. package/templates/helpdesk/app/error.tsx +43 -0
  147. package/templates/helpdesk/app/globals.css +201 -0
  148. package/templates/helpdesk/app/inbox-view.tsx +147 -0
  149. package/templates/helpdesk/app/layout.tsx +31 -0
  150. package/templates/helpdesk/app/login/auth-form.tsx +122 -0
  151. package/templates/helpdesk/app/login/page.tsx +38 -0
  152. package/templates/helpdesk/app/not-found.tsx +29 -0
  153. package/templates/helpdesk/app/page.tsx +38 -0
  154. package/templates/helpdesk/app/robots.ts +12 -0
  155. package/templates/helpdesk/app/sitemap.ts +27 -0
  156. package/templates/helpdesk/app/tickets/[id]/page.tsx +23 -0
  157. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +164 -0
  158. package/templates/helpdesk/app/workspace.tsx +210 -0
  159. package/templates/helpdesk/app.ts +142 -0
  160. package/templates/helpdesk/bunfig +4 -0
  161. package/templates/helpdesk/components/avatar.tsx +40 -0
  162. package/templates/helpdesk/components/command-palette.tsx +162 -0
  163. package/templates/helpdesk/components/data-table.tsx +90 -0
  164. package/templates/helpdesk/components/empty-state.tsx +43 -0
  165. package/templates/helpdesk/components/kbd.tsx +23 -0
  166. package/templates/helpdesk/components/new-ticket-dialog.tsx +147 -0
  167. package/templates/helpdesk/components/page-header.tsx +36 -0
  168. package/templates/helpdesk/components/priority-badge.tsx +61 -0
  169. package/templates/helpdesk/components/record-dialog.tsx +126 -0
  170. package/templates/helpdesk/components/sidebar.tsx +111 -0
  171. package/templates/helpdesk/components/sla-indicator.tsx +47 -0
  172. package/templates/helpdesk/components/thread.tsx +143 -0
  173. package/templates/helpdesk/components/ticket-list.tsx +78 -0
  174. package/templates/helpdesk/components/ui/badge.tsx +37 -0
  175. package/templates/helpdesk/components/ui/button.tsx +56 -0
  176. package/templates/helpdesk/components/ui/card.tsx +90 -0
  177. package/templates/helpdesk/components/ui/dialog.tsx +110 -0
  178. package/templates/helpdesk/components/ui/input.tsx +30 -0
  179. package/templates/helpdesk/components/ui/label.tsx +21 -0
  180. package/templates/helpdesk/components/ui/select.tsx +39 -0
  181. package/templates/helpdesk/components/ui/table.tsx +124 -0
  182. package/templates/helpdesk/components/ui/textarea.tsx +24 -0
  183. package/templates/helpdesk/components.json +20 -0
  184. package/templates/helpdesk/functions/createTicket.ts +62 -0
  185. package/templates/helpdesk/functions/replyToTicket.ts +55 -0
  186. package/templates/helpdesk/functions/seedWorkspace.ts +52 -0
  187. package/templates/helpdesk/functions/setTicketState.ts +58 -0
  188. package/templates/helpdesk/gitignore +10 -0
  189. package/templates/helpdesk/lib/format.ts +53 -0
  190. package/templates/helpdesk/lib/search.ts +78 -0
  191. package/templates/helpdesk/lib/seed.ts +185 -0
  192. package/templates/helpdesk/lib/tickets.ts +184 -0
  193. package/templates/helpdesk/lib/utils.ts +10 -0
  194. package/templates/helpdesk/package.json +39 -0
  195. package/templates/helpdesk/tests/components.test.tsx +202 -0
  196. package/templates/helpdesk/tests/search.test.ts +83 -0
  197. package/templates/helpdesk/tests/setup.ts +5 -0
  198. package/templates/helpdesk/tests/tickets.test.ts +215 -0
  199. package/templates/helpdesk/tsconfig.json +33 -0
  200. package/templates/inventory/AGENTS.md +116 -0
  201. package/templates/inventory/CLAUDE.md +7 -0
  202. package/templates/inventory/README.md +103 -0
  203. package/templates/inventory/app/error.tsx +43 -0
  204. package/templates/inventory/app/globals.css +201 -0
  205. package/templates/inventory/app/layout.tsx +31 -0
  206. package/templates/inventory/app/login/auth-form.tsx +122 -0
  207. package/templates/inventory/app/login/page.tsx +38 -0
  208. package/templates/inventory/app/movements/movements-view.tsx +103 -0
  209. package/templates/inventory/app/movements/page.tsx +17 -0
  210. package/templates/inventory/app/not-found.tsx +29 -0
  211. package/templates/inventory/app/page.tsx +34 -0
  212. package/templates/inventory/app/products/[id]/page.tsx +23 -0
  213. package/templates/inventory/app/products/[id]/product-view.tsx +178 -0
  214. package/templates/inventory/app/products-view.tsx +213 -0
  215. package/templates/inventory/app/robots.ts +12 -0
  216. package/templates/inventory/app/sitemap.ts +27 -0
  217. package/templates/inventory/app/workspace.tsx +172 -0
  218. package/templates/inventory/app.ts +154 -0
  219. package/templates/inventory/bunfig +4 -0
  220. package/templates/inventory/components/avatar.tsx +40 -0
  221. package/templates/inventory/components/command-palette.tsx +162 -0
  222. package/templates/inventory/components/data-table.tsx +90 -0
  223. package/templates/inventory/components/empty-state.tsx +43 -0
  224. package/templates/inventory/components/kbd.tsx +23 -0
  225. package/templates/inventory/components/movement-dialog.tsx +182 -0
  226. package/templates/inventory/components/page-header.tsx +36 -0
  227. package/templates/inventory/components/record-dialog.tsx +126 -0
  228. package/templates/inventory/components/sidebar.tsx +111 -0
  229. package/templates/inventory/components/stock-level.tsx +47 -0
  230. package/templates/inventory/components/summary-bar.tsx +56 -0
  231. package/templates/inventory/components/ui/badge.tsx +37 -0
  232. package/templates/inventory/components/ui/button.tsx +56 -0
  233. package/templates/inventory/components/ui/card.tsx +90 -0
  234. package/templates/inventory/components/ui/dialog.tsx +110 -0
  235. package/templates/inventory/components/ui/input.tsx +30 -0
  236. package/templates/inventory/components/ui/label.tsx +21 -0
  237. package/templates/inventory/components/ui/select.tsx +39 -0
  238. package/templates/inventory/components/ui/table.tsx +124 -0
  239. package/templates/inventory/components/ui/textarea.tsx +24 -0
  240. package/templates/inventory/components.json +20 -0
  241. package/templates/inventory/functions/recordMovement.ts +72 -0
  242. package/templates/inventory/functions/seedWorkspace.ts +40 -0
  243. package/templates/inventory/gitignore +10 -0
  244. package/templates/inventory/lib/format.ts +53 -0
  245. package/templates/inventory/lib/search.ts +78 -0
  246. package/templates/inventory/lib/seed.ts +102 -0
  247. package/templates/inventory/lib/stock.ts +207 -0
  248. package/templates/inventory/lib/utils.ts +10 -0
  249. package/templates/inventory/package.json +39 -0
  250. package/templates/inventory/tests/components.test.tsx +155 -0
  251. package/templates/inventory/tests/search.test.ts +83 -0
  252. package/templates/inventory/tests/setup.ts +5 -0
  253. package/templates/inventory/tests/stock.test.ts +255 -0
  254. package/templates/inventory/tsconfig.json +33 -0
  255. package/templates/invoices/AGENTS.md +116 -0
  256. package/templates/invoices/CLAUDE.md +7 -0
  257. package/templates/invoices/README.md +99 -0
  258. package/templates/invoices/app/clients/clients-view.tsx +130 -0
  259. package/templates/invoices/app/clients/page.tsx +17 -0
  260. package/templates/invoices/app/error.tsx +43 -0
  261. package/templates/invoices/app/globals.css +201 -0
  262. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +253 -0
  263. package/templates/invoices/app/invoices/[id]/page.tsx +23 -0
  264. package/templates/invoices/app/invoices-view.tsx +183 -0
  265. package/templates/invoices/app/layout.tsx +31 -0
  266. package/templates/invoices/app/login/auth-form.tsx +122 -0
  267. package/templates/invoices/app/login/page.tsx +38 -0
  268. package/templates/invoices/app/not-found.tsx +29 -0
  269. package/templates/invoices/app/page.tsx +36 -0
  270. package/templates/invoices/app/robots.ts +12 -0
  271. package/templates/invoices/app/sitemap.ts +27 -0
  272. package/templates/invoices/app/workspace.tsx +174 -0
  273. package/templates/invoices/app.ts +169 -0
  274. package/templates/invoices/bunfig +4 -0
  275. package/templates/invoices/components/avatar.tsx +40 -0
  276. package/templates/invoices/components/command-palette.tsx +162 -0
  277. package/templates/invoices/components/data-table.tsx +90 -0
  278. package/templates/invoices/components/empty-state.tsx +43 -0
  279. package/templates/invoices/components/kbd.tsx +23 -0
  280. package/templates/invoices/components/line-items.tsx +172 -0
  281. package/templates/invoices/components/page-header.tsx +36 -0
  282. package/templates/invoices/components/payment-dialog.tsx +132 -0
  283. package/templates/invoices/components/record-dialog.tsx +126 -0
  284. package/templates/invoices/components/sidebar.tsx +111 -0
  285. package/templates/invoices/components/status-badge.tsx +35 -0
  286. package/templates/invoices/components/summary-bar.tsx +65 -0
  287. package/templates/invoices/components/totals-panel.tsx +70 -0
  288. package/templates/invoices/components/ui/badge.tsx +37 -0
  289. package/templates/invoices/components/ui/button.tsx +56 -0
  290. package/templates/invoices/components/ui/card.tsx +90 -0
  291. package/templates/invoices/components/ui/dialog.tsx +110 -0
  292. package/templates/invoices/components/ui/input.tsx +30 -0
  293. package/templates/invoices/components/ui/label.tsx +21 -0
  294. package/templates/invoices/components/ui/select.tsx +39 -0
  295. package/templates/invoices/components/ui/table.tsx +124 -0
  296. package/templates/invoices/components/ui/textarea.tsx +24 -0
  297. package/templates/invoices/components.json +20 -0
  298. package/templates/invoices/functions/createInvoice.ts +49 -0
  299. package/templates/invoices/functions/recordPayment.ts +72 -0
  300. package/templates/invoices/functions/seedWorkspace.ts +57 -0
  301. package/templates/invoices/functions/setInvoiceStatus.ts +35 -0
  302. package/templates/invoices/gitignore +10 -0
  303. package/templates/invoices/lib/billing.ts +259 -0
  304. package/templates/invoices/lib/format.ts +53 -0
  305. package/templates/invoices/lib/search.ts +78 -0
  306. package/templates/invoices/lib/seed.ts +190 -0
  307. package/templates/invoices/lib/utils.ts +10 -0
  308. package/templates/invoices/package.json +39 -0
  309. package/templates/invoices/tests/billing.test.ts +283 -0
  310. package/templates/invoices/tests/components.test.tsx +172 -0
  311. package/templates/invoices/tests/search.test.ts +83 -0
  312. package/templates/invoices/tests/setup.ts +5 -0
  313. package/templates/invoices/tsconfig.json +33 -0
  314. package/templates/local-service/components/ui/badge.tsx +37 -0
  315. package/templates/local-service/components/ui/dialog.tsx +110 -0
  316. package/templates/local-service/components/ui/input.tsx +30 -0
  317. package/templates/local-service/components/ui/label.tsx +21 -0
  318. package/templates/local-service/components/ui/select.tsx +39 -0
  319. package/templates/local-service/components/ui/table.tsx +124 -0
  320. package/templates/local-service/components/ui/textarea.tsx +24 -0
  321. package/templates/local-service/package.json +1 -0
  322. package/templates/marketplace/components/ui/badge.tsx +37 -0
  323. package/templates/marketplace/components/ui/button.tsx +56 -0
  324. package/templates/marketplace/components/ui/card.tsx +90 -0
  325. package/templates/marketplace/components/ui/dialog.tsx +110 -0
  326. package/templates/marketplace/components/ui/input.tsx +30 -0
  327. package/templates/marketplace/components/ui/label.tsx +21 -0
  328. package/templates/marketplace/components/ui/select.tsx +39 -0
  329. package/templates/marketplace/components/ui/table.tsx +124 -0
  330. package/templates/marketplace/components/ui/textarea.tsx +24 -0
  331. package/templates/marketplace/lib/utils.ts +10 -0
  332. package/templates/marketplace/package.json +1 -0
  333. package/templates/projects/AGENTS.md +116 -0
  334. package/templates/projects/CLAUDE.md +7 -0
  335. package/templates/projects/README.md +96 -0
  336. package/templates/projects/app/clients/clients-view.tsx +126 -0
  337. package/templates/projects/app/clients/page.tsx +17 -0
  338. package/templates/projects/app/error.tsx +43 -0
  339. package/templates/projects/app/globals.css +201 -0
  340. package/templates/projects/app/layout.tsx +31 -0
  341. package/templates/projects/app/login/auth-form.tsx +122 -0
  342. package/templates/projects/app/login/page.tsx +38 -0
  343. package/templates/projects/app/not-found.tsx +29 -0
  344. package/templates/projects/app/page.tsx +30 -0
  345. package/templates/projects/app/projects/[id]/page.tsx +23 -0
  346. package/templates/projects/app/projects/[id]/project-view.tsx +179 -0
  347. package/templates/projects/app/projects-view.tsx +181 -0
  348. package/templates/projects/app/robots.ts +12 -0
  349. package/templates/projects/app/sitemap.ts +27 -0
  350. package/templates/projects/app/workspace.tsx +190 -0
  351. package/templates/projects/app.ts +186 -0
  352. package/templates/projects/bunfig +4 -0
  353. package/templates/projects/components/avatar.tsx +40 -0
  354. package/templates/projects/components/board-skeleton.tsx +37 -0
  355. package/templates/projects/components/budget-bar.tsx +61 -0
  356. package/templates/projects/components/command-palette.tsx +162 -0
  357. package/templates/projects/components/data-table.tsx +90 -0
  358. package/templates/projects/components/empty-state.tsx +43 -0
  359. package/templates/projects/components/kbd.tsx +23 -0
  360. package/templates/projects/components/page-header.tsx +36 -0
  361. package/templates/projects/components/record-dialog.tsx +126 -0
  362. package/templates/projects/components/sidebar.tsx +111 -0
  363. package/templates/projects/components/task-board.tsx +109 -0
  364. package/templates/projects/components/task-card.tsx +77 -0
  365. package/templates/projects/components/time-dialog.tsx +114 -0
  366. package/templates/projects/components/ui/badge.tsx +37 -0
  367. package/templates/projects/components/ui/button.tsx +56 -0
  368. package/templates/projects/components/ui/card.tsx +90 -0
  369. package/templates/projects/components/ui/dialog.tsx +110 -0
  370. package/templates/projects/components/ui/input.tsx +30 -0
  371. package/templates/projects/components/ui/label.tsx +21 -0
  372. package/templates/projects/components/ui/select.tsx +39 -0
  373. package/templates/projects/components/ui/table.tsx +124 -0
  374. package/templates/projects/components/ui/textarea.tsx +24 -0
  375. package/templates/projects/components.json +20 -0
  376. package/templates/projects/functions/logTime.ts +49 -0
  377. package/templates/projects/functions/moveTask.ts +40 -0
  378. package/templates/projects/functions/seedWorkspace.ts +59 -0
  379. package/templates/projects/gitignore +10 -0
  380. package/templates/projects/lib/format.ts +53 -0
  381. package/templates/projects/lib/search.ts +78 -0
  382. package/templates/projects/lib/seed.ts +140 -0
  383. package/templates/projects/lib/utils.ts +10 -0
  384. package/templates/projects/lib/work.ts +233 -0
  385. package/templates/projects/package.json +39 -0
  386. package/templates/projects/tests/components.test.tsx +200 -0
  387. package/templates/projects/tests/search.test.ts +83 -0
  388. package/templates/projects/tests/setup.ts +5 -0
  389. package/templates/projects/tests/work.test.ts +238 -0
  390. package/templates/projects/tsconfig.json +33 -0
  391. package/templates/restaurant/components/ui/badge.tsx +37 -0
  392. package/templates/restaurant/components/ui/dialog.tsx +110 -0
  393. package/templates/restaurant/components/ui/input.tsx +30 -0
  394. package/templates/restaurant/components/ui/label.tsx +21 -0
  395. package/templates/restaurant/components/ui/select.tsx +39 -0
  396. package/templates/restaurant/components/ui/table.tsx +124 -0
  397. package/templates/restaurant/components/ui/textarea.tsx +24 -0
  398. package/templates/restaurant/package.json +1 -0
  399. package/templates/shop/components/ui/badge.tsx +37 -0
  400. package/templates/shop/components/ui/dialog.tsx +110 -0
  401. package/templates/shop/components/ui/input.tsx +30 -0
  402. package/templates/shop/components/ui/label.tsx +21 -0
  403. package/templates/shop/components/ui/select.tsx +39 -0
  404. package/templates/shop/components/ui/table.tsx +124 -0
  405. package/templates/shop/components/ui/textarea.tsx +24 -0
  406. package/templates/shop/package.json +1 -0
  407. package/templates/todo/components/ui/badge.tsx +37 -0
  408. package/templates/todo/components/ui/dialog.tsx +110 -0
  409. package/templates/todo/components/ui/input.tsx +30 -0
  410. package/templates/todo/components/ui/label.tsx +21 -0
  411. package/templates/todo/components/ui/select.tsx +39 -0
  412. package/templates/todo/components/ui/table.tsx +124 -0
  413. package/templates/todo/components/ui/textarea.tsx +24 -0
  414. package/templates/todo/package.json +1 -0
  415. package/templates/waitlist/components/ui/badge.tsx +37 -0
  416. package/templates/waitlist/components/ui/dialog.tsx +110 -0
  417. package/templates/waitlist/components/ui/input.tsx +30 -0
  418. package/templates/waitlist/components/ui/label.tsx +21 -0
  419. package/templates/waitlist/components/ui/select.tsx +39 -0
  420. package/templates/waitlist/components/ui/table.tsx +124 -0
  421. package/templates/waitlist/components/ui/textarea.tsx +24 -0
  422. package/templates/waitlist/package.json +1 -0
@@ -0,0 +1,190 @@
1
+ // Demo data for a brand-new billing workspace.
2
+ //
3
+ // An empty invoice list shows you nothing about totals, ageing, or partial
4
+ // payment. The first sign-in seeds a realistic book — including one invoice
5
+ // already overdue and one part-paid — so those states are visible rather than
6
+ // theoretical.
7
+ //
8
+ // Pure data + pure shaping; `functions/seedWorkspace.ts` stays a thin wrapper.
9
+
10
+ export interface SeedClient {
11
+ key: string;
12
+ name: string;
13
+ email: string;
14
+ address: string;
15
+ }
16
+
17
+ export interface SeedLine {
18
+ description: string;
19
+ /** Whole units; shaped into thousandths. */
20
+ quantity: number;
21
+ /** Whole dollars; shaped into cents. */
22
+ unitPrice: number;
23
+ }
24
+
25
+ export interface SeedInvoice {
26
+ key: string;
27
+ client: string;
28
+ number: string;
29
+ status: string;
30
+ taxRateBps: number;
31
+ /** Days from today; negative is in the past. */
32
+ issuedInDays: number;
33
+ dueInDays: number;
34
+ lines: SeedLine[];
35
+ /** Dollars already received; 0 means nothing paid. */
36
+ paid?: number;
37
+ paidInDays?: number;
38
+ }
39
+
40
+ export const SEED_CLIENTS: SeedClient[] = [
41
+ {
42
+ key: "northwind",
43
+ name: "Northwind Logistics",
44
+ email: "ap@northwind.co",
45
+ address: "412 Dock Road\nRotterdam, NL",
46
+ },
47
+ {
48
+ key: "hallmark",
49
+ name: "Hallmark Dental",
50
+ email: "billing@hallmarkdental.com",
51
+ address: "88 Mercer Street\nAustin, TX 78701",
52
+ },
53
+ {
54
+ key: "riverbed",
55
+ name: "Riverbed Coffee",
56
+ email: "tom@riverbed.coffee",
57
+ address: "5 Mill Lane\nPortland, OR 97209",
58
+ },
59
+ {
60
+ key: "quarry",
61
+ name: "Quarry Design Co",
62
+ email: "ben@quarry.design",
63
+ address: "19 Foundry Street\nManchester, UK",
64
+ },
65
+ ];
66
+
67
+ export const SEED_INVOICES: SeedInvoice[] = [
68
+ // Overdue: sent, past due, nothing paid.
69
+ {
70
+ key: "overdue",
71
+ client: "northwind",
72
+ number: "INV-2026-0001",
73
+ status: "sent",
74
+ taxRateBps: 0,
75
+ issuedInDays: -52,
76
+ dueInDays: -22,
77
+ lines: [
78
+ { description: "Dispatch integration — discovery", quantity: 1, unitPrice: 4800 },
79
+ { description: "Senior engineering", quantity: 32, unitPrice: 165 },
80
+ ],
81
+ },
82
+ // Part-paid: a payment against a larger balance.
83
+ {
84
+ key: "partial",
85
+ client: "hallmark",
86
+ number: "INV-2026-0002",
87
+ status: "sent",
88
+ taxRateBps: 875,
89
+ issuedInDays: -18,
90
+ dueInDays: 12,
91
+ lines: [
92
+ { description: "Patient intake portal — build", quantity: 1, unitPrice: 12_400 },
93
+ { description: "Training session", quantity: 2, unitPrice: 450 },
94
+ ],
95
+ paid: 5000,
96
+ paidInDays: -6,
97
+ },
98
+ // Settled.
99
+ {
100
+ key: "paid",
101
+ client: "riverbed",
102
+ number: "INV-2026-0003",
103
+ status: "paid",
104
+ taxRateBps: 0,
105
+ issuedInDays: -40,
106
+ dueInDays: -10,
107
+ lines: [{ description: "Inventory sync — monthly", quantity: 1, unitPrice: 1800 }],
108
+ paid: 1800,
109
+ paidInDays: -12,
110
+ },
111
+ // Still being written.
112
+ {
113
+ key: "draft",
114
+ client: "quarry",
115
+ number: "INV-2026-0004",
116
+ status: "draft",
117
+ taxRateBps: 2000,
118
+ issuedInDays: 0,
119
+ dueInDays: 30,
120
+ lines: [
121
+ { description: "Client portal — phase 1", quantity: 1, unitPrice: 9500 },
122
+ { description: "SSO configuration", quantity: 4, unitPrice: 180 },
123
+ ],
124
+ },
125
+ ];
126
+
127
+ export interface ShapedSeed {
128
+ clients: Array<{ key: string; row: Record<string, unknown> }>;
129
+ invoices: Array<{ key: string; client: string; row: Record<string, unknown> }>;
130
+ lines: Array<{ invoice: string; row: Record<string, unknown> }>;
131
+ payments: Array<{ invoice: string; row: Record<string, unknown> }>;
132
+ }
133
+
134
+ export function shapeSeed(now: number = Date.now()): ShapedSeed {
135
+ const at = (days: number) => new Date(now + days * 86_400_000).toISOString();
136
+
137
+ const lines: ShapedSeed["lines"] = [];
138
+ const payments: ShapedSeed["payments"] = [];
139
+
140
+ for (const invoice of SEED_INVOICES) {
141
+ invoice.lines.forEach((line, index) => {
142
+ lines.push({
143
+ invoice: invoice.key,
144
+ row: {
145
+ description: line.description,
146
+ quantityMilli: Math.round(line.quantity * 1000),
147
+ unitPriceCents: Math.round(line.unitPrice * 100),
148
+ position: index,
149
+ },
150
+ });
151
+ });
152
+ if (invoice.paid) {
153
+ payments.push({
154
+ invoice: invoice.key,
155
+ row: {
156
+ amountCents: Math.round(invoice.paid * 100),
157
+ method: "bank",
158
+ paidAt: at(invoice.paidInDays ?? 0),
159
+ },
160
+ });
161
+ }
162
+ }
163
+
164
+ return {
165
+ clients: SEED_CLIENTS.map((c) => ({
166
+ key: c.key,
167
+ row: {
168
+ name: c.name,
169
+ email: c.email,
170
+ address: c.address,
171
+ createdAt: at(-90),
172
+ },
173
+ })),
174
+ invoices: SEED_INVOICES.map((i) => ({
175
+ key: i.key,
176
+ client: i.client,
177
+ row: {
178
+ number: i.number,
179
+ status: i.status,
180
+ taxRateBps: i.taxRateBps,
181
+ issueDate: at(i.issuedInDays),
182
+ dueDate: at(i.dueInDays),
183
+ createdAt: at(i.issuedInDays),
184
+ updatedAt: at(i.paidInDays ?? i.issuedInDays),
185
+ },
186
+ })),
187
+ lines,
188
+ payments,
189
+ };
190
+ }
@@ -0,0 +1,10 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // `cn` — the shadcn class merger. clsx resolves conditional/array class
5
+ // inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
6
+ // the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
7
+ // component routes its className through this.
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "__APP_NAME_KEBAB__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev",
8
+ "deploy": "pylon deploy",
9
+ "check": "tsc --noEmit",
10
+ "test": "pylon test"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/react": "^__PYLON_VERSION__",
14
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
15
+ "@pylonsync/functions": "^__PYLON_VERSION__",
16
+ "@pylonsync/client": "^__PYLON_VERSION__",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0",
19
+ "tailwindcss": "^4.3.0",
20
+ "@tailwindcss/cli": "^4.3.0",
21
+ "tw-animate-css": "^1.2.0",
22
+ "class-variance-authority": "^0.7.1",
23
+ "clsx": "^2.1.1",
24
+ "tailwind-merge": "^2.5.0",
25
+ "lucide-react": "^0.460.0",
26
+ "@radix-ui/react-dialog": "^1.1.4",
27
+ "@radix-ui/react-slot": "^1.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@happy-dom/global-registrator": "^20.10.0",
31
+ "@pylonsync/cli": "^__PYLON_VERSION__",
32
+ "@testing-library/dom": "^10.4.0",
33
+ "@testing-library/react": "^16.3.0",
34
+ "@types/node": "^22.0.0",
35
+ "@types/react": "^19.0.0",
36
+ "@types/react-dom": "^19.0.0",
37
+ "typescript": "^5.6.0"
38
+ }
39
+ }
@@ -0,0 +1,283 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ daysOverdue,
4
+ displayStatus,
5
+ isOverdue,
6
+ isValidStatus,
7
+ lineTotalCents,
8
+ money,
9
+ nextNumber,
10
+ parseAmount,
11
+ parseQuantity,
12
+ quantity,
13
+ summarize,
14
+ totals,
15
+ type Invoice,
16
+ type LineItem,
17
+ type Payment,
18
+ } from "../lib/billing";
19
+
20
+ const NOW = Date.parse("2026-07-27T12:00:00Z");
21
+ const at = (days: number) => new Date(NOW + days * 86_400_000).toISOString();
22
+
23
+ const invoice = (over: Partial<Invoice> = {}): Invoice => ({
24
+ id: "i1",
25
+ number: "INV-2026-0001",
26
+ status: "sent",
27
+ taxRateBps: 0,
28
+ dueDate: at(10),
29
+ ...over,
30
+ });
31
+
32
+ const line = (over: Partial<LineItem> = {}): LineItem => ({
33
+ id: "l1",
34
+ invoiceId: "i1",
35
+ description: "Work",
36
+ quantityMilli: 1000,
37
+ unitPriceCents: 10_000,
38
+ ...over,
39
+ });
40
+
41
+ describe("lineTotalCents", () => {
42
+ test("multiplies quantity by unit price", () => {
43
+ expect(lineTotalCents({ quantityMilli: 32_000, unitPriceCents: 16_500 })).toBe(528_000);
44
+ });
45
+
46
+ test("handles fractional quantities exactly", () => {
47
+ // 1.5 hours at $165.00 — the case a float would get wrong.
48
+ expect(lineTotalCents({ quantityMilli: 1500, unitPriceCents: 16_500 })).toBe(24_750);
49
+ });
50
+
51
+ test("rounds to a whole cent", () => {
52
+ // 0.333 × $10.00 = $3.33
53
+ expect(lineTotalCents({ quantityMilli: 333, unitPriceCents: 1000 })).toBe(333);
54
+ });
55
+
56
+ test("garbage is zero, not NaN", () => {
57
+ expect(
58
+ lineTotalCents({ quantityMilli: NaN as unknown as number, unitPriceCents: 100 }),
59
+ ).toBe(0);
60
+ });
61
+ });
62
+
63
+ describe("totals", () => {
64
+ test("sums lines belonging to THIS invoice only", () => {
65
+ const t = totals(
66
+ invoice(),
67
+ [line({ id: "a" }), line({ id: "b", invoiceId: "other", unitPriceCents: 999_999 })],
68
+ [],
69
+ );
70
+ expect(t.subtotalCents).toBe(10_000);
71
+ });
72
+
73
+ test("applies tax to the rounded subtotal", () => {
74
+ // 8.75% of $124.00 = $10.85
75
+ const t = totals(
76
+ invoice({ taxRateBps: 875 }),
77
+ [line({ unitPriceCents: 12_400 })],
78
+ [],
79
+ );
80
+ expect(t.subtotalCents).toBe(12_400);
81
+ expect(t.taxCents).toBe(1085);
82
+ expect(t.totalCents).toBe(13_485);
83
+ });
84
+
85
+ test("balance is total minus payments", () => {
86
+ const payments: Payment[] = [{ id: "p1", invoiceId: "i1", amountCents: 4000 }];
87
+ const t = totals(invoice(), [line()], payments);
88
+ expect(t.paidCents).toBe(4000);
89
+ expect(t.balanceCents).toBe(6000);
90
+ });
91
+
92
+ test("an overpayment floors the balance at zero", () => {
93
+ // A credit is a decision to handle deliberately, not a negative balance
94
+ // that quietly offsets the next invoice.
95
+ const payments: Payment[] = [{ id: "p1", invoiceId: "i1", amountCents: 99_999 }];
96
+ expect(totals(invoice(), [line()], payments).balanceCents).toBe(0);
97
+ });
98
+
99
+ test("no lines is zero across the board", () => {
100
+ expect(totals(invoice(), [], [])).toEqual({
101
+ subtotalCents: 0,
102
+ taxCents: 0,
103
+ totalCents: 0,
104
+ paidCents: 0,
105
+ balanceCents: 0,
106
+ });
107
+ });
108
+ });
109
+
110
+ describe("isOverdue / displayStatus", () => {
111
+ test("sent, past due, and unpaid is overdue", () => {
112
+ expect(isOverdue(invoice({ dueDate: at(-1) }), 5000, NOW)).toBe(true);
113
+ });
114
+
115
+ test("due today is not yet overdue", () => {
116
+ // The customer has until the end of the day they were given.
117
+ expect(isOverdue(invoice({ dueDate: at(0) }), 5000, NOW)).toBe(false);
118
+ });
119
+
120
+ test("a paid balance is never overdue", () => {
121
+ expect(isOverdue(invoice({ dueDate: at(-30) }), 0, NOW)).toBe(false);
122
+ });
123
+
124
+ test("drafts and voids are never overdue", () => {
125
+ expect(isOverdue(invoice({ status: "draft", dueDate: at(-30) }), 5000, NOW)).toBe(false);
126
+ expect(isOverdue(invoice({ status: "void", dueDate: at(-30) }), 5000, NOW)).toBe(false);
127
+ });
128
+
129
+ test("no due date means nothing to be late for", () => {
130
+ expect(isOverdue(invoice({ dueDate: null }), 5000, NOW)).toBe(false);
131
+ });
132
+
133
+ test("displayStatus surfaces the derived states", () => {
134
+ expect(displayStatus(invoice({ dueDate: at(-3) }), 5000, NOW)).toBe("overdue");
135
+ // Fully paid reads as paid even before someone changes the field.
136
+ expect(displayStatus(invoice(), 0, NOW)).toBe("paid");
137
+ expect(displayStatus(invoice({ status: "draft" }), 100, NOW)).toBe("draft");
138
+ expect(displayStatus(invoice({ status: "void" }), 100, NOW)).toBe("void");
139
+ });
140
+
141
+ test("daysOverdue counts whole days", () => {
142
+ expect(daysOverdue(invoice({ dueDate: at(-7) }), NOW)).toBe(7);
143
+ expect(daysOverdue(invoice({ dueDate: null }), NOW)).toBeNull();
144
+ });
145
+ });
146
+
147
+ describe("summarize", () => {
148
+ const items: LineItem[] = [
149
+ line({ id: "a", invoiceId: "late", unitPriceCents: 100_000 }),
150
+ line({ id: "b", invoiceId: "open", unitPriceCents: 50_000 }),
151
+ line({ id: "c", invoiceId: "done", unitPriceCents: 20_000 }),
152
+ line({ id: "d", invoiceId: "draft", unitPriceCents: 30_000 }),
153
+ ];
154
+ const payments: Payment[] = [{ id: "p", invoiceId: "done", amountCents: 20_000 }];
155
+ const invoices: Invoice[] = [
156
+ invoice({ id: "late", dueDate: at(-5) }),
157
+ invoice({ id: "open", dueDate: at(5) }),
158
+ invoice({ id: "done", status: "paid" }),
159
+ invoice({ id: "draft", status: "draft" }),
160
+ ];
161
+
162
+ test("outstanding covers every sent invoice, overdue only the late ones", () => {
163
+ const s = summarize(invoices, items, payments, NOW);
164
+ expect(s.outstandingCents).toBe(150_000);
165
+ expect(s.overdueCents).toBe(100_000);
166
+ expect(s.overdueCount).toBe(1);
167
+ });
168
+
169
+ test("collected counts payments regardless of status", () => {
170
+ expect(summarize(invoices, items, payments, NOW).paidCents).toBe(20_000);
171
+ });
172
+
173
+ test("drafts are counted but never billed", () => {
174
+ // A draft has real line items ($300) but isn't owed until it's sent —
175
+ // including it would overstate what the business is waiting on.
176
+ const s = summarize(invoices, items, payments, NOW);
177
+ expect(s.draftCount).toBe(1);
178
+ expect(s.outstandingCents).toBe(150_000);
179
+
180
+ const withoutDraft = summarize(
181
+ invoices.filter((i) => i.status !== "draft"),
182
+ items,
183
+ payments,
184
+ NOW,
185
+ );
186
+ expect(withoutDraft.outstandingCents).toBe(s.outstandingCents);
187
+ });
188
+
189
+ test("an empty book is zeroes", () => {
190
+ expect(summarize([], [], [], NOW)).toEqual({
191
+ outstandingCents: 0,
192
+ overdueCents: 0,
193
+ paidCents: 0,
194
+ draftCount: 0,
195
+ overdueCount: 0,
196
+ });
197
+ });
198
+ });
199
+
200
+ describe("money", () => {
201
+ test("shows exact amounts — an invoice never compacts", () => {
202
+ expect(money(124_000)).toBe("$1,240.00");
203
+ expect(money(5)).toBe("$0.05");
204
+ expect(money(0)).toBe("$0.00");
205
+ expect(money(1_234_567)).toBe("$12,345.67");
206
+ });
207
+
208
+ test("handles nothing and negatives", () => {
209
+ expect(money(null)).toBe("$0.00");
210
+ expect(money(-2500)).toBe("-$25.00");
211
+ });
212
+ });
213
+
214
+ describe("quantity", () => {
215
+ test("drops trailing zeroes", () => {
216
+ expect(quantity(1000)).toBe("1");
217
+ expect(quantity(1500)).toBe("1.5");
218
+ expect(quantity(333)).toBe("0.333");
219
+ expect(quantity(null)).toBe("0");
220
+ });
221
+ });
222
+
223
+ describe("parseAmount", () => {
224
+ test("reads what a person actually types", () => {
225
+ expect(parseAmount("1,240.50")).toBe(124_050);
226
+ expect(parseAmount("$1240.5")).toBe(124_050);
227
+ expect(parseAmount("1240")).toBe(124_000);
228
+ expect(parseAmount(" 12.34 ")).toBe(1234);
229
+ });
230
+
231
+ test("rejects nonsense rather than silently billing $0.00", () => {
232
+ expect(parseAmount("")).toBeNull();
233
+ expect(parseAmount("abc")).toBeNull();
234
+ expect(parseAmount("1.2.3")).toBeNull();
235
+ expect(parseAmount(".")).toBeNull();
236
+ });
237
+ });
238
+
239
+ describe("parseQuantity", () => {
240
+ test("reads decimals into thousandths", () => {
241
+ expect(parseQuantity("1.5")).toBe(1500);
242
+ expect(parseQuantity("32")).toBe(32_000);
243
+ });
244
+
245
+ test("rejects nonsense and negatives", () => {
246
+ expect(parseQuantity("abc")).toBeNull();
247
+ expect(parseQuantity("-1")).toBeNull();
248
+ expect(parseQuantity("")).toBeNull();
249
+ });
250
+ });
251
+
252
+ describe("nextNumber", () => {
253
+ test("continues the year's series", () => {
254
+ expect(nextNumber(["INV-2026-0001", "INV-2026-0007"], 2026)).toBe("INV-2026-0008");
255
+ });
256
+
257
+ test("starts a new year at one", () => {
258
+ expect(nextNumber(["INV-2025-0042"], 2026)).toBe("INV-2026-0001");
259
+ });
260
+
261
+ test("ignores numbers it can't read", () => {
262
+ expect(nextNumber(["INV-2026-abc", "", "INV-2026-0003"], 2026)).toBe("INV-2026-0004");
263
+ });
264
+
265
+ test("an empty book starts at one", () => {
266
+ expect(nextNumber([], 2026)).toBe("INV-2026-0001");
267
+ });
268
+
269
+ test("pads so the series sorts lexically", () => {
270
+ // The list view sorts on the string; unpadded numbers would put 10 before 9.
271
+ expect(nextNumber(["INV-2026-0009"], 2026)).toBe("INV-2026-0010");
272
+ });
273
+ });
274
+
275
+ describe("statuses", () => {
276
+ test("overdue is derived, never a stored status", () => {
277
+ expect(isValidStatus("overdue")).toBe(false);
278
+ expect(isValidStatus("draft")).toBe(true);
279
+ expect(isValidStatus("sent")).toBe(true);
280
+ expect(isValidStatus("paid")).toBe(true);
281
+ expect(isValidStatus("void")).toBe(true);
282
+ });
283
+ });
@@ -0,0 +1,172 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ // This project uses the classic JSX transform, so .tsx tests import React.
3
+ import React from "react";
4
+ import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
5
+ import { LineItems } from "../components/line-items";
6
+ import { TotalsPanel } from "../components/totals-panel";
7
+ import { PaymentDialog } from "../components/payment-dialog";
8
+ import { StatusBadge } from "../components/status-badge";
9
+ import { totals, type LineItem } from "../lib/billing";
10
+
11
+ afterEach(cleanup);
12
+
13
+ // Tier 2: components. No mocking needed — they take data as props and report
14
+ // changes through callbacks, because the container owns `db`.
15
+
16
+ const items: LineItem[] = [
17
+ {
18
+ id: "l1",
19
+ invoiceId: "i1",
20
+ description: "Senior engineering",
21
+ quantityMilli: 32_000,
22
+ unitPriceCents: 16_500,
23
+ },
24
+ ];
25
+
26
+ describe("LineItems", () => {
27
+ test("renders the line with its computed amount", () => {
28
+ render(<LineItems items={items} editable onAdd={() => {}} onRemove={() => {}} />);
29
+ expect(screen.getByText("Senior engineering")).toBeDefined();
30
+ expect(screen.getByText("32")).toBeDefined();
31
+ expect(screen.getByText("$5,280.00")).toBeDefined();
32
+ });
33
+
34
+ test("adding parses the typed quantity and price into integers", () => {
35
+ const added: unknown[] = [];
36
+ render(<LineItems items={[]} editable onAdd={(d) => added.push(d)} onRemove={() => {}} />);
37
+ fireEvent.change(screen.getByLabelText("Description"), {
38
+ target: { value: "Training" },
39
+ });
40
+ fireEvent.change(screen.getByLabelText("Quantity"), { target: { value: "1.5" } });
41
+ fireEvent.change(screen.getByLabelText("Unit price"), { target: { value: "450.50" } });
42
+ fireEvent.click(screen.getByRole("button", { name: /add/i }));
43
+ expect(added).toEqual([
44
+ { description: "Training", quantityMilli: 1500, unitPriceCents: 45_050 },
45
+ ]);
46
+ });
47
+
48
+ test("an unreadable price can't be added", () => {
49
+ // Silently billing $0.00 for a typo is the failure this prevents.
50
+ const added: unknown[] = [];
51
+ render(<LineItems items={[]} editable onAdd={(d) => added.push(d)} onRemove={() => {}} />);
52
+ fireEvent.change(screen.getByLabelText("Description"), { target: { value: "X" } });
53
+ fireEvent.change(screen.getByLabelText("Unit price"), { target: { value: "abc" } });
54
+ fireEvent.click(screen.getByRole("button", { name: /add/i }));
55
+ expect(added).toEqual([]);
56
+ });
57
+
58
+ test("a sent invoice hides the editing controls entirely", () => {
59
+ // It's a document someone is paying against; editing desyncs it from their copy.
60
+ render(
61
+ <LineItems items={items} editable={false} onAdd={() => {}} onRemove={() => {}} />,
62
+ );
63
+ expect(screen.queryByLabelText("Description")).toBeNull();
64
+ expect(screen.queryByRole("button", { name: /remove/i })).toBeNull();
65
+ });
66
+
67
+ test("removing reports the line id", () => {
68
+ const removed: string[] = [];
69
+ render(
70
+ <LineItems items={items} editable onAdd={() => {}} onRemove={(id) => removed.push(id)} />,
71
+ );
72
+ fireEvent.click(screen.getByRole("button", { name: /remove senior engineering/i }));
73
+ expect(removed).toEqual(["l1"]);
74
+ });
75
+
76
+ test("no lines shows a placeholder row, not an empty table", () => {
77
+ render(<LineItems items={[]} editable onAdd={() => {}} onRemove={() => {}} />);
78
+ expect(screen.getByText("No lines yet.")).toBeDefined();
79
+ });
80
+ });
81
+
82
+ describe("TotalsPanel", () => {
83
+ test("shows subtotal, tax, total and balance in reading order", () => {
84
+ const t = totals(
85
+ { id: "i1", number: "INV-1", status: "sent", taxRateBps: 875 },
86
+ items,
87
+ [{ id: "p", invoiceId: "i1", amountCents: 100_000 }],
88
+ );
89
+ const { container } = render(<TotalsPanel totals={t} taxRateBps={875} />);
90
+ const text = container.textContent ?? "";
91
+ expect(text).toContain("$5,280.00");
92
+ expect(text).toContain("Tax (8.75%)");
93
+ expect(text).toContain("Balance due");
94
+ });
95
+
96
+ test("hides the tax row when there's no tax", () => {
97
+ const t = totals({ id: "i1", number: "INV-1", status: "sent" }, items, []);
98
+ const { container } = render(<TotalsPanel totals={t} taxRateBps={0} />);
99
+ expect(container.textContent).not.toContain("Tax");
100
+ });
101
+ });
102
+
103
+ describe("PaymentDialog", () => {
104
+ test("defaults to the outstanding balance", () => {
105
+ // Retyping the figure is how a typo gets in.
106
+ render(
107
+ <PaymentDialog open balanceCents={52_800} onOpenChange={() => {}} onRecord={() => {}} />,
108
+ );
109
+ expect((screen.getByLabelText("Amount") as HTMLInputElement).value).toBe("528.00");
110
+ });
111
+
112
+ test("records the parsed amount in cents", () => {
113
+ const recorded: unknown[] = [];
114
+ render(
115
+ <PaymentDialog
116
+ open
117
+ balanceCents={52_800}
118
+ onOpenChange={() => {}}
119
+ onRecord={(cents, method) => recorded.push([cents, method])}
120
+ />,
121
+ );
122
+ fireEvent.change(screen.getByLabelText("Amount"), { target: { value: "250.25" } });
123
+ fireEvent.click(screen.getByRole("button", { name: /record payment/i }));
124
+ expect(recorded).toEqual([[25_025, "bank"]]);
125
+ });
126
+
127
+ test("refuses more than the balance", () => {
128
+ const recorded: unknown[] = [];
129
+ render(
130
+ <PaymentDialog
131
+ open
132
+ balanceCents={1000}
133
+ onOpenChange={() => {}}
134
+ onRecord={(cents) => recorded.push(cents)}
135
+ />,
136
+ );
137
+ fireEvent.change(screen.getByLabelText("Amount"), { target: { value: "50" } });
138
+ fireEvent.click(screen.getByRole("button", { name: /record payment/i }));
139
+ expect(recorded).toEqual([]);
140
+ expect(screen.getByText(/more than the balance/i)).toBeDefined();
141
+ });
142
+
143
+ test("renders nothing when closed", () => {
144
+ const { container } = render(
145
+ <PaymentDialog
146
+ open={false}
147
+ balanceCents={1000}
148
+ onOpenChange={() => {}}
149
+ onRecord={() => {}}
150
+ />,
151
+ );
152
+ expect(container.firstChild).toBeNull();
153
+ });
154
+ });
155
+
156
+ describe("StatusBadge", () => {
157
+ test("labels the derived overdue state", () => {
158
+ render(<StatusBadge status="overdue" />);
159
+ expect(screen.getByText("Overdue")).toBeDefined();
160
+ });
161
+
162
+ test("labels the stored ones", () => {
163
+ const { container } = render(
164
+ <>
165
+ <StatusBadge status="draft" />
166
+ <StatusBadge status="paid" />
167
+ </>,
168
+ );
169
+ expect(within(container).getByText("Draft")).toBeDefined();
170
+ expect(within(container).getByText("Paid")).toBeDefined();
171
+ });
172
+ });