shutro-erp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +17 -10
  2. package/dist/account_portal/.kosh/bindings.d.ts +22 -0
  3. package/dist/account_portal/.kosh/bindings.js +15 -0
  4. package/dist/account_portal/.kosh/schema.d.ts +1350 -0
  5. package/dist/account_portal/index.d.ts +3 -0
  6. package/dist/account_portal/index.js +73 -0
  7. package/dist/account_portal/module.d.ts +5983 -0
  8. package/dist/account_portal/module.js +18 -0
  9. package/dist/accounting/.kosh/bindings.js +1 -1
  10. package/dist/accounting/.kosh/schema.d.ts +142 -9
  11. package/dist/accounting/chart.d.ts +40 -8
  12. package/dist/accounting/chart.js +124 -30
  13. package/dist/accounting/closing.d.ts +6 -1
  14. package/dist/accounting/closing.js +5 -1
  15. package/dist/accounting/demo.js +6 -5
  16. package/dist/accounting/index.d.ts +3 -3
  17. package/dist/accounting/index.js +200 -23
  18. package/dist/accounting/module.d.ts +3326 -1086
  19. package/dist/accounting/module.js +408 -66
  20. package/dist/accounting/payments.d.ts +25 -0
  21. package/dist/accounting/payments.js +96 -4
  22. package/dist/accounting/web/RegisterPayment.d.ts +3 -2
  23. package/dist/accounting/web/RegisterPayment.js +39 -6
  24. package/dist/accounting/web/SettingsPage.js +5 -1
  25. package/dist/accounting/web/messages.js +10 -0
  26. package/dist/cli/index.d.ts +1 -1
  27. package/dist/cli/index.js +1 -1
  28. package/dist/cli/init.d.ts +5 -0
  29. package/dist/cli/init.js +195 -53
  30. package/dist/cli/launch.d.ts +16 -0
  31. package/dist/cli/launch.js +138 -0
  32. package/dist/cli/prompt.d.ts +35 -0
  33. package/dist/cli/prompt.js +46 -0
  34. package/dist/cli/templates.d.ts +11 -3
  35. package/dist/cli/templates.js +32 -7
  36. package/dist/cli/tui.d.ts +7 -0
  37. package/dist/cli/tui.js +254 -0
  38. package/dist/cli/ui.d.ts +25 -5
  39. package/dist/cli/ui.js +49 -9
  40. package/dist/contacts/.kosh/bindings.js +1 -1
  41. package/dist/contacts/.kosh/schema.d.ts +3 -1
  42. package/dist/contacts/index.js +24 -1
  43. package/dist/contacts/module.d.ts +42 -0
  44. package/dist/contacts/module.js +3 -1
  45. package/dist/import/convert.d.ts +55 -0
  46. package/dist/import/convert.js +224 -0
  47. package/dist/import/csv.d.ts +28 -0
  48. package/dist/import/csv.js +115 -0
  49. package/dist/import/index.d.ts +33 -0
  50. package/dist/import/index.js +60 -0
  51. package/dist/import/mapping.d.ts +15 -0
  52. package/dist/import/mapping.js +33 -0
  53. package/dist/import/match.d.ts +24 -0
  54. package/dist/import/match.js +60 -0
  55. package/dist/import/models.d.ts +2 -0
  56. package/dist/import/models.js +102 -0
  57. package/dist/import/routes.d.ts +13 -0
  58. package/dist/import/routes.js +392 -0
  59. package/dist/import/run.d.ts +131 -0
  60. package/dist/import/run.js +568 -0
  61. package/dist/import/service.d.ts +153 -0
  62. package/dist/import/service.js +367 -0
  63. package/dist/import/web/ImportButton.d.ts +7 -0
  64. package/dist/import/web/ImportButton.js +31 -0
  65. package/dist/import/web/ImportWizard.d.ts +75 -0
  66. package/dist/import/web/ImportWizard.js +217 -0
  67. package/dist/import/web/index.d.ts +13 -0
  68. package/dist/import/web/index.js +18 -0
  69. package/dist/import/web/messages.d.ts +2 -0
  70. package/dist/import/web/messages.js +139 -0
  71. package/dist/import/worker.d.ts +3 -0
  72. package/dist/import/worker.js +52 -0
  73. package/dist/import/xlsx.d.ts +20 -0
  74. package/dist/import/xlsx.js +192 -0
  75. package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
  76. package/dist/l10n_bd/.kosh/bindings.js +15 -0
  77. package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
  78. package/dist/l10n_bd/documents.d.ts +8 -0
  79. package/dist/l10n_bd/documents.js +183 -0
  80. package/dist/l10n_bd/index.d.ts +8 -0
  81. package/dist/l10n_bd/index.js +156 -0
  82. package/dist/l10n_bd/module.d.ts +5705 -0
  83. package/dist/l10n_bd/module.js +163 -0
  84. package/dist/l10n_bd/mushak.d.ts +67 -0
  85. package/dist/l10n_bd/mushak.js +139 -0
  86. package/dist/l10n_bd/routes.d.ts +3 -0
  87. package/dist/l10n_bd/routes.js +205 -0
  88. package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
  89. package/dist/l10n_bd/web/MushakPage.js +53 -0
  90. package/dist/l10n_bd/web/index.d.ts +13 -0
  91. package/dist/l10n_bd/web/index.js +18 -0
  92. package/dist/l10n_bd/web/messages.d.ts +2 -0
  93. package/dist/l10n_bd/web/messages.js +101 -0
  94. package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
  95. package/dist/l10n_in/.kosh/bindings.js +15 -0
  96. package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
  97. package/dist/l10n_in/documents.d.ts +8 -0
  98. package/dist/l10n_in/documents.js +114 -0
  99. package/dist/l10n_in/einvoice.d.ts +253 -0
  100. package/dist/l10n_in/einvoice.js +485 -0
  101. package/dist/l10n_in/gstr.d.ts +109 -0
  102. package/dist/l10n_in/gstr.js +430 -0
  103. package/dist/l10n_in/index.d.ts +20 -0
  104. package/dist/l10n_in/index.js +232 -0
  105. package/dist/l10n_in/module.d.ts +7823 -0
  106. package/dist/l10n_in/module.js +427 -0
  107. package/dist/l10n_in/routes-einvoice.d.ts +15 -0
  108. package/dist/l10n_in/routes-einvoice.js +124 -0
  109. package/dist/l10n_in/routes.d.ts +17 -0
  110. package/dist/l10n_in/routes.js +282 -0
  111. package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
  112. package/dist/l10n_in/web/EInvoicePanel.js +63 -0
  113. package/dist/l10n_in/web/GstPage.d.ts +7 -0
  114. package/dist/l10n_in/web/GstPage.js +57 -0
  115. package/dist/l10n_in/web/GstSettings.d.ts +10 -0
  116. package/dist/l10n_in/web/GstSettings.js +36 -0
  117. package/dist/l10n_in/web/index.d.ts +15 -0
  118. package/dist/l10n_in/web/index.js +22 -0
  119. package/dist/l10n_in/web/messages.d.ts +2 -0
  120. package/dist/l10n_in/web/messages.js +166 -0
  121. package/dist/modules-sdk/demo.d.ts +2 -0
  122. package/dist/modules-sdk/demo.js +7 -3
  123. package/dist/modules-sdk/index.d.ts +2 -2
  124. package/dist/modules-sdk/index.js +1 -1
  125. package/dist/modules-sdk/manifest.d.ts +132 -0
  126. package/dist/modules-sdk/manifest.js +22 -0
  127. package/dist/notify/inbox.js +21 -0
  128. package/dist/notify/messages.js +4 -0
  129. package/dist/notify/models.d.ts +1 -1
  130. package/dist/notify/models.js +2 -0
  131. package/dist/notify/web/messages.js +4 -0
  132. package/dist/portal/.kosh/bindings.d.ts +22 -0
  133. package/dist/portal/.kosh/bindings.js +15 -0
  134. package/dist/portal/.kosh/schema.d.ts +346 -0
  135. package/dist/portal/announce.d.ts +16 -0
  136. package/dist/portal/announce.js +117 -0
  137. package/dist/portal/documents.d.ts +17 -0
  138. package/dist/portal/documents.js +61 -0
  139. package/dist/portal/index.d.ts +20 -0
  140. package/dist/portal/index.js +65 -0
  141. package/dist/portal/module.d.ts +1405 -0
  142. package/dist/portal/module.js +84 -0
  143. package/dist/portal/routes-documents.d.ts +2 -0
  144. package/dist/portal/routes-documents.js +391 -0
  145. package/dist/portal/routes.d.ts +13 -0
  146. package/dist/portal/routes.js +313 -0
  147. package/dist/portal/rules.d.ts +11 -0
  148. package/dist/portal/rules.js +25 -0
  149. package/dist/portal/web/PortalAccess.d.ts +8 -0
  150. package/dist/portal/web/PortalAccess.js +66 -0
  151. package/dist/portal/web/index.d.ts +12 -0
  152. package/dist/portal/web/index.js +17 -0
  153. package/dist/portal/web/messages.d.ts +2 -0
  154. package/dist/portal/web/messages.js +33 -0
  155. package/dist/product/.kosh/bindings.js +1 -1
  156. package/dist/product/.kosh/schema.d.ts +12 -3
  157. package/dist/product/demo.js +6 -5
  158. package/dist/product/index.d.ts +1 -0
  159. package/dist/product/index.js +17 -4
  160. package/dist/product/module.d.ts +346 -14
  161. package/dist/product/module.js +48 -18
  162. package/dist/product/taxes.d.ts +59 -0
  163. package/dist/product/taxes.js +61 -0
  164. package/dist/project/.kosh/bindings.js +1 -1
  165. package/dist/project/.kosh/schema.d.ts +3 -1
  166. package/dist/project/module.d.ts +20 -0
  167. package/dist/project_portal/.kosh/bindings.d.ts +22 -0
  168. package/dist/project_portal/.kosh/bindings.js +15 -0
  169. package/dist/project_portal/.kosh/schema.d.ts +671 -0
  170. package/dist/project_portal/index.d.ts +9 -0
  171. package/dist/project_portal/index.js +42 -0
  172. package/dist/project_portal/module.d.ts +2142 -0
  173. package/dist/project_portal/module.js +22 -0
  174. package/dist/purchase/.kosh/bindings.js +1 -1
  175. package/dist/purchase/.kosh/schema.d.ts +15 -4
  176. package/dist/purchase/demo.js +3 -3
  177. package/dist/purchase/index.js +1 -1
  178. package/dist/purchase/module.d.ts +232 -8
  179. package/dist/purchase/module.js +51 -9
  180. package/dist/purchase_account/.kosh/bindings.js +1 -1
  181. package/dist/purchase_account/.kosh/schema.d.ts +144 -10
  182. package/dist/purchase_account/module.d.ts +2008 -641
  183. package/dist/purchase_account/module.js +2 -2
  184. package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
  185. package/dist/purchase_portal/.kosh/bindings.js +15 -0
  186. package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
  187. package/dist/purchase_portal/index.d.ts +9 -0
  188. package/dist/purchase_portal/index.js +53 -0
  189. package/dist/purchase_portal/module.d.ts +2758 -0
  190. package/dist/purchase_portal/module.js +18 -0
  191. package/dist/reporting/documents.js +20 -3
  192. package/dist/reporting/index.d.ts +1 -0
  193. package/dist/reporting/index.js +1 -0
  194. package/dist/reporting/routes-documents.js +9 -2
  195. package/dist/reporting/words.d.ts +11 -0
  196. package/dist/reporting/words.js +229 -0
  197. package/dist/sale_account/.kosh/bindings.js +1 -1
  198. package/dist/sale_account/.kosh/schema.d.ts +144 -10
  199. package/dist/sale_account/module.d.ts +2008 -641
  200. package/dist/sale_account/module.js +3 -3
  201. package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
  202. package/dist/sale_portal/.kosh/bindings.js +15 -0
  203. package/dist/sale_portal/.kosh/schema.d.ts +736 -0
  204. package/dist/sale_portal/index.d.ts +9 -0
  205. package/dist/sale_portal/index.js +71 -0
  206. package/dist/sale_portal/module.d.ts +2718 -0
  207. package/dist/sale_portal/module.js +19 -0
  208. package/dist/sale_project/.kosh/bindings.js +1 -1
  209. package/dist/sale_project/.kosh/schema.d.ts +15 -4
  210. package/dist/sale_project/module.d.ts +390 -10
  211. package/dist/sales/.kosh/bindings.js +1 -1
  212. package/dist/sales/.kosh/schema.d.ts +15 -4
  213. package/dist/sales/demo.js +3 -3
  214. package/dist/sales/index.js +67 -1
  215. package/dist/sales/module.d.ts +232 -8
  216. package/dist/sales/module.js +51 -12
  217. package/dist/server/auth/base.js +2 -0
  218. package/dist/server/auth/service.d.ts +2 -0
  219. package/dist/server/auth/service.js +7 -1
  220. package/dist/server/index.d.ts +1 -1
  221. package/dist/server/index.js +1 -1
  222. package/dist/server/server.js +26 -1
  223. package/dist/ui/api.d.ts +2 -0
  224. package/dist/ui/app.js +1 -1
  225. package/dist/ui/context.d.ts +3 -1
  226. package/dist/ui/context.js +2 -2
  227. package/dist/ui/index.d.ts +1 -1
  228. package/dist/ui/index.js +1 -1
  229. package/dist/ui/lib/format.d.ts +2 -0
  230. package/dist/ui/lib/format.js +3 -1
  231. package/dist/ui/views/FormView.js +19 -6
  232. package/package.json +50 -2
