shutro-erp 0.3.3 → 0.3.5

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 (412) hide show
  1. package/dist/account_portal/.kosh/bindings.js +1 -1
  2. package/dist/account_portal/.kosh/schema.d.ts +181 -3
  3. package/dist/account_portal/module.d.ts +359 -6
  4. package/dist/accounting/.kosh/bindings.js +1 -1
  5. package/dist/accounting/.kosh/schema.d.ts +181 -3
  6. package/dist/accounting/index.js +214 -2
  7. package/dist/accounting/module.d.ts +217 -5
  8. package/dist/accounting/module.js +23 -2
  9. package/dist/calendar/.kosh/bindings.d.ts +22 -0
  10. package/dist/calendar/.kosh/bindings.js +15 -0
  11. package/dist/calendar/.kosh/schema.d.ts +795 -0
  12. package/dist/calendar/demo.d.ts +6 -0
  13. package/dist/calendar/demo.js +24 -0
  14. package/dist/calendar/index.d.ts +3 -0
  15. package/dist/calendar/index.js +291 -0
  16. package/dist/calendar/module.d.ts +1705 -0
  17. package/dist/calendar/module.js +180 -0
  18. package/dist/cli/init.js +48 -0
  19. package/dist/cli/templates.d.ts +1 -1
  20. package/dist/cli/templates.js +137 -0
  21. package/dist/contacts/.kosh/bindings.js +1 -1
  22. package/dist/contacts/.kosh/schema.d.ts +205 -3
  23. package/dist/contacts/demo.js +30 -1
  24. package/dist/contacts/index.d.ts +1 -1
  25. package/dist/contacts/index.js +155 -9
  26. package/dist/contacts/module.d.ts +338 -2
  27. package/dist/contacts/module.js +82 -5
  28. package/dist/crm/.kosh/bindings.d.ts +22 -0
  29. package/dist/crm/.kosh/bindings.js +15 -0
  30. package/dist/crm/.kosh/schema.d.ts +799 -0
  31. package/dist/crm/demo.d.ts +6 -0
  32. package/dist/crm/demo.js +45 -0
  33. package/dist/crm/index.d.ts +3 -0
  34. package/dist/crm/index.js +361 -0
  35. package/dist/crm/module.d.ts +1843 -0
  36. package/dist/crm/module.js +207 -0
  37. package/dist/event/.kosh/bindings.d.ts +22 -0
  38. package/dist/event/.kosh/bindings.js +15 -0
  39. package/dist/event/.kosh/schema.d.ts +843 -0
  40. package/dist/event/demo.d.ts +6 -0
  41. package/dist/event/demo.js +65 -0
  42. package/dist/event/index.d.ts +14 -0
  43. package/dist/event/index.js +333 -0
  44. package/dist/event/mails.d.ts +15 -0
  45. package/dist/event/mails.js +112 -0
  46. package/dist/event/module.d.ts +2800 -0
  47. package/dist/event/module.js +323 -0
  48. package/dist/event/web/CheckInBox.d.ts +9 -0
  49. package/dist/event/web/CheckInBox.js +87 -0
  50. package/dist/event/web/index.d.ts +11 -0
  51. package/dist/event/web/index.js +16 -0
  52. package/dist/event/web/messages.d.ts +2 -0
  53. package/dist/event/web/messages.js +23 -0
  54. package/dist/event_account/.kosh/bindings.d.ts +22 -0
  55. package/dist/event_account/.kosh/bindings.js +15 -0
  56. package/dist/event_account/.kosh/schema.d.ts +1639 -0
  57. package/dist/event_account/index.d.ts +3 -0
  58. package/dist/event_account/index.js +28 -0
  59. package/dist/event_account/module.d.ts +6968 -0
  60. package/dist/event_account/module.js +90 -0
  61. package/dist/hr/.kosh/bindings.d.ts +22 -0
  62. package/dist/hr/.kosh/bindings.js +15 -0
  63. package/dist/hr/.kosh/schema.d.ts +878 -0
  64. package/dist/hr/demo.d.ts +6 -0
  65. package/dist/hr/demo.js +60 -0
  66. package/dist/hr/index.d.ts +3 -0
  67. package/dist/hr/index.js +483 -0
  68. package/dist/hr/module.d.ts +2611 -0
  69. package/dist/hr/module.js +350 -0
  70. package/dist/integrations/index.js +12 -0
  71. package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
  72. package/dist/integrations/web/OAuthAppsPage.js +79 -0
  73. package/dist/integrations/web/index.d.ts +1 -0
  74. package/dist/integrations/web/index.js +3 -0
  75. package/dist/integrations/web/messages.js +58 -0
  76. package/dist/l10n_bd/.kosh/bindings.js +1 -1
  77. package/dist/l10n_bd/.kosh/schema.d.ts +181 -3
  78. package/dist/l10n_bd/module.d.ts +359 -6
  79. package/dist/l10n_in/.kosh/bindings.js +1 -1
  80. package/dist/l10n_in/.kosh/schema.d.ts +181 -3
  81. package/dist/l10n_in/module.d.ts +380 -7
  82. package/dist/maintenance/.kosh/bindings.d.ts +22 -0
  83. package/dist/maintenance/.kosh/bindings.js +15 -0
  84. package/dist/maintenance/.kosh/schema.d.ts +911 -0
  85. package/dist/maintenance/demo.d.ts +6 -0
  86. package/dist/maintenance/demo.js +60 -0
  87. package/dist/maintenance/index.d.ts +3 -0
  88. package/dist/maintenance/index.js +422 -0
  89. package/dist/maintenance/module.d.ts +3545 -0
  90. package/dist/maintenance/module.js +446 -0
  91. package/dist/maintenance_stock/.kosh/bindings.d.ts +22 -0
  92. package/dist/maintenance_stock/.kosh/bindings.js +15 -0
  93. package/dist/maintenance_stock/.kosh/schema.d.ts +1371 -0
  94. package/dist/maintenance_stock/index.d.ts +9 -0
  95. package/dist/maintenance_stock/index.js +52 -0
  96. package/dist/maintenance_stock/module.d.ts +6953 -0
  97. package/dist/maintenance_stock/module.js +192 -0
  98. package/dist/messaging/web/index.js +1 -1
  99. package/dist/modules-sdk/index.d.ts +1 -1
  100. package/dist/modules-sdk/manifest.d.ts +70 -0
  101. package/dist/modules-sdk/manifest.js +35 -1
  102. package/dist/mrp/.kosh/bindings.d.ts +22 -0
  103. package/dist/mrp/.kosh/bindings.js +15 -0
  104. package/dist/mrp/.kosh/schema.d.ts +1378 -0
  105. package/dist/mrp/demo.d.ts +7 -0
  106. package/dist/mrp/demo.js +42 -0
  107. package/dist/mrp/index.d.ts +3 -0
  108. package/dist/mrp/index.js +392 -0
  109. package/dist/mrp/module.d.ts +8608 -0
  110. package/dist/mrp/module.js +695 -0
  111. package/dist/mrp_account/.kosh/bindings.d.ts +22 -0
  112. package/dist/mrp_account/.kosh/bindings.js +15 -0
  113. package/dist/mrp_account/.kosh/schema.d.ts +2106 -0
  114. package/dist/mrp_account/index.d.ts +3 -0
  115. package/dist/mrp_account/index.js +22 -0
  116. package/dist/mrp_account/module.d.ts +20840 -0
  117. package/dist/mrp_account/module.js +123 -0
  118. package/dist/notify/web/index.js +1 -1
  119. package/dist/payment/.kosh/bindings.js +1 -1
  120. package/dist/payment/.kosh/schema.d.ts +181 -3
  121. package/dist/payment/module.d.ts +359 -6
  122. package/dist/portal/.kosh/bindings.js +1 -1
  123. package/dist/portal/.kosh/schema.d.ts +205 -3
  124. package/dist/portal/module.d.ts +166 -2
  125. package/dist/pos/.kosh/bindings.d.ts +22 -0
  126. package/dist/pos/.kosh/bindings.js +15 -0
  127. package/dist/pos/.kosh/schema.d.ts +1690 -0
  128. package/dist/pos/accounting.d.ts +10 -0
  129. package/dist/pos/accounting.js +149 -0
  130. package/dist/pos/demo.d.ts +6 -0
  131. package/dist/pos/demo.js +90 -0
  132. package/dist/pos/index.d.ts +9 -0
  133. package/dist/pos/index.js +349 -0
  134. package/dist/pos/module.d.ts +9636 -0
  135. package/dist/pos/module.js +423 -0
  136. package/dist/pos/routes.d.ts +39 -0
  137. package/dist/pos/routes.js +368 -0
  138. package/dist/pos/web/Register.d.ts +9 -0
  139. package/dist/pos/web/Register.js +252 -0
  140. package/dist/pos/web/index.d.ts +11 -0
  141. package/dist/pos/web/index.js +16 -0
  142. package/dist/pos/web/messages.d.ts +2 -0
  143. package/dist/pos/web/messages.js +87 -0
  144. package/dist/pos_stock/.kosh/bindings.d.ts +22 -0
  145. package/dist/pos_stock/.kosh/bindings.js +15 -0
  146. package/dist/pos_stock/.kosh/schema.d.ts +2015 -0
  147. package/dist/pos_stock/index.d.ts +9 -0
  148. package/dist/pos_stock/index.js +35 -0
  149. package/dist/pos_stock/module.d.ts +11935 -0
  150. package/dist/pos_stock/module.js +115 -0
  151. package/dist/product/.kosh/bindings.js +1 -1
  152. package/dist/product/.kosh/schema.d.ts +178 -1
  153. package/dist/product/index.js +81 -4
  154. package/dist/product/module.d.ts +43 -1
  155. package/dist/product/module.js +9 -2
  156. package/dist/project/.kosh/bindings.js +1 -1
  157. package/dist/project/.kosh/schema.d.ts +181 -3
  158. package/dist/project/index.js +147 -7
  159. package/dist/project/module.d.ts +210 -2
  160. package/dist/project/module.js +52 -5
  161. package/dist/project_portal/.kosh/bindings.js +1 -1
  162. package/dist/project_portal/.kosh/schema.d.ts +181 -3
  163. package/dist/project_portal/module.d.ts +353 -4
  164. package/dist/purchase/.kosh/bindings.js +1 -1
  165. package/dist/purchase/.kosh/schema.d.ts +181 -3
  166. package/dist/purchase/index.js +163 -3
  167. package/dist/purchase/module.d.ts +215 -3
  168. package/dist/purchase/module.js +21 -3
  169. package/dist/purchase_account/.kosh/bindings.js +1 -1
  170. package/dist/purchase_account/.kosh/schema.d.ts +182 -3
  171. package/dist/purchase_account/index.js +26 -1
  172. package/dist/purchase_account/module.d.ts +393 -7
  173. package/dist/purchase_portal/.kosh/bindings.js +1 -1
  174. package/dist/purchase_portal/.kosh/schema.d.ts +181 -3
  175. package/dist/purchase_portal/module.d.ts +366 -5
  176. package/dist/purchase_stock/.kosh/bindings.d.ts +22 -0
  177. package/dist/purchase_stock/.kosh/bindings.js +15 -0
  178. package/dist/purchase_stock/.kosh/schema.d.ts +1310 -0
  179. package/dist/purchase_stock/index.d.ts +7 -0
  180. package/dist/purchase_stock/index.js +91 -0
  181. package/dist/purchase_stock/module.d.ts +6763 -0
  182. package/dist/purchase_stock/module.js +144 -0
  183. package/dist/purchase_stock_account/.kosh/bindings.d.ts +22 -0
  184. package/dist/purchase_stock_account/.kosh/bindings.js +15 -0
  185. package/dist/purchase_stock_account/.kosh/schema.d.ts +1959 -0
  186. package/dist/purchase_stock_account/index.d.ts +3 -0
  187. package/dist/purchase_stock_account/index.js +24 -0
  188. package/dist/purchase_stock_account/module.d.ts +35366 -0
  189. package/dist/purchase_stock_account/module.js +162 -0
  190. package/dist/recruitment/.kosh/bindings.d.ts +22 -0
  191. package/dist/recruitment/.kosh/bindings.js +15 -0
  192. package/dist/recruitment/.kosh/schema.d.ts +990 -0
  193. package/dist/recruitment/demo.d.ts +6 -0
  194. package/dist/recruitment/demo.js +45 -0
  195. package/dist/recruitment/index.d.ts +3 -0
  196. package/dist/recruitment/index.js +385 -0
  197. package/dist/recruitment/module.d.ts +3247 -0
  198. package/dist/recruitment/module.js +193 -0
  199. package/dist/recruitment/web/HireButton.d.ts +9 -0
  200. package/dist/recruitment/web/HireButton.js +32 -0
  201. package/dist/recruitment/web/index.d.ts +11 -0
  202. package/dist/recruitment/web/index.js +16 -0
  203. package/dist/recruitment/web/messages.d.ts +2 -0
  204. package/dist/recruitment/web/messages.js +19 -0
  205. package/dist/reporting/documents.d.ts +5 -0
  206. package/dist/reporting/documents.js +28 -1
  207. package/dist/reporting/index.d.ts +1 -1
  208. package/dist/reporting/index.js +4 -2
  209. package/dist/reporting/models.js +9 -0
  210. package/dist/reporting/overviews.d.ts +16 -0
  211. package/dist/reporting/overviews.js +56 -0
  212. package/dist/reporting/routes-documents.js +1 -1
  213. package/dist/reporting/routes.js +138 -80
  214. package/dist/reporting/web/DashboardView.d.ts +11 -0
  215. package/dist/reporting/web/DashboardView.js +2 -2
  216. package/dist/reporting/web/DesignerView.js +5 -0
  217. package/dist/reporting/web/Overview.d.ts +9 -0
  218. package/dist/reporting/web/Overview.js +53 -0
  219. package/dist/reporting/web/index.d.ts +1 -0
  220. package/dist/reporting/web/index.js +4 -1
  221. package/dist/reporting/web/messages.js +9 -0
  222. package/dist/sale_account/.kosh/bindings.js +1 -1
  223. package/dist/sale_account/.kosh/schema.d.ts +186 -3
  224. package/dist/sale_account/index.js +22 -1
  225. package/dist/sale_account/module.d.ts +461 -7
  226. package/dist/sale_portal/.kosh/bindings.js +1 -1
  227. package/dist/sale_portal/.kosh/schema.d.ts +185 -3
  228. package/dist/sale_portal/module.d.ts +434 -5
  229. package/dist/sale_project/.kosh/bindings.js +1 -1
  230. package/dist/sale_project/.kosh/schema.d.ts +187 -3
  231. package/dist/sale_project/module.d.ts +476 -6
  232. package/dist/sale_stock/.kosh/bindings.d.ts +22 -0
  233. package/dist/sale_stock/.kosh/bindings.js +15 -0
  234. package/dist/sale_stock/.kosh/schema.d.ts +1228 -0
  235. package/dist/sale_stock/index.d.ts +7 -0
  236. package/dist/sale_stock/index.js +84 -0
  237. package/dist/sale_stock/module.d.ts +6791 -0
  238. package/dist/sale_stock/module.js +146 -0
  239. package/dist/sales/.kosh/bindings.js +1 -1
  240. package/dist/sales/.kosh/schema.d.ts +185 -3
  241. package/dist/sales/index.js +191 -4
  242. package/dist/sales/module.d.ts +355 -3
  243. package/dist/sales/module.js +78 -3
  244. package/dist/server/api/metadata.d.ts +5 -0
  245. package/dist/server/api/metadata.js +5 -0
  246. package/dist/server/api/rest.js +64 -1
  247. package/dist/server/api/views.js +1 -0
  248. package/dist/server/app-settings.d.ts +7 -0
  249. package/dist/server/app-settings.js +10 -0
  250. package/dist/server/auth/base.js +85 -0
  251. package/dist/server/auth/oauth.d.ts +21 -0
  252. package/dist/server/auth/oauth.js +579 -0
  253. package/dist/server/auth/service.d.ts +4 -0
  254. package/dist/server/auth/service.js +17 -0
  255. package/dist/server/identity.d.ts +2 -0
  256. package/dist/server/images.d.ts +12 -0
  257. package/dist/server/images.js +139 -0
  258. package/dist/server/index.d.ts +2 -0
  259. package/dist/server/index.js +2 -0
  260. package/dist/server/server.d.ts +4 -0
  261. package/dist/server/server.js +9 -1
  262. package/dist/settings/app-settings.d.ts +2 -0
  263. package/dist/settings/app-settings.js +518 -0
  264. package/dist/settings/company.js +3 -1
  265. package/dist/settings/index.d.ts +1 -0
  266. package/dist/settings/index.js +15 -0
  267. package/dist/settings/people.js +32 -6
  268. package/dist/settings/web/AppSettingsPage.d.ts +11 -0
  269. package/dist/settings/web/AppSettingsPage.js +210 -0
  270. package/dist/settings/web/CompaniesPage.js +2 -2
  271. package/dist/settings/web/CompanyPage.js +13 -2
  272. package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
  273. package/dist/settings/web/ConnectedAppsPage.js +33 -0
  274. package/dist/settings/web/OverviewPage.js +28 -4
  275. package/dist/settings/web/PeoplePage.js +23 -18
  276. package/dist/settings/web/ProfilePage.js +9 -7
  277. package/dist/settings/web/index.d.ts +2 -0
  278. package/dist/settings/web/index.js +8 -0
  279. package/dist/settings/web/messages.js +146 -2
  280. package/dist/stock/.kosh/bindings.d.ts +22 -0
  281. package/dist/stock/.kosh/bindings.js +15 -0
  282. package/dist/stock/.kosh/schema.d.ts +1147 -0
  283. package/dist/stock/demo.d.ts +7 -0
  284. package/dist/stock/demo.js +55 -0
  285. package/dist/stock/index.d.ts +3 -0
  286. package/dist/stock/index.js +674 -0
  287. package/dist/stock/module.d.ts +6330 -0
  288. package/dist/stock/module.js +1325 -0
  289. package/dist/stock/web/ScanBox.d.ts +8 -0
  290. package/dist/stock/web/ScanBox.js +90 -0
  291. package/dist/stock/web/index.d.ts +11 -0
  292. package/dist/stock/web/index.js +16 -0
  293. package/dist/stock/web/messages.d.ts +2 -0
  294. package/dist/stock/web/messages.js +25 -0
  295. package/dist/stock_account/.kosh/bindings.d.ts +22 -0
  296. package/dist/stock_account/.kosh/bindings.js +15 -0
  297. package/dist/stock_account/.kosh/schema.d.ts +1875 -0
  298. package/dist/stock_account/index.d.ts +3 -0
  299. package/dist/stock_account/index.js +130 -0
  300. package/dist/stock_account/module.d.ts +10655 -0
  301. package/dist/stock_account/module.js +339 -0
  302. package/dist/storage/attachments.d.ts +5 -0
  303. package/dist/storage/attachments.js +12 -1
  304. package/dist/storage/models.js +3 -0
  305. package/dist/storage/routes.js +13 -2
  306. package/dist/todo/.kosh/bindings.d.ts +22 -0
  307. package/dist/todo/.kosh/bindings.js +15 -0
  308. package/dist/todo/.kosh/schema.d.ts +732 -0
  309. package/dist/todo/demo.d.ts +6 -0
  310. package/dist/todo/demo.js +35 -0
  311. package/dist/todo/index.d.ts +3 -0
  312. package/dist/todo/index.js +187 -0
  313. package/dist/todo/module.d.ts +1180 -0
  314. package/dist/todo/module.js +130 -0
  315. package/dist/trade_finance/.kosh/bindings.d.ts +22 -0
  316. package/dist/trade_finance/.kosh/bindings.js +15 -0
  317. package/dist/trade_finance/.kosh/schema.d.ts +884 -0
  318. package/dist/trade_finance/demo.d.ts +6 -0
  319. package/dist/trade_finance/demo.js +63 -0
  320. package/dist/trade_finance/index.d.ts +3 -0
  321. package/dist/trade_finance/index.js +413 -0
  322. package/dist/trade_finance/module.d.ts +3779 -0
  323. package/dist/trade_finance/module.js +505 -0
  324. package/dist/trade_finance_account/.kosh/bindings.d.ts +22 -0
  325. package/dist/trade_finance_account/.kosh/bindings.js +15 -0
  326. package/dist/trade_finance_account/.kosh/schema.d.ts +1711 -0
  327. package/dist/trade_finance_account/index.d.ts +3 -0
  328. package/dist/trade_finance_account/index.js +92 -0
  329. package/dist/trade_finance_account/module.d.ts +7928 -0
  330. package/dist/trade_finance_account/module.js +244 -0
  331. package/dist/trade_finance_purchase/.kosh/bindings.d.ts +22 -0
  332. package/dist/trade_finance_purchase/.kosh/bindings.js +15 -0
  333. package/dist/trade_finance_purchase/.kosh/schema.d.ts +1153 -0
  334. package/dist/trade_finance_purchase/index.d.ts +8 -0
  335. package/dist/trade_finance_purchase/index.js +50 -0
  336. package/dist/trade_finance_purchase/module.d.ts +4252 -0
  337. package/dist/trade_finance_purchase/module.js +44 -0
  338. package/dist/trade_finance_sale/.kosh/bindings.d.ts +22 -0
  339. package/dist/trade_finance_sale/.kosh/bindings.js +15 -0
  340. package/dist/trade_finance_sale/.kosh/schema.d.ts +1071 -0
  341. package/dist/trade_finance_sale/index.d.ts +8 -0
  342. package/dist/trade_finance_sale/index.js +50 -0
  343. package/dist/trade_finance_sale/module.d.ts +4280 -0
  344. package/dist/trade_finance_sale/module.js +44 -0
  345. package/dist/ui/api.d.ts +6 -1
  346. package/dist/ui/api.js +2 -1
  347. package/dist/ui/app.js +51 -17
  348. package/dist/ui/components/AppIcon.js +33 -1
  349. package/dist/ui/components/Avatar.d.ts +21 -0
  350. package/dist/ui/components/Avatar.js +71 -0
  351. package/dist/ui/components/primitives.d.ts +2 -0
  352. package/dist/ui/components/primitives.js +7 -1
  353. package/dist/ui/i18n/messages.d.ts +53 -0
  354. package/dist/ui/i18n/messages.js +98 -0
  355. package/dist/ui/index.d.ts +2 -0
  356. package/dist/ui/index.js +2 -0
  357. package/dist/ui/offline/queue.d.ts +5 -1
  358. package/dist/ui/offline/queue.js +2 -0
  359. package/dist/ui/registry.d.ts +12 -1
  360. package/dist/ui/registry.js +10 -1
  361. package/dist/ui/search/ViewTabs.d.ts +14 -0
  362. package/dist/ui/search/ViewTabs.js +42 -0
  363. package/dist/ui/shell/Consent.d.ts +5 -0
  364. package/dist/ui/shell/Consent.js +35 -0
  365. package/dist/ui/shell/Home.js +136 -16
  366. package/dist/ui/shell/Sidebar.js +15 -22
  367. package/dist/ui/views/FormView.js +98 -17
  368. package/dist/ui/views/KanbanView.d.ts +2 -0
  369. package/dist/ui/views/KanbanView.js +32 -3
  370. package/dist/ui/views/ListView.js +155 -31
  371. package/dist/ui/views/StatsRow.d.ts +14 -0
  372. package/dist/ui/views/StatsRow.js +88 -0
  373. package/dist/ui/views/widgets.d.ts +2 -1
  374. package/dist/ui/views/widgets.js +194 -1
  375. package/dist/views/builders.d.ts +19 -2
  376. package/dist/views/builders.js +21 -1
  377. package/dist/views/condition.d.ts +6 -0
  378. package/dist/views/condition.js +20 -0
  379. package/dist/views/documents.d.ts +8 -0
  380. package/dist/views/extend.d.ts +2 -2
  381. package/dist/views/extend.js +11 -1
  382. package/dist/views/index.d.ts +2 -2
  383. package/dist/views/index.js +2 -2
  384. package/dist/views/types.d.ts +52 -5
  385. package/dist/views/validate.js +39 -1
  386. package/dist/website/.kosh/bindings.d.ts +22 -0
  387. package/dist/website/.kosh/bindings.js +15 -0
  388. package/dist/website/.kosh/schema.d.ts +881 -0
  389. package/dist/website/demo.d.ts +6 -0
  390. package/dist/website/demo.js +115 -0
  391. package/dist/website/index.d.ts +9 -0
  392. package/dist/website/index.js +250 -0
  393. package/dist/website/module.d.ts +2987 -0
  394. package/dist/website/module.js +281 -0
  395. package/dist/website/render.d.ts +45 -0
  396. package/dist/website/render.js +102 -0
  397. package/dist/website/routes.d.ts +12 -0
  398. package/dist/website/routes.js +246 -0
  399. package/dist/website/web/PagePreview.d.ts +7 -0
  400. package/dist/website/web/PagePreview.js +33 -0
  401. package/dist/website/web/index.d.ts +11 -0
  402. package/dist/website/web/index.js +16 -0
  403. package/dist/website/web/messages.d.ts +2 -0
  404. package/dist/website/web/messages.js +15 -0
  405. package/dist/website_crm/.kosh/bindings.d.ts +22 -0
  406. package/dist/website_crm/.kosh/bindings.js +15 -0
  407. package/dist/website_crm/.kosh/schema.d.ts +966 -0
  408. package/dist/website_crm/index.d.ts +3 -0
  409. package/dist/website_crm/index.js +39 -0
  410. package/dist/website_crm/module.d.ts +2955 -0
  411. package/dist/website_crm/module.js +44 -0
  412. package/package.json +118 -1
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Counter's routes (RFC 0021): what the register needs in few calls — the shop's catalogue, the cashier's open
3
+ * session, a sale posted whole (lines and payments, accepted once by its client id, paid at once), a session
4
+ * closed with its count.
5
+ *
6
+ * - `GET /api/v1/counter/shops` the shops the cashier may sell in, with their payment methods
7
+ * - `GET /api/v1/counter/catalogue?shop=` products for the register: price, taxes, barcode, picture
8
+ * - `GET /api/v1/counter/session?shop=` the cashier's open session at the shop, or null
9
+ * - `POST /api/v1/counter/sessions` `{ shop, openingCash }`: a session opened
10
+ * - `POST /api/v1/counter/orders` a sale: `{ session, clientId, partner?, lines, payments, note? }`
11
+ * - `POST /api/v1/counter/sessions/:id/close` `{ closingCash, differenceReason? }`: the count, then closed
12
+ */
13
+ import { mailerFromEnv } from '../mail/index.js';
14
+ import { messengerFromEnv } from '../notify/index.js';
15
+ import { documentFields, renderDocument } from '../reporting/index.js';
16
+ import { HttpProblem, publicApiModels } from '../server/index.js';
17
+ import { orm } from '../server/orm.js';
18
+ const idOf = (value, what) => {
19
+ const id = Number(value);
20
+ if (!Number.isSafeInteger(id) || id <= 0)
21
+ throw new HttpProblem(400, 'invalid-id', 'Invalid id', `${what} is a positive integer.`);
22
+ return id;
23
+ };
24
+ const text = (value) => (typeof value === 'string' ? value.trim() : '');
25
+ export function registerCounterRoutes(context, options = {}) {
26
+ const { app } = context;
27
+ const mailer = 'mailer' in options ? options.mailer : mailerFromEnv();
28
+ const messenger = 'messenger' in options ? options.messenger : messengerFromEnv();
29
+ const models = publicApiModels(context.registry);
30
+ const fields = documentFields(models);
31
+ // `POST /api/v1/counter/orders/:id/receipt` `{ email?, phone? }`: the receipt sent — the document by email,
32
+ // a summary by text; what was sent is said back.
33
+ app.post('/api/v1/counter/orders/:id/receipt', async (request) => {
34
+ const id = idOf(request.params.id, 'id');
35
+ const body = (request.body ?? {});
36
+ const email = text(body.email);
37
+ const phone = text(body.phone);
38
+ if (email === '' && phone === '')
39
+ throw new HttpProblem(422, 'no-address', 'Nowhere to send', 'Give an email address or a phone number.');
40
+ const model = models.get('pos.order');
41
+ if (model === undefined)
42
+ throw new HttpProblem(404, 'unknown-model', 'Unknown model', 'pos.order');
43
+ return request.env(async (raw) => {
44
+ const env = raw;
45
+ const order = await orderOut(env, id);
46
+ const sent = [];
47
+ if (email !== '') {
48
+ if (mailer === undefined)
49
+ throw new HttpProblem(409, 'no-mailer', 'No email set up', 'Set SMTP_URL to send receipts by email.');
50
+ const [template] = (await (await env.model('report.template').search({ where: { key: 'pos.receipt' }, limit: 1 })).load([])).read(['id', 'name', 'layout', 'scope']);
51
+ if (template === undefined)
52
+ throw new HttpProblem(409, 'no-template', 'No receipt template', 'Run the apps\u2019 defaults.');
53
+ const rendered = await renderDocument(env, model, template, fields, { ids: [id] }, 'html');
54
+ await mailer.send({
55
+ to: email,
56
+ subject: `Receipt ${String(order['name'])}`,
57
+ text: receiptText(order),
58
+ html: rendered.content.toString('utf8'),
59
+ });
60
+ sent.push('email');
61
+ }
62
+ if (phone !== '') {
63
+ if (messenger === undefined)
64
+ throw new HttpProblem(409, 'no-messenger', 'No text messaging set up', 'Set up Twilio to send receipts by text.');
65
+ await messenger.send({ channel: 'sms', to: phone, text: receiptText(order) });
66
+ sent.push('sms');
67
+ }
68
+ return { sent };
69
+ });
70
+ });
71
+ app.get('/api/v1/counter/shops', async (request) => request.env(async (raw) => {
72
+ const env = raw;
73
+ const shops = await (await env.model('pos.shop').search({})).load({ methods: true });
74
+ return {
75
+ shops: shops.map((s) => ({
76
+ id: s.id,
77
+ name: s.name,
78
+ pricesIncludeTax: s.pricesIncludeTax,
79
+ receiptFooter: s.receiptFooter,
80
+ methods: [...s.methods].map((m) => ({ id: m.id, name: m.name, kind: m.kind })),
81
+ })),
82
+ };
83
+ }));
84
+ app.get('/api/v1/counter/catalogue', async (request) => request.env(async (raw) => {
85
+ const env = raw;
86
+ const products = await (await env
87
+ .model('product.product')
88
+ .search({ where: { saleOk: true }, order: ['name asc'], limit: 2000 })).load({ taxes: true, category: true, uom: true, image: true });
89
+ return {
90
+ products: products.map((p) => ({
91
+ id: p.id,
92
+ name: p.name,
93
+ code: p.code,
94
+ barcode: p.barcode,
95
+ price: p.listPrice.toString(),
96
+ category: p.category?.name ?? null,
97
+ uom: p.uom?.name ?? null,
98
+ image: p.image?.id ?? null,
99
+ taxes: [...p.taxes].map((t) => ({
100
+ id: t.id,
101
+ name: t.name,
102
+ amountType: t.amountType,
103
+ rate: t.rate.toString(),
104
+ fixedAmount: t.fixedAmount.toString(),
105
+ })),
106
+ })),
107
+ };
108
+ }));
109
+ app.get('/api/v1/counter/session', async (request) => request.env(async (raw) => {
110
+ const env = raw;
111
+ const shop = idOf(request.query.shop, 'shop');
112
+ const [session] = await env.model('pos.session').search({
113
+ where: { shop: shop, cashier: env.userId, state: 'open' },
114
+ limit: 1,
115
+ });
116
+ return { session: session === undefined ? null : sessionOut(session) };
117
+ }));
118
+ app.post('/api/v1/counter/sessions', async (request, reply) => {
119
+ const body = (request.body ?? {});
120
+ const shop = idOf(body.shop, 'shop');
121
+ const openingCash = text(body.openingCash) || '0';
122
+ const made = await request.env(async (raw) => {
123
+ const env = raw;
124
+ const created = await env
125
+ .model('pos.session')
126
+ .create([
127
+ { shop: shop, openingCash: orm.toDecimal(openingCash) },
128
+ ]);
129
+ await env.flush();
130
+ const [s] = await env.model('pos.session').browse(created.ids);
131
+ return s === undefined ? null : sessionOut(s);
132
+ }, { write: true });
133
+ return reply.status(201).send({ session: made });
134
+ });
135
+ app.post('/api/v1/counter/orders', async (request, reply) => {
136
+ const input = request.body;
137
+ if (input === null ||
138
+ typeof input !== 'object' ||
139
+ !Array.isArray(input.lines) ||
140
+ !Array.isArray(input.payments))
141
+ throw new HttpProblem(400, 'invalid-body', 'Invalid sale', 'Send the session, the lines and the payments.');
142
+ if (input.lines.length === 0)
143
+ throw new HttpProblem(422, 'empty-sale', 'Nothing to sell', 'Add at least one line.');
144
+ const session = idOf(input.session, 'session');
145
+ const clientId = text(input.clientId);
146
+ const outcome = await request.env(async (raw) => {
147
+ const env = raw;
148
+ // The same sale sent twice (a device back online): the first one, unchanged.
149
+ if (clientId !== '') {
150
+ const [known] = await env.model('pos.order').search({
151
+ where: { session: session, clientId },
152
+ limit: 1,
153
+ });
154
+ if (known !== undefined)
155
+ return { order: await orderOut(env, known.id), repeated: true };
156
+ }
157
+ const [current] = await env
158
+ .model('pos.session')
159
+ .browse([session])
160
+ .load({ shop: true });
161
+ if (current === undefined)
162
+ throw new HttpProblem(404, 'missing-record', 'Not found', 'No such session.');
163
+ const products = await env
164
+ .model('product.product')
165
+ .browse(input.lines.map((l) => idOf(l.product, 'product')))
166
+ .load({ taxes: true });
167
+ const byId = new Map(products.map((p) => [p.id, p]));
168
+ const created = await env.model('pos.order').create([
169
+ {
170
+ session: session,
171
+ pricesIncludeTax: current.shop.pricesIncludeTax === true,
172
+ ...(clientId === '' ? {} : { clientId }),
173
+ ...(input.partner === undefined || input.partner === null
174
+ ? {}
175
+ : { partner: idOf(input.partner, 'partner') }),
176
+ ...(text(input.note) === '' ? {} : { note: text(input.note) }),
177
+ lines: {
178
+ create: input.lines.map((l) => {
179
+ const product = byId.get(idOf(l.product, 'product'));
180
+ if (product === undefined)
181
+ throw new HttpProblem(422, 'unknown-product', 'Unknown product', `No product ${String(l.product)}.`);
182
+ return {
183
+ product: product.id,
184
+ name: product.name,
185
+ quantity: orm.toDecimal(text(l.quantity) || '1'),
186
+ priceUnit: orm.toDecimal(text(l.priceUnit) || product.listPrice.toString()),
187
+ discount: orm.toDecimal(text(l.discount) || '0'),
188
+ taxes: { set: [...product.taxes].map((t) => t.id) },
189
+ };
190
+ }),
191
+ },
192
+ payments: {
193
+ create: input.payments.map((p) => ({
194
+ method: idOf(p.method, 'method'),
195
+ amount: orm.toDecimal(text(p.amount) || '0'),
196
+ ...(text(p.reference) === '' ? {} : { reference: text(p.reference) }),
197
+ })),
198
+ },
199
+ },
200
+ ]);
201
+ await env.flush();
202
+ await env.model('pos.order').browse(created.ids).pay();
203
+ await env.flush();
204
+ return { order: await orderOut(env, created.ids[0]), repeated: false };
205
+ }, { write: true });
206
+ return reply.status(outcome.repeated ? 200 : 201).send(outcome);
207
+ });
208
+ // `POST /api/v1/counter/orders/:id/refund` `{ session, reason, lines?: [{ product, quantity }], method? }`:
209
+ // the sale (or part of it) taken back — a negative sale in the open session, paid back by the method given
210
+ // (the shop's cash by default), linked to the original. Cashiers refund the sales of their own open session;
211
+ // `pos.refund_any` (managers) refunds any sale.
212
+ app.post('/api/v1/counter/orders/:id/refund', async (request, reply) => {
213
+ const id = idOf(request.params.id, 'id');
214
+ const body = (request.body ?? {});
215
+ const session = idOf(body.session, 'session');
216
+ const reason = text(body.reason);
217
+ if (reason === '')
218
+ throw new HttpProblem(422, 'reason-missing', 'Say why', 'A refund needs its reason.');
219
+ const made = await request.env(async (raw) => {
220
+ const env = raw;
221
+ const [original] = await env
222
+ .model('pos.order')
223
+ .browse([id])
224
+ .load({ lines: { taxes: true }, payments: { method: true }, session: true });
225
+ if (original === undefined || original.state !== 'paid')
226
+ throw new HttpProblem(404, 'missing-record', 'Not found', 'No paid sale with this id.');
227
+ const [current] = await env
228
+ .model('pos.session')
229
+ .browse([session])
230
+ .load({ shop: { methods: true } });
231
+ if (current === undefined || current.state !== 'open')
232
+ throw new HttpProblem(409, 'session-closed', 'Session closed', 'Refund in an open session.');
233
+ const mine = original.session.id === current.id;
234
+ let may = mine;
235
+ if (!may) {
236
+ try {
237
+ may = env.access.has('pos.refund_any');
238
+ }
239
+ catch {
240
+ may = false;
241
+ }
242
+ }
243
+ if (!may)
244
+ throw new HttpProblem(403, 'refund-forbidden', 'Not yours to refund', 'Cashiers refund the sales of their own open session; a manager refunds any sale.');
245
+ const wanted = new Map();
246
+ for (const l of body.lines ?? [])
247
+ wanted.set(idOf(l.product, 'product'), text(l.quantity) || '0');
248
+ const lines = [...original.lines]
249
+ .map((l) => {
250
+ const qty = wanted.size === 0 ? l.quantity : orm.toDecimal(wanted.get(l.product.id) ?? '0');
251
+ return { line: l, qty };
252
+ })
253
+ .filter(({ qty }) => qty.gt(0));
254
+ if (lines.length === 0)
255
+ throw new HttpProblem(422, 'nothing-to-refund', 'Nothing to refund', 'Give the quantities taken back.');
256
+ for (const { line, qty } of lines)
257
+ if (qty.gt(line.quantity))
258
+ throw new HttpProblem(422, 'too-much', 'More than sold', `${line.name}: ${qty.toString()} of ${line.quantity.toString()} sold.`);
259
+ const methodId = body.method === undefined
260
+ ? ([...current.shop.methods].find((mm) => mm.kind === 'cash') ??
261
+ [...current.shop.methods][0])?.id
262
+ : idOf(body.method, 'method');
263
+ if (methodId === undefined)
264
+ throw new HttpProblem(409, 'no-method', 'No payment method', 'The shop has no payment method.');
265
+ const created = await env.model('pos.order').create([
266
+ {
267
+ session: session,
268
+ pricesIncludeTax: original.pricesIncludeTax,
269
+ partner: original.partner?.id ?? null,
270
+ refundOf: original.id,
271
+ refundReason: reason,
272
+ note: `Refund of ${original.name}: ${reason}`,
273
+ lines: {
274
+ create: lines.map(({ line, qty }) => ({
275
+ product: line.product.id,
276
+ name: line.name,
277
+ quantity: qty.neg(),
278
+ priceUnit: line.priceUnit,
279
+ discount: line.discount,
280
+ taxes: { set: [...line.taxes].map((t) => t.id) },
281
+ })),
282
+ },
283
+ },
284
+ ]);
285
+ await env.flush();
286
+ const [draft] = await env.model('pos.order').browse(created.ids);
287
+ const total = draft?.total ?? orm.toDecimal(0);
288
+ await env
289
+ .model('pos.order.payment')
290
+ .create([{ order: created.ids[0], method: methodId, amount: total }]);
291
+ await env.flush();
292
+ await env.model('pos.order').browse(created.ids).pay();
293
+ await env.flush();
294
+ return orderOut(env, created.ids[0]);
295
+ }, { write: true });
296
+ return reply.status(201).send({ order: made });
297
+ });
298
+ app.post('/api/v1/counter/sessions/:id/close', async (request) => {
299
+ const id = idOf(request.params.id, 'id');
300
+ const body = (request.body ?? {});
301
+ const closingCash = text(body.closingCash);
302
+ if (closingCash === '')
303
+ throw new HttpProblem(422, 'count-missing', 'Count the cash', 'Send the cash counted in the drawer.');
304
+ return request.env(async (raw) => {
305
+ const env = raw;
306
+ const set = env.model('pos.session').browse([id]);
307
+ await set.write({
308
+ closingCash: orm.toDecimal(closingCash),
309
+ ...(text(body.differenceReason) === ''
310
+ ? {}
311
+ : { differenceReason: text(body.differenceReason) }),
312
+ });
313
+ await env.flush();
314
+ await set.close();
315
+ await env.flush();
316
+ const [s] = await env.model('pos.session').browse([id]);
317
+ return { session: s === undefined ? null : sessionOut(s) };
318
+ }, { write: true });
319
+ });
320
+ }
321
+ const textOrNull = (v) => v === null || v === undefined ? null : String(v);
322
+ /** The receipt in a few lines of text (a text message, an email's plain part). */
323
+ function receiptText(order) {
324
+ const lines = order['lines'].map((l) => `${l.name} × ${l.quantity}: ${l.subtotal}`);
325
+ return [
326
+ `Receipt ${String(order['name'])}`,
327
+ ...lines,
328
+ `Total ${String(order['total'])} (taxes ${String(order['amountTax'])})`,
329
+ `Paid ${String(order['amountPaid'])}, change ${String(order['change'])}`,
330
+ ].join(NEWLINE);
331
+ }
332
+ const NEWLINE = String.fromCharCode(10);
333
+ function sessionOut(raw) {
334
+ const s = raw;
335
+ return {
336
+ id: s['id'],
337
+ name: String(s['name'] ?? ''),
338
+ state: String(s['state'] ?? ''),
339
+ openingCash: String(s['openingCash'] ?? '0'),
340
+ expectedCash: textOrNull(s['expectedCash']),
341
+ closingCash: textOrNull(s['closingCash']),
342
+ closingDifference: textOrNull(s['closingDifference']),
343
+ orderCount: typeof s['orderCount'] === 'number' ? s['orderCount'] : 0,
344
+ total: String(s['total'] ?? '0'),
345
+ };
346
+ }
347
+ async function orderOut(env, id) {
348
+ const [o] = await env.model('pos.order').browse([id]).load({ lines: true, payments: true });
349
+ if (o === undefined)
350
+ throw new HttpProblem(404, 'missing-record', 'Not found', 'No such sale.');
351
+ return {
352
+ id: o.id,
353
+ name: o.name,
354
+ state: o.state,
355
+ total: o.total?.toString() ?? '0',
356
+ amountTax: o.amountTax?.toString() ?? '0',
357
+ amountPaid: o.amountPaid?.toString() ?? '0',
358
+ change: o.change?.toString() ?? '0',
359
+ lines: [...o.lines].map((l) => ({
360
+ product: l.product.id,
361
+ name: l.name,
362
+ quantity: l.quantity.toString(),
363
+ priceUnit: l.priceUnit.toString(),
364
+ subtotal: l.subtotal?.toString() ?? '0',
365
+ })),
366
+ payments: [...o.payments].map((p) => ({ method: p.method.id, amount: p.amount.toString() })),
367
+ };
368
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The register (RFC 0021, Counter): a page for a tablet or a phone. The shop's products as tiles with pictures,
3
+ * search and a barcode box (a scanner types into it; Enter adds the match), the cart, the payment screen with
4
+ * several methods and the change, the receipt to print, the session opened with its cash and closed with its
5
+ * count. Totals here are the register's own arithmetic for the cashier's eye; the server computes the sale.
6
+ */
7
+ import { type PageProps } from '../../ui/index.js';
8
+ import { type ReactElement } from 'react';
9
+ export declare function Register({ api }: PageProps): ReactElement;
@@ -0,0 +1,252 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The register (RFC 0021, Counter): a page for a tablet or a phone. The shop's products as tiles with pictures,
4
+ * search and a barcode box (a scanner types into it; Enter adds the match), the cart, the payment screen with
5
+ * several methods and the change, the receipt to print, the session opened with its cash and closed with its
6
+ * count. Totals here are the register's own arithmetic for the cashier's eye; the server computes the sale.
7
+ */
8
+ import { Alert, Button, Dialog, Input, Label, NetworkError, Spinner, useKosh, } from '../../ui/index.js';
9
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
10
+ import { useEffect, useMemo, useRef, useState } from 'react';
11
+ import { useTranslation } from 'react-i18next';
12
+ const money = (n) => n.toFixed(2);
13
+ /** The line's amount as the customer pays it (taxes added when the shop's prices do not include them). */
14
+ function lineAmount(line, pricesIncludeTax) {
15
+ const gross = Number(line.product.price) * line.quantity;
16
+ if (pricesIncludeTax)
17
+ return gross;
18
+ const percent = line.product.taxes
19
+ .filter((t) => t.amountType === 'percent')
20
+ .reduce((sum, t) => sum + Number(t.rate), 0);
21
+ const fixed = line.product.taxes
22
+ .filter((t) => t.amountType === 'fixed')
23
+ .reduce((sum, t) => sum + Number(t.fixedAmount) * line.quantity, 0);
24
+ return gross + (gross * percent) / 100 + fixed;
25
+ }
26
+ export function Register({ api }) {
27
+ const { t } = useTranslation('pos');
28
+ const { format, offline } = useKosh();
29
+ const queryClient = useQueryClient();
30
+ const [queued, setQueued] = useState(0);
31
+ const shops = useQuery({
32
+ queryKey: ['counter', 'shops'],
33
+ queryFn: () => api.call('GET', '/api/v1/counter/shops').then((r) => r.shops),
34
+ });
35
+ const [shopId, setShopId] = useState(null);
36
+ const shop = (shops.data ?? []).find((s) => s.id === shopId) ??
37
+ (shops.data?.length === 1 ? shops.data[0] : undefined);
38
+ const session = useQuery({
39
+ queryKey: ['counter', 'session', shop?.id],
40
+ queryFn: () => api
41
+ .call('GET', `/api/v1/counter/session?shop=${shop?.id ?? 0}`)
42
+ .then((r) => r.session),
43
+ enabled: shop !== undefined,
44
+ });
45
+ const catalogue = useQuery({
46
+ queryKey: ['counter', 'catalogue', shop?.id],
47
+ queryFn: () => api
48
+ .call('GET', `/api/v1/counter/catalogue?shop=${shop?.id ?? 0}`)
49
+ .then((r) => r.products),
50
+ enabled: shop !== undefined,
51
+ });
52
+ const [query, setQuery] = useState('');
53
+ const [cart, setCart] = useState([]);
54
+ const [paying, setPaying] = useState(false);
55
+ const [closing, setClosing] = useState(false);
56
+ const [opening, setOpening] = useState('0');
57
+ const [closingCash, setClosingCash] = useState('');
58
+ const [reason, setReason] = useState('');
59
+ const [payments, setPayments] = useState({});
60
+ const [lastSale, setLastSale] = useState(null);
61
+ const [error, setError] = useState(null);
62
+ const clientId = useRef(crypto.randomUUID());
63
+ const search = useRef(null);
64
+ const products = catalogue.data ?? [];
65
+ const shown = useMemo(() => {
66
+ const q = query.trim().toLowerCase();
67
+ if (q === '')
68
+ return products.slice(0, 200);
69
+ return products
70
+ .filter((p) => p.name.toLowerCase().includes(q) ||
71
+ (p.code ?? '').toLowerCase().includes(q) ||
72
+ (p.barcode ?? '').toLowerCase() === q)
73
+ .slice(0, 200);
74
+ }, [products, query]);
75
+ const total = cart.reduce((sum, l) => sum + lineAmount(l, shop?.pricesIncludeTax ?? true), 0);
76
+ const paid = Object.values(payments).reduce((sum, v) => sum + (Number(v) || 0), 0);
77
+ const change = Math.max(0, paid - total);
78
+ const add = (p) => {
79
+ setCart((lines) => {
80
+ const i = lines.findIndex((l) => l.product.id === p.id);
81
+ if (i < 0)
82
+ return [...lines, { product: p, quantity: 1 }];
83
+ const next = [...lines];
84
+ next[i] = { product: p, quantity: (next[i]?.quantity ?? 0) + 1 };
85
+ return next;
86
+ });
87
+ };
88
+ const setQuantity = (id, quantity) => setCart((lines) => lines
89
+ .map((l) => (l.product.id === id ? { product: l.product, quantity } : l))
90
+ .filter((l) => l.quantity > 0 || l.quantity === 0));
91
+ const removeLine = (id) => setCart((lines) => lines.filter((l) => l.product.id !== id));
92
+ // A scanner types the barcode and sends Enter: the exact match goes to the cart, the box clears.
93
+ const onSearchKey = (key) => {
94
+ if (key !== 'Enter')
95
+ return;
96
+ const q = query.trim().toLowerCase();
97
+ const hit = products.find((p) => (p.barcode ?? '').toLowerCase() === q) ??
98
+ (shown.length === 1 ? shown[0] : undefined);
99
+ if (hit !== undefined) {
100
+ add(hit);
101
+ setQuery('');
102
+ }
103
+ };
104
+ useEffect(() => {
105
+ search.current?.focus();
106
+ }, []);
107
+ const open = useMutation({
108
+ mutationFn: () => api.call('POST', '/api/v1/counter/sessions', {
109
+ shop: shop?.id,
110
+ openingCash: opening,
111
+ }),
112
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['counter', 'session'] }),
113
+ onError: (e) => setError(e instanceof Error ? e.message : String(e)),
114
+ });
115
+ const sell = useMutation({
116
+ mutationFn: async () => {
117
+ const body = {
118
+ session: session.data?.id,
119
+ clientId: clientId.current,
120
+ lines: cart.map((l) => ({ product: l.product.id, quantity: String(l.quantity) })),
121
+ payments: Object.entries(payments)
122
+ .filter(([, amount]) => Number(amount) > 0)
123
+ .map(([method, amount]) => ({ method: Number(method), amount })),
124
+ };
125
+ try {
126
+ const r = await api.call('POST', '/api/v1/counter/orders', body);
127
+ return { order: r.order, queued: false };
128
+ }
129
+ catch (error) {
130
+ // Offline: the sale waits on the device, sent in order when the network is back, accepted once by
131
+ // its device reference (offline/queue.ts).
132
+ if (!(error instanceof NetworkError) || offline === undefined)
133
+ throw error;
134
+ await offline.add({
135
+ kind: 'request',
136
+ method: 'POST',
137
+ path: '/api/v1/counter/orders',
138
+ model: 'pos.order',
139
+ modelLabel: t('cart'),
140
+ recordId: null,
141
+ title: `${t('cart')} ${format.decimal(money(total))}`,
142
+ values: body,
143
+ etag: null,
144
+ });
145
+ return { order: null, queued: true };
146
+ }
147
+ },
148
+ onSuccess: (r) => {
149
+ if (r.queued)
150
+ setQueued((n) => n + 1);
151
+ setLastSale(r.order);
152
+ setCart([]);
153
+ setPayments({});
154
+ setPaying(false);
155
+ setError(null);
156
+ clientId.current = crypto.randomUUID();
157
+ void queryClient.invalidateQueries({ queryKey: ['counter', 'session'] });
158
+ search.current?.focus();
159
+ },
160
+ onError: (e) => setError(e instanceof Error ? e.message : String(e)),
161
+ });
162
+ const close = useMutation({
163
+ mutationFn: () => api.call('POST', `/api/v1/counter/sessions/${session.data?.id}/close`, {
164
+ closingCash,
165
+ differenceReason: reason,
166
+ }),
167
+ onSuccess: (r) => {
168
+ setClosing(false);
169
+ setLastSale(null);
170
+ setError(null);
171
+ void queryClient.invalidateQueries({ queryKey: ['counter', 'session'] });
172
+ setClosedInfo(r.session);
173
+ },
174
+ onError: (e) => setError(e instanceof Error ? e.message : String(e)),
175
+ });
176
+ const [closedInfo, setClosedInfo] = useState(null);
177
+ const [sendTo, setSendTo] = useState('');
178
+ const [refundReason, setRefundReason] = useState('');
179
+ // The whole last sale taken back, paid back in cash, with its reason.
180
+ const refund = useMutation({
181
+ mutationFn: (id) => api.call('POST', `/api/v1/counter/orders/${id}/refund`, {
182
+ session: session.data?.id,
183
+ reason: refundReason.trim(),
184
+ }),
185
+ onSuccess: (r) => {
186
+ setLastSale(r.order);
187
+ setRefundReason('');
188
+ setError(null);
189
+ void queryClient.invalidateQueries({ queryKey: ['counter', 'session'] });
190
+ },
191
+ onError: (e) => setError(e instanceof Error ? e.message : String(e)),
192
+ });
193
+ const [sentNote, setSentNote] = useState(null);
194
+ // The receipt by email or text: an address with an @ is an email, anything else a phone number.
195
+ const sendReceipt = useMutation({
196
+ mutationFn: (id) => api.call('POST', `/api/v1/counter/orders/${id}/receipt`, {
197
+ ...(sendTo.includes('@') ? { email: sendTo.trim() } : { phone: sendTo.trim() }),
198
+ }),
199
+ onSuccess: (r) => {
200
+ setSentNote(t('sent', { how: r.sent.join(', ') }));
201
+ setSendTo('');
202
+ },
203
+ onError: (e) => setError(e instanceof Error ? e.message : String(e)),
204
+ });
205
+ const printReceipt = async (sale) => {
206
+ try {
207
+ const { templates } = await api.call('GET', '/api/v1/models/pos.order/documents');
208
+ const template = templates.find((tpl) => tpl.name === 'Receipt') ??
209
+ templates.find((tpl) => tpl.scope === 'record');
210
+ if (template === undefined)
211
+ throw new Error(t('noReceiptTemplate'));
212
+ const file = await api.download(`/api/v1/models/pos.order/documents/${template.id}`, {
213
+ ids: [sale.id],
214
+ });
215
+ const url = URL.createObjectURL(file.blob);
216
+ window.open(url, '_blank', 'noopener');
217
+ setTimeout(() => URL.revokeObjectURL(url), 60_000);
218
+ }
219
+ catch (e) {
220
+ setError(e instanceof Error ? e.message : String(e));
221
+ }
222
+ };
223
+ if (shops.isPending)
224
+ return _jsx(Spinner, { label: t('loading') });
225
+ if ((shops.data ?? []).length === 0)
226
+ return _jsx(Alert, { tone: "info", children: t('noShops') });
227
+ if (shop === undefined)
228
+ return (_jsxs("div", { className: "grid gap-3", children: [_jsx("h2", { className: "text-base font-semibold", children: t('chooseShop') }), _jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(12rem,1fr))] gap-3", children: (shops.data ?? []).map((s) => (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setShopId(s.id), children: s.name }, s.id))) })] }));
229
+ if (session.isPending)
230
+ return _jsx(Spinner, { label: t('loading') });
231
+ if (session.data === null || session.data === undefined)
232
+ return (_jsxs("div", { className: "mx-auto grid max-w-sm gap-3", children: [_jsx("h2", { className: "text-base font-semibold", children: t('openSession', { shop: shop.name }) }), closedInfo === null ? null : (_jsx(Alert, { tone: "info", children: t('closedSummary', {
233
+ name: closedInfo.name,
234
+ total: format.decimal(closedInfo.total),
235
+ difference: format.decimal(closedInfo.closingDifference ?? '0'),
236
+ }) })), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: "counter-opening", children: t('openingCash') }), _jsx(Input, { id: "counter-opening", inputMode: "decimal", value: opening, onChange: (e) => setOpening(e.target.value) })] }), error === null ? null : _jsx(Alert, { children: error }), _jsx(Button, { variant: "primary", busy: open.isPending, onClick: () => open.mutate(), children: t('open') })] }));
237
+ const current = session.data;
238
+ return (_jsxs("div", { className: "grid gap-3 lg:grid-cols-[1fr_22rem]", children: [_jsxs("section", { "aria-label": t('products'), className: "grid content-start gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Input, { ref: search, "aria-label": t('search'), placeholder: t('searchHint'), value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => onSearchKey(e.key), className: "max-w-md" }), _jsx("span", { className: "ms-auto text-xs text-muted-foreground", children: t('sessionLine', {
239
+ name: current.name,
240
+ count: current.orderCount,
241
+ total: format.decimal(current.total),
242
+ }) }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setClosing(true), children: t('closeSession') })] }), catalogue.isPending ? _jsx(Spinner, { label: t('loading') }) : null, _jsx("ul", { className: "grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2", children: shown.map((p) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "grid w-full gap-1 rounded-lg border bg-surface p-2 text-start hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring", onClick: () => add(p), children: [p.image === null ? (_jsx("span", { className: "aspect-square w-full rounded bg-muted", "aria-hidden": true })) : (_jsx("img", { src: `/api/v1/attachments/${p.image}/download?inline=true&w=256`, alt: "", className: "aspect-square w-full rounded object-cover" })), _jsx("span", { className: "line-clamp-2 text-sm font-medium", children: p.name }), _jsx("span", { className: "tabular-nums text-xs text-muted-foreground", children: format.decimal(p.price) })] }) }, p.id))) })] }), _jsxs("aside", { "aria-label": t('cart'), className: "grid content-start gap-3 rounded-lg border bg-surface p-3", children: [_jsx("h2", { className: "text-sm font-semibold", children: t('cart') }), cart.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('emptyCart') })) : null, _jsx("ul", { className: "grid gap-2", children: cart.map((l) => (_jsxs("li", { className: "grid grid-cols-[1fr_auto_auto] items-center gap-2 text-sm", children: [_jsx("span", { className: "truncate", children: l.product.name }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", "aria-label": t('less', { name: l.product.name }), onClick: () => l.quantity <= 1
243
+ ? removeLine(l.product.id)
244
+ : setQuantity(l.product.id, l.quantity - 1), children: "\u2212" }), _jsx("input", { "aria-label": t('quantityOf', { name: l.product.name }), inputMode: "decimal", className: "w-14 rounded border bg-surface px-1 text-center tabular-nums", value: String(l.quantity), onChange: (e) => {
245
+ const q = Number(e.target.value);
246
+ if (Number.isFinite(q) && q >= 0)
247
+ setQuantity(l.product.id, q);
248
+ } }), _jsx(Button, { size: "icon", variant: "ghost", "aria-label": t('more', { name: l.product.name }), onClick: () => setQuantity(l.product.id, l.quantity + 1), children: "+" })] }), _jsx("span", { className: "tabular-nums", children: format.decimal(money(lineAmount(l, shop.pricesIncludeTax))) })] }, l.product.id))) }), _jsxs("div", { className: "flex items-baseline justify-between border-t pt-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t('total') }), _jsx("span", { className: "text-xl font-semibold tabular-nums", "aria-live": "polite", children: format.decimal(money(total)) })] }), _jsx(Button, { variant: "primary", disabled: cart.length === 0, onClick: () => setPaying(true), children: t('pay') }), queued === 0 ? null : (_jsx("p", { role: "status", className: "text-xs text-muted-foreground", children: t('queued', { count: queued }) })), lastSale === null ? null : (_jsxs("div", { role: "status", className: "grid gap-1 rounded-md border bg-muted/40 p-2 text-sm", children: [_jsx("span", { children: t('soldLine', { name: lastSale.name, total: format.decimal(lastSale.total) }) }), Number(lastSale.change) > 0 ? (_jsx("span", { className: "font-semibold", children: t('changeLine', { change: format.decimal(lastSale.change) }) })) : null, _jsx(Button, { size: "sm", variant: "secondary", onClick: () => void printReceipt(lastSale), children: t('printReceipt') }), _jsxs("div", { className: "grid grid-cols-[1fr_auto] gap-1", children: [_jsx(Input, { "aria-label": t('sendTo'), placeholder: t('sendToHint'), value: sendTo, onChange: (e) => setSendTo(e.target.value) }), _jsx(Button, { size: "sm", variant: "ghost", busy: sendReceipt.isPending, disabled: sendTo.trim() === '', onClick: () => sendReceipt.mutate(lastSale.id), children: t('send') })] }), sentNote === null ? null : (_jsx("span", { className: "text-xs text-muted-foreground", children: sentNote })), _jsxs("div", { className: "grid grid-cols-[1fr_auto] gap-1", children: [_jsx(Input, { "aria-label": t('refundReason'), placeholder: t('refundReasonHint'), value: refundReason, onChange: (e) => setRefundReason(e.target.value) }), _jsx(Button, { size: "sm", variant: "ghost", busy: refund.isPending, disabled: refundReason.trim() === '', onClick: () => refund.mutate(lastSale.id), children: t('refund') })] })] })), error === null ? null : _jsx(Alert, { children: error })] }), _jsx(Dialog, { open: paying, onOpenChange: setPaying, title: t('payTitle', { total: format.decimal(money(total)) }), children: _jsxs("div", { className: "grid gap-3", children: [shop.methods.map((m) => (_jsxs("div", { className: "grid grid-cols-[1fr_8rem] items-center gap-2", children: [_jsx(Label, { htmlFor: `pay-${m.id}`, children: m.name }), _jsx(Input, { id: `pay-${m.id}`, inputMode: "decimal", value: payments[m.id] ?? '', onFocus: () => {
249
+ if ((payments[m.id] ?? '') === '' && paid < total)
250
+ setPayments({ ...payments, [m.id]: money(total - paid) });
251
+ }, onChange: (e) => setPayments({ ...payments, [m.id]: e.target.value }) })] }, m.id))), _jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { children: t('paid') }), _jsx("span", { className: "tabular-nums", children: format.decimal(money(paid)) })] }), _jsxs("div", { className: "flex justify-between text-sm font-semibold", children: [_jsx("span", { children: t('change') }), _jsx("span", { className: "tabular-nums", children: format.decimal(money(change)) })] }), error === null ? null : _jsx(Alert, { children: error }), _jsx(Button, { variant: "primary", disabled: paid + 0.004 < total, busy: sell.isPending, onClick: () => sell.mutate(), children: t('confirmSale') })] }) }), _jsx(Dialog, { open: closing, onOpenChange: setClosing, title: t('closeTitle', { name: current.name }), children: _jsxs("div", { className: "grid gap-3", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t('expectedCash', { amount: format.decimal(current.expectedCash ?? '0') }) }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: "counter-closing", children: t('closingCash') }), _jsx(Input, { id: "counter-closing", inputMode: "decimal", value: closingCash, onChange: (e) => setClosingCash(e.target.value) })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: "counter-reason", children: t('differenceReason') }), _jsx(Input, { id: "counter-reason", value: reason, onChange: (e) => setReason(e.target.value) })] }), error === null ? null : _jsx(Alert, { children: error }), _jsx(Button, { variant: "primary", busy: close.isPending, onClick: () => close.mutate(), children: t('closeSession') })] }) })] }));
252
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * The web contribution of `shutro-erp/pos`: the register page.
4
+ */
5
+ import { type Registry } from '../../ui/index.js';
6
+ import type { i18n } from 'i18next';
7
+ import { messages } from './messages.js';
8
+ export { Register } from './Register.js';
9
+ /** Registers the page (and the messages in `i18n`, when given). */
10
+ export declare function registerPosWeb(registry: Registry, i18n?: i18n): Registry;
11
+ export { messages as posMessages };