@@ -0,0 +1,217 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The import screen (RFC 0014 §5.8): three steps in a wide dialog. The file (with a sample workbook to start from);
4
+ * the columns (each with its first values and the field it fills, the mode and key, what to do with errors, saved
5
+ * mappings); the result (counts, the errors with their row and column, the rejected rows, undo). Large files run
6
+ * in the background and the dialog follows their progress. The server reads, converts and checks everything; the
7
+ * screen only shows what it says.
8
+ */
9
+ import { Alert, ApiError, Button, Checkbox, Dialog, Input, Label, Select, } from '../../ui/index.js';
10
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
11
+ import { Download, Save, Undo2, Upload } from 'lucide-react';
12
+ import { useId, useState } from 'react';
13
+ import { useTranslation } from 'react-i18next';
14
+ const ACCEPT = '.csv,.xlsx,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
15
+ const SKIP = '__skip';
16
+ function saveFile(blob, filename) {
17
+ const url = URL.createObjectURL(blob);
18
+ const a = document.createElement('a');
19
+ a.href = url;
20
+ a.download = filename;
21
+ a.click();
22
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
23
+ }
24
+ const message = (error) => (error instanceof ApiError ? error.message : String(error));
25
+ const cell = (value) => value === null || value === undefined ? '' : typeof value === 'string' ? value : String(value);
26
+ export function ImportWizard({ api, model, open, onOpenChange, }) {
27
+ const { t } = useTranslation('import');
28
+ const id = useId();
29
+ const queryClient = useQueryClient();
30
+ const [file, setFile] = useState(null);
31
+ const [session, setSession] = useState(null);
32
+ const [step, setStep] = useState('file');
33
+ const [errorColumn, setErrorColumn] = useState('all');
34
+ const [mappingName, setMappingName] = useState('');
35
+ const [share, setShare] = useState(false);
36
+ const [savedName, setSavedName] = useState(null);
37
+ const [undone, setUndone] = useState(null);
38
+ const mappings = useQuery({
39
+ queryKey: ['import', 'mappings', model],
40
+ queryFn: () => api.call('GET', `/api/v1/import-mappings?model=${encodeURIComponent(model)}`),
41
+ enabled: open,
42
+ });
43
+ // A background run: follow it until it ends.
44
+ const running = session?.state === 'running';
45
+ useQuery({
46
+ queryKey: ['import', 'session', session?.id],
47
+ queryFn: async () => {
48
+ const next = await api.call('GET', `/api/v1/imports/${session?.id}`);
49
+ setSession(next);
50
+ if (next.state !== 'running') {
51
+ setStep('result');
52
+ if (next.state === 'done')
53
+ void queryClient.invalidateQueries();
54
+ }
55
+ return next;
56
+ },
57
+ enabled: running,
58
+ refetchInterval: running ? 1000 : false,
59
+ });
60
+ const reset = () => {
61
+ setFile(null);
62
+ setSession(null);
63
+ setStep('file');
64
+ setUndone(null);
65
+ setErrorColumn('all');
66
+ };
67
+ const upload = useMutation({
68
+ mutationFn: (chosen) => api.upload(`/api/v1/imports?model=${encodeURIComponent(model)}`, chosen, chosen.name),
69
+ onSuccess: (created) => {
70
+ setSession(created);
71
+ setStep('columns');
72
+ },
73
+ });
74
+ const map = useMutation({
75
+ mutationFn: (change) => api.call('PUT', `/api/v1/imports/${session?.id}/mapping`, {
76
+ columns: (change.columns ?? session?.columns ?? []).map((c) => ({
77
+ index: c.index,
78
+ field: c.field,
79
+ })),
80
+ options: change.options ?? session?.options ?? {},
81
+ }),
82
+ onSuccess: setSession,
83
+ });
84
+ const apply = useMutation({
85
+ mutationFn: (mapping) => api.call('POST', `/api/v1/imports/${session?.id}/apply-mapping`, { mapping }),
86
+ onSuccess: setSession,
87
+ });
88
+ const save = useMutation({
89
+ mutationFn: () => api.call('POST', `/api/v1/imports/${session?.id}/save-mapping`, {
90
+ name: mappingName.trim(),
91
+ shared: share,
92
+ }),
93
+ onSuccess: () => {
94
+ setSavedName(mappingName.trim());
95
+ setMappingName('');
96
+ void mappings.refetch();
97
+ },
98
+ });
99
+ const run = useMutation({
100
+ mutationFn: async (dry) => {
101
+ const path = `/api/v1/imports/${session?.id}/${dry ? 'dry-run' : 'commit'}`;
102
+ try {
103
+ return { dry, answer: await api.call('POST', path, {}) };
104
+ }
105
+ catch (error) {
106
+ // A commit that stopped on errors answers 422 with its result.
107
+ if (error instanceof ApiError && error.status === 422 && !dry) {
108
+ const again = await api.call('GET', `/api/v1/imports/${session?.id}`);
109
+ return { dry, answer: again };
110
+ }
111
+ throw error;
112
+ }
113
+ },
114
+ onSuccess: ({ answer }) => {
115
+ if ('state' in answer) {
116
+ setSession(answer);
117
+ if (answer.state !== 'running')
118
+ setStep('result');
119
+ return;
120
+ }
121
+ void api.call('GET', `/api/v1/imports/${session?.id}`).then((next) => {
122
+ setSession(next);
123
+ setStep('result');
124
+ if (next.state === 'done')
125
+ void queryClient.invalidateQueries();
126
+ });
127
+ },
128
+ });
129
+ const undo = useMutation({
130
+ mutationFn: () => api.call('POST', `/api/v1/imports/${session?.id}/undo`, {}),
131
+ onSuccess: async (result) => {
132
+ setUndone(result);
133
+ setSession(await api.call('GET', `/api/v1/imports/${session?.id}`));
134
+ void queryClient.invalidateQueries();
135
+ },
136
+ });
137
+ const download = useMutation({
138
+ mutationFn: async (path) => {
139
+ const { blob, filename } = await api.download(path, undefined, 'GET');
140
+ saveFile(blob, filename);
141
+ },
142
+ });
143
+ const fields = session?.fields ?? [];
144
+ const mapped = new Set((session?.columns ?? []).flatMap((c) => (c.field === null ? [] : [c.field])));
145
+ const options = session?.options ?? {};
146
+ const mode = options.mode ?? 'create';
147
+ const missing = mode === 'create'
148
+ ? fields.filter((f) => f.required && f.line === undefined && !mapped.has(f.key))
149
+ : [];
150
+ const keyChoices = fields.filter((f) => f.keyable && mapped.has(f.key));
151
+ const needsKey = mode !== 'create' && (options.key === undefined || !mapped.has(options.key));
152
+ const result = session?.result ?? null;
153
+ const fieldOptions = [
154
+ { value: SKIP, label: t('skip') },
155
+ ...fields.map((f) => ({
156
+ value: f.key,
157
+ label: `${f.line === undefined ? '' : `${t('lineFields')}: `}${f.label}${f.required ? ` (${t('required')})` : ''}`,
158
+ })),
159
+ ];
160
+ const setOption = (change) => map.mutate({
161
+ options: Object.fromEntries(Object.entries({ ...options, ...change }).filter(([, v]) => v !== undefined)),
162
+ });
163
+ const busy = map.isPending || apply.isPending || run.isPending;
164
+ const error = upload.error ?? map.error ?? apply.error ?? run.error ?? save.error ?? undo.error;
165
+ const shownErrors = (result?.errors ?? []).filter((e) => errorColumn === 'all' ||
166
+ (errorColumn === 'row' ? e.column === null : String(e.column) === errorColumn));
167
+ const footer = step === 'file' ? (_jsxs(Button, { variant: "primary", busy: upload.isPending, disabled: file === null, onClick: () => file !== null && upload.mutate(file), children: [_jsx(Upload, { "aria-hidden": true }), t('upload')] })) : step === 'columns' ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", disabled: busy || running, onClick: reset, children: t('back') }), _jsx(Button, { disabled: busy || running || missing.length > 0 || needsKey, busy: run.isPending && run.variables === true, onClick: () => run.mutate(true), children: t('check') }), _jsx(Button, { variant: "primary", disabled: busy || running || missing.length > 0 || needsKey, busy: run.isPending && run.variables === false, onClick: () => run.mutate(false), children: t('run') })] })) : (_jsxs(_Fragment, { children: [session?.state === 'draft' ? (_jsx(Button, { variant: "ghost", onClick: () => setStep('columns'), children: t('back') })) : null, _jsx(Button, { onClick: reset, children: t('another') }), _jsx(Button, { variant: "primary", onClick: () => {
168
+ onOpenChange(false);
169
+ reset();
170
+ }, children: t('done') })] }));
171
+ return (_jsx(Dialog, { open: open, onOpenChange: (next) => {
172
+ onOpenChange(next);
173
+ if (!next && !running)
174
+ reset();
175
+ }, title: t('title'), description: t('intro'), wide: true, footer: footer, children: _jsxs("div", { className: "space-y-4", children: [_jsx("ol", { className: "flex gap-4 text-sm", "aria-label": t('title'), children: ['file', 'columns', 'result'].map((s, i) => (_jsxs("li", { "aria-current": step === s ? 'step' : undefined, className: step === s ? 'font-semibold text-foreground' : 'text-muted-foreground', children: [i + 1, ". ", t(`step${s[0]?.toUpperCase()}${s.slice(1)}`)] }, s))) }), error === null ? null : _jsx(Alert, { children: message(error) }), step === 'file' ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `${id}-file`, children: t('chooseFile') }), _jsx("input", { id: `${id}-file`, type: "file", accept: ACCEPT, className: "block w-full text-sm", onChange: (e) => setFile(e.target.files?.[0] ?? null) })] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { size: "sm", variant: "ghost", busy: download.isPending, onClick: () => download.mutate(`/api/v1/import-templates/${encodeURIComponent(model)}`), children: [_jsx(Download, { "aria-hidden": true }), t('template')] }), (mappings.data?.mappings ?? [])
176
+ .filter((m) => m.template)
177
+ .map((m) => (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => download.mutate(`/api/v1/import-templates/${encodeURIComponent(model)}?mapping=${encodeURIComponent(m.id)}`), children: [_jsx(Download, { "aria-hidden": true }), t('templateOf', { name: m.name })] }, m.id)))] })] })) : null, step === 'columns' && session !== null ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: [session.fileName, " \u00B7 ", t('rows', { count: session.rowCount })] }), session.applied ? (_jsx(Alert, { tone: "info", children: t('applied', { name: session.applied.name }) })) : null, session.background ? _jsx(Alert, { tone: "info", children: t('background') }) : null, running ? (_jsx(Alert, { tone: "info", children: session.progress === null || session.progress.done === 0
178
+ ? t('queued')
179
+ : t('running', session.progress) })) : null, _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-start text-muted-foreground", children: [_jsx("th", { className: "py-1.5 pe-3 text-start font-medium", children: t('column') }), _jsx("th", { className: "py-1.5 pe-3 text-start font-medium", children: t('sample') }), _jsx("th", { className: "py-1.5 text-start font-medium", children: t('field') })] }) }), _jsx("tbody", { children: session.columns.map((c) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsx("td", { className: "py-1.5 pe-3 font-medium", children: session.headers[c.index] ?? '' }), _jsx("td", { className: "max-w-48 truncate py-1.5 pe-3 text-muted-foreground", children: session.sample
180
+ .slice(0, 2)
181
+ .map((r) => cell(r[c.index]))
182
+ .filter((v) => v !== '')
183
+ .join(' · ') }), _jsx("td", { className: "py-1.5", children: _jsx(Select, { label: `${t('field')}: ${session.headers[c.index] ?? ''}`, value: c.field ?? SKIP, options: fieldOptions, disabled: busy || running, onChange: (value) => map.mutate({
184
+ columns: session.columns.map((other) => other.index === c.index
185
+ ? { ...other, field: value === SKIP ? null : value }
186
+ : value !== SKIP && other.field === value
187
+ ? { ...other, field: null }
188
+ : other),
189
+ }) }) })] }, c.index))) })] }) }), missing.length > 0 ? (_jsx(Alert, { tone: "warning", children: t('missing', { fields: missing.map((f) => f.label).join(', ') }) })) : null, _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `${id}-mode`, children: t('mode') }), _jsx(Select, { id: `${id}-mode`, value: mode, disabled: busy || running, options: [
190
+ { value: 'create', label: t('modeCreate') },
191
+ { value: 'update', label: t('modeUpdate') },
192
+ { value: 'upsert', label: t('modeUpsert') },
193
+ ], onChange: (value) => setOption({ mode: value }) })] }), mode === 'create' && options.key !== 'externalId' ? null : (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `${id}-key`, children: t('key') }), _jsx(Select, { id: `${id}-key`, value: options.key ?? null, placeholder: t('noKey'), disabled: busy || running, options: keyChoices.map((f) => ({ value: f.key, label: f.label })), onChange: (value) => setOption({ key: value }) })] })), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `${id}-errors`, children: t('onError') }), _jsx(Select, { id: `${id}-errors`, value: options.onError ?? 'stop', disabled: busy || running, options: [
194
+ { value: 'stop', label: t('stop') },
195
+ { value: 'skip', label: t('skipErrors') },
196
+ ], onChange: (value) => setOption({ onError: value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `${id}-dates`, children: t('dateOrder') }), _jsx(Select, { id: `${id}-dates`, value: options.dateOrder ?? 'dmy', disabled: busy || running, options: [
197
+ { value: 'dmy', label: '31/12/2026' },
198
+ { value: 'mdy', label: '12/31/2026' },
199
+ { value: 'ymd', label: '2026/12/31' },
200
+ ], onChange: (value) => setOption({ dateOrder: value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `${id}-decimal`, children: t('decimal') }), _jsx(Select, { id: `${id}-decimal`, value: options.decimal ?? '.', disabled: busy || running, options: [
201
+ { value: '.', label: t('decimalDot') },
202
+ { value: ',', label: t('decimalComma') },
203
+ ], onChange: (value) => setOption({ decimal: value }) })] }), (mappings.data?.mappings ?? []).length === 0 ? null : (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `${id}-apply`, children: t('savedMappings') }), _jsx(Select, { id: `${id}-apply`, value: null, placeholder: t('applyMapping'), disabled: busy || running, options: (mappings.data?.mappings ?? []).map((m) => ({
204
+ value: m.id,
205
+ label: m.name,
206
+ })), onChange: (value) => apply.mutate(value) })] }))] }), _jsxs("div", { className: "flex flex-wrap items-end gap-3 border-t pt-3", children: [_jsxs("div", { className: "min-w-48 flex-1 space-y-1", children: [_jsx(Label, { htmlFor: `${id}-name`, children: t('mappingName') }), _jsx(Input, { id: `${id}-name`, value: mappingName, maxLength: 100, onChange: (e) => setMappingName(e.target.value) })] }), _jsxs("div", { className: "flex items-center gap-2 pb-2", children: [_jsx(Checkbox, { id: `${id}-share`, checked: share, onCheckedChange: (v) => setShare(v === true) }), _jsx(Label, { htmlFor: `${id}-share`, children: t('shareMapping') })] }), _jsxs(Button, { size: "sm", busy: save.isPending, disabled: mappingName.trim() === '' || mapped.size === 0, onClick: () => save.mutate(), children: [_jsx(Save, { "aria-hidden": true }), t('saveMapping')] }), savedName === null ? null : (_jsx("p", { role: "status", className: "w-full text-sm text-muted-foreground", children: t('saved') }))] })] })) : null, step === 'result' && session !== null ? (_jsxs("div", { className: "space-y-3", "aria-live": "polite", children: [session.failure === null ? null : (_jsx(Alert, { children: t('failure', { reason: session.failure }) })), result === null ? null : session.resultDry ? (_jsx(Alert, { tone: "info", children: t('checked', { create: result.wouldCreate, update: result.wouldUpdate }) })) : session.state === 'done' || session.state === 'undone' ? (_jsxs(Alert, { tone: "info", children: [t('imported', { created: result.created.length, updated: result.updated.length }), ' ', result.skipped > 0 ? t('skippedRows', { count: result.skipped }) : null] })) : null, undone === null ? null : (_jsxs(Alert, { tone: "info", children: [t('undone', { deleted: undone.deleted, restored: undone.restored }), ' ', undone.changed.length > 0
207
+ ? t('changedSince', { count: undone.changed.length })
208
+ : null] })), _jsxs("div", { className: "flex flex-wrap gap-2", children: [session.rejected > 0 ? (_jsxs(Button, { size: "sm", busy: download.isPending, onClick: () => download.mutate(`/api/v1/imports/${session.id}/rejected`), children: [_jsx(Download, { "aria-hidden": true }), t('rejected')] })) : null, session.undoUntil === null ? null : (_jsxs(Button, { size: "sm", variant: "danger", busy: undo.isPending, onClick: () => undo.mutate(), children: [_jsx(Undo2, { "aria-hidden": true }), t('undo')] }))] }), result === null ? null : result.errors.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('noErrors') })) : (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsx("h3", { className: "text-sm font-semibold", children: t('errors', { count: result.errors.length }) }), _jsx(Select, { label: t('errorColumn'), className: "w-56", value: errorColumn, onChange: setErrorColumn, options: [
209
+ { value: 'all', label: t('allColumns') },
210
+ { value: 'row', label: t('wholeRow') },
211
+ ...[
212
+ ...new Set(result.errors.flatMap((e) => (e.column === null ? [] : [e.column]))),
213
+ ]
214
+ .sort((a, b) => a - b)
215
+ .map((c) => ({ value: String(c), label: session.headers[c] ?? String(c) })),
216
+ ] })] }), _jsx("div", { className: "max-h-72 overflow-y-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "sticky top-0 bg-surface", children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "py-1.5 pe-3 text-start font-medium", children: t('errorRow') }), _jsx("th", { className: "py-1.5 pe-3 text-start font-medium", children: t('errorColumn') }), _jsx("th", { className: "py-1.5 text-start font-medium", children: t('errorMessage') })] }) }), _jsx("tbody", { children: shownErrors.map((e, i) => (_jsxs("tr", { className: "border-b last:border-0", children: [_jsx("td", { className: "py-1.5 pe-3 tabular-nums", children: e.row }), _jsx("td", { className: "py-1.5 pe-3", children: e.column === null ? t('wholeRow') : (session.headers[e.column] ?? '') }), _jsx("td", { className: "py-1.5", children: e.message })] }, i))) })] }) })] }))] })) : null] }) }));
217
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * The web contribution of `shutro-erp/import` (RFC 0014): "Import" next to every list's "New" (`import.Import`), the
4
+ * import screen, and their messages.
5
+ */
6
+ import { type Registry } from '../../ui/index.js';
7
+ import type { i18n } from 'i18next';
8
+ import { messages } from './messages.js';
9
+ export { ImportButton } from './ImportButton.js';
10
+ export { type ImportSession, ImportWizard } from './ImportWizard.js';
11
+ /** Registers "Import" (and the messages in `i18n`, when given). */
12
+ export declare function registerImportWeb(registry: Registry, i18n?: i18n): Registry;
13
+ export { messages as importMessages };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * The web contribution of `shutro-erp/import` (RFC 0014): "Import" next to every list's "New" (`import.Import`), the
4
+ * import screen, and their messages.
5
+ */
6
+ import { addMessages } from '../../ui/index.js';
7
+ import { ImportButton } from './ImportButton.js';
8
+ import { messages } from './messages.js';
9
+ export { ImportButton } from './ImportButton.js';
10
+ export { ImportWizard } from './ImportWizard.js';
11
+ /** Registers "Import" (and the messages in `i18n`, when given). */
12
+ export function registerImportWeb(registry, i18n) {
13
+ registry.registerCreateAssist('import.Import', ImportButton);
14
+ if (i18n !== undefined)
15
+ addMessages(i18n, 'import', messages);
16
+ return registry;
17
+ }
18
+ export { messages as importMessages };
@@ -0,0 +1,2 @@
1
+ /** Messages of the import screen (namespace `import`). */
2
+ export declare const messages: Readonly<Record<string, object>>;
@@ -0,0 +1,139 @@
1
+ /** Messages of the import screen (namespace `import`). */
2
+ export const messages = {
3
+ en: {
4
+ import: 'Import',
5
+ title: 'Import records',
6
+ intro: 'Upload a CSV or Excel file. Nothing is saved until you import, and an import can be undone for 7 days.',
7
+ stepFile: 'File',
8
+ stepColumns: 'Columns',
9
+ stepResult: 'Result',
10
+ chooseFile: 'CSV or Excel file',
11
+ upload: 'Read the file',
12
+ template: 'Download a sample file',
13
+ templateOf: 'Sample: {{name}}',
14
+ applied: 'The saved mapping “{{name}}” was applied.',
15
+ rows_one: '{{count}} row',
16
+ rows_other: '{{count}} rows',
17
+ background: 'Large file: it is checked and imported in the background. You can close this window.',
18
+ column: 'Column in the file',
19
+ sample: 'First values',
20
+ field: 'Field',
21
+ skip: 'Skip this column',
22
+ keyColumns: 'Keys',
23
+ lineFields: 'Lines',
24
+ required: 'required',
25
+ missing: 'Required fields without a column: {{fields}}',
26
+ mode: 'What to do',
27
+ modeCreate: 'Create new records',
28
+ modeUpdate: 'Update existing records',
29
+ modeUpsert: 'Update existing records, create the others',
30
+ key: 'Find records by',
31
+ noKey: 'Choose the column that finds the records',
32
+ onError: 'Rows with errors',
33
+ stop: 'Import nothing if any row has an error',
34
+ skipErrors: 'Import the other rows; give me the rejected ones',
35
+ dateOrder: 'Dates written as',
36
+ decimal: 'Decimal separator',
37
+ decimalDot: 'Point (1,250.50)',
38
+ decimalComma: 'Comma (1.250,50)',
39
+ savedMappings: 'Saved mappings',
40
+ applyMapping: 'Apply a saved mapping',
41
+ saveMapping: 'Save this mapping',
42
+ mappingName: 'Name of the mapping',
43
+ shareMapping: 'Share with every import user of the company',
44
+ saved: 'Mapping saved.',
45
+ check: 'Check',
46
+ run: 'Import',
47
+ running: 'Working… {{done}} of {{total}} records',
48
+ queued: 'Waiting to start…',
49
+ checked: 'Check finished: {{create}} to create, {{update}} to update.',
50
+ imported: 'Imported: {{created}} created, {{updated}} updated.',
51
+ skippedRows_one: '{{count}} record left out.',
52
+ skippedRows_other: '{{count}} records left out.',
53
+ noErrors: 'No errors.',
54
+ errors_one: '{{count}} error',
55
+ errors_other: '{{count}} errors',
56
+ errorRow: 'Row',
57
+ errorColumn: 'Column',
58
+ errorMessage: 'Problem',
59
+ allColumns: 'All columns',
60
+ wholeRow: 'Whole row',
61
+ rejected: 'Download the rejected rows',
62
+ undo: 'Undo this import',
63
+ undone: 'Undone: {{deleted}} deleted, {{restored}} restored.',
64
+ changedSince_one: '{{count}} record changed since the import was left as it is.',
65
+ changedSince_other: '{{count}} records changed since the import were left as they are.',
66
+ failure: 'The import could not finish: {{reason}}',
67
+ back: 'Back',
68
+ another: 'Import another file',
69
+ done: 'Done',
70
+ },
71
+ bn: {
72
+ import: 'ইমপোর্ট',
73
+ title: 'রেকর্ড ইমপোর্ট',
74
+ intro: 'একটি CSV বা Excel ফাইল আপলোড করুন। ইমপোর্ট না করা পর্যন্ত কিছুই সংরক্ষিত হয় না, এবং ৭ দিন পর্যন্ত ইমপোর্ট ফিরিয়ে নেওয়া যায়।',
75
+ stepFile: 'ফাইল',
76
+ stepColumns: 'কলাম',
77
+ stepResult: 'ফলাফল',
78
+ chooseFile: 'CSV বা Excel ফাইল',
79
+ upload: 'ফাইল পড়ুন',
80
+ template: 'নমুনা ফাইল ডাউনলোড করুন',
81
+ templateOf: 'নমুনা: {{name}}',
82
+ applied: 'সংরক্ষিত ম্যাপিং “{{name}}” প্রয়োগ করা হয়েছে।',
83
+ rows_one: '{{count}}টি সারি',
84
+ rows_other: '{{count}}টি সারি',
85
+ background: 'বড় ফাইল: এটি পটভূমিতে যাচাই ও ইমপোর্ট হবে। আপনি এই উইন্ডো বন্ধ করতে পারেন।',
86
+ column: 'ফাইলের কলাম',
87
+ sample: 'প্রথম মানগুলো',
88
+ field: 'ফিল্ড',
89
+ skip: 'এই কলাম বাদ দিন',
90
+ keyColumns: 'কী',
91
+ lineFields: 'লাইন',
92
+ required: 'আবশ্যক',
93
+ missing: 'কলাম ছাড়া আবশ্যক ফিল্ড: {{fields}}',
94
+ mode: 'কী করবেন',
95
+ modeCreate: 'নতুন রেকর্ড তৈরি করুন',
96
+ modeUpdate: 'বিদ্যমান রেকর্ড হালনাগাদ করুন',
97
+ modeUpsert: 'বিদ্যমান রেকর্ড হালনাগাদ করুন, বাকিগুলো তৈরি করুন',
98
+ key: 'রেকর্ড খুঁজুন',
99
+ noKey: 'যে কলাম দিয়ে রেকর্ড খুঁজবেন তা বেছে নিন',
100
+ onError: 'ত্রুটিপূর্ণ সারি',
101
+ stop: 'কোনো সারিতে ত্রুটি থাকলে কিছুই ইমপোর্ট করবেন না',
102
+ skipErrors: 'বাকি সারিগুলো ইমপোর্ট করুন; বাদ পড়াগুলো আমাকে দিন',
103
+ dateOrder: 'তারিখ লেখা আছে',
104
+ decimal: 'দশমিক চিহ্ন',
105
+ decimalDot: 'বিন্দু (1,250.50)',
106
+ decimalComma: 'কমা (1.250,50)',
107
+ savedMappings: 'সংরক্ষিত ম্যাপিং',
108
+ applyMapping: 'সংরক্ষিত ম্যাপিং প্রয়োগ করুন',
109
+ saveMapping: 'এই ম্যাপিং সংরক্ষণ করুন',
110
+ mappingName: 'ম্যাপিংয়ের নাম',
111
+ shareMapping: 'কোম্পানির সব ইমপোর্ট ব্যবহারকারীর সাথে শেয়ার করুন',
112
+ saved: 'ম্যাপিং সংরক্ষিত হয়েছে।',
113
+ check: 'যাচাই করুন',
114
+ run: 'ইমপোর্ট করুন',
115
+ running: 'কাজ চলছে… {{total}}টির মধ্যে {{done}}টি রেকর্ড',
116
+ queued: 'শুরু হওয়ার অপেক্ষায়…',
117
+ checked: 'যাচাই শেষ: {{create}}টি তৈরি হবে, {{update}}টি হালনাগাদ হবে।',
118
+ imported: 'ইমপোর্ট হয়েছে: {{created}}টি তৈরি, {{updated}}টি হালনাগাদ।',
119
+ skippedRows_one: '{{count}}টি রেকর্ড বাদ পড়েছে।',
120
+ skippedRows_other: '{{count}}টি রেকর্ড বাদ পড়েছে।',
121
+ noErrors: 'কোনো ত্রুটি নেই।',
122
+ errors_one: '{{count}}টি ত্রুটি',
123
+ errors_other: '{{count}}টি ত্রুটি',
124
+ errorRow: 'সারি',
125
+ errorColumn: 'কলাম',
126
+ errorMessage: 'সমস্যা',
127
+ allColumns: 'সব কলাম',
128
+ wholeRow: 'পুরো সারি',
129
+ rejected: 'বাদ পড়া সারিগুলো ডাউনলোড করুন',
130
+ undo: 'এই ইমপোর্ট ফিরিয়ে নিন',
131
+ undone: 'ফিরিয়ে নেওয়া হয়েছে: {{deleted}}টি মুছে ফেলা, {{restored}}টি পুনরুদ্ধার।',
132
+ changedSince_one: 'ইমপোর্টের পরে বদলানো {{count}}টি রেকর্ড যেমন ছিল তেমনই রাখা হয়েছে।',
133
+ changedSince_other: 'ইমপোর্টের পরে বদলানো {{count}}টি রেকর্ড যেমন ছিল তেমনই রাখা হয়েছে।',
134
+ failure: 'ইমপোর্ট শেষ করা যায়নি: {{reason}}',
135
+ back: 'পেছনে',
136
+ another: 'আরেকটি ফাইল ইমপোর্ট করুন',
137
+ done: 'সম্পন্ন',
138
+ },
139
+ };
@@ -0,0 +1,3 @@
1
+ import type { Worker } from '../server/orm.js';
2
+ import { type ImportContext, type ImportService } from './service.js';
3
+ export declare function importWorker(context: ImportContext, service?: ImportService): Worker;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Background imports (RFC 0014 §5.3): a file of more than `INLINE_ROWS` rows runs as a Shutro job, as the user who
3
+ * started it, in the job's one transaction (all-or-nothing stays all-or-nothing). Progress is written in short
4
+ * transactions of its own so the screen sees it; the end emits `import.finished`, which the notification inbox
5
+ * turns into a notification for the user. A fault is retried by Shutro; on the last attempt, or for a refusal (the
6
+ * user lost a permission), the session goes back to draft with the reason.
7
+ */
8
+ import { problemOf } from '../server/index.js';
9
+ import { createImportService, FINISHED_TOPIC, RUN_JOB, ref, } from './service.js';
10
+ export function importWorker(context, service = createImportService(context)) {
11
+ return context.shutro.work({
12
+ [RUN_JOB]: async (payload, env, job) => {
13
+ const { session: id, dryRun } = payload;
14
+ let session;
15
+ try {
16
+ session = await service.sessionOf(env, id);
17
+ }
18
+ catch (error) {
19
+ if (problemOf(error).status === 404)
20
+ return; // deleted since
21
+ throw error;
22
+ }
23
+ if (session.state !== 'running')
24
+ return;
25
+ try {
26
+ const result = await service.execute(env, session, dryRun, (done, total) => service.reportProgress(env.companyId, id, done, total));
27
+ await service.record(env, session, result, dryRun);
28
+ await env.emit(FINISHED_TOPIC, {
29
+ model: 'import.session',
30
+ id,
31
+ dry: dryRun,
32
+ file: session.fileName,
33
+ created: dryRun ? result.wouldCreate : result.created.length,
34
+ updated: dryRun ? result.wouldUpdate : result.updated.length,
35
+ errors: result.errors.length,
36
+ });
37
+ }
38
+ catch (error) {
39
+ const problem = problemOf(error);
40
+ if (problem.status >= 500 && job.attempt < job.maxAttempts)
41
+ throw error; // retried
42
+ const failure = (problem.status >= 500
43
+ ? 'The import failed; try again.'
44
+ : (problem.detail ?? problem.title)).slice(0, 500);
45
+ await context.shutro.bootstrap({ reason: 'import failed', company: env.companyId }, async (su) => {
46
+ const found = await ref(su, 'import.session').search({ where: { id }, limit: 1 });
47
+ await found.write({ state: 'draft', failure });
48
+ });
49
+ }
50
+ },
51
+ }, { pollMs: 5000 });
52
+ }
@@ -0,0 +1,20 @@
1
+ export type XlsxCell = string | number | boolean | Date | null;
2
+ export interface XlsxSheet {
3
+ readonly name: string;
4
+ readonly rows: XlsxCell[][];
5
+ }
6
+ export declare class XlsxError extends Error {
7
+ readonly name = "XlsxError";
8
+ }
9
+ /** The entries of a zip archive by name (stored or deflated; no ZIP64, no encryption). */
10
+ export declare function unzip(bytes: Uint8Array): Map<string, Uint8Array>;
11
+ /** XML text with its entities resolved. */
12
+ export declare function unescapeXml(text: string): string;
13
+ /** Whether a custom number format code shows a date or time (letters outside quotes and brackets). */
14
+ export declare function isDateFormat(code: string): boolean;
15
+ /** An Excel serial date (days since 1899-12-30) as a UTC date, to the second. */
16
+ export declare function fromSerial(serial: number): Date;
17
+ /** The sheet names of a workbook, in order. */
18
+ export declare function xlsxSheetNames(bytes: Uint8Array): string[];
19
+ /** Reads one sheet of a workbook: by name, else the first. @throws {XlsxError} */
20
+ export declare function readXlsx(bytes: Uint8Array, sheet?: string): XlsxSheet;