@multiplatform.one/backoffice 7.28.1 → 7.29.1

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 (269) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/desk/DeskSurface.native.js.map +1 -1
  3. package/dist/cjs/form/DocFormScreen.cjs +167 -224
  4. package/dist/cjs/form/DocFormScreen.native.js +182 -259
  5. package/dist/cjs/form/DocFormScreen.native.js.map +1 -1
  6. package/dist/cjs/i18n/en.cjs +1 -0
  7. package/dist/cjs/i18n/en.native.js +1 -0
  8. package/dist/cjs/i18n/en.native.js.map +1 -1
  9. package/dist/cjs/list/ListBulkActionsBar.cjs +30 -18
  10. package/dist/cjs/list/ListBulkActionsBar.native.js +43 -31
  11. package/dist/cjs/list/ListBulkActionsBar.native.js.map +1 -1
  12. package/dist/cjs/list/sidebar-section.cjs +15 -7
  13. package/dist/cjs/list/sidebar-section.native.js +16 -7
  14. package/dist/cjs/list/sidebar-section.native.js.map +1 -1
  15. package/dist/cjs/report/DoctypeReportView.cjs +20 -374
  16. package/dist/cjs/report/DoctypeReportView.native.js +23 -455
  17. package/dist/cjs/report/DoctypeReportView.native.js.map +1 -1
  18. package/dist/cjs/report/QueryReportScreen.cjs +33 -23
  19. package/dist/cjs/report/QueryReportScreen.native.js +33 -23
  20. package/dist/cjs/report/QueryReportScreen.native.js.map +1 -1
  21. package/dist/cjs/report/queryReportColumns.cjs +3 -105
  22. package/dist/cjs/report/queryReportColumns.native.js +2 -129
  23. package/dist/cjs/report/queryReportColumns.native.js.map +1 -1
  24. package/dist/cjs/report/queryReportFilterBar.cjs +3 -4
  25. package/dist/cjs/report/queryReportFilterBar.native.js +3 -4
  26. package/dist/cjs/report/queryReportFilterBar.native.js.map +1 -1
  27. package/dist/cjs/report/queryReportUrlState.cjs +2 -2
  28. package/dist/cjs/report/queryReportUrlState.native.js +2 -2
  29. package/dist/cjs/report/queryReportUrlState.native.js.map +1 -1
  30. package/dist/cjs/report/report-grid.cjs +2 -426
  31. package/dist/cjs/report/report-grid.native.js +1 -479
  32. package/dist/cjs/report/report-grid.native.js.map +1 -1
  33. package/dist/cjs/report/report-toolbar.cjs +2 -228
  34. package/dist/cjs/report/report-toolbar.native.js +1 -274
  35. package/dist/cjs/report/report-toolbar.native.js.map +1 -1
  36. package/dist/cjs/report/reportShapes.cjs +12 -383
  37. package/dist/cjs/report/reportShapes.native.js +12 -501
  38. package/dist/cjs/report/reportShapes.native.js.map +1 -1
  39. package/dist/cjs/report/reportViewModel.cjs +20 -360
  40. package/dist/cjs/report/reportViewModel.native.js +19 -627
  41. package/dist/cjs/report/reportViewModel.native.js.map +1 -1
  42. package/dist/cjs/report/savedReports.cjs +2 -106
  43. package/dist/cjs/report/savedReports.native.js +1 -132
  44. package/dist/cjs/report/savedReports.native.js.map +1 -1
  45. package/dist/cjs/report/useQueryReport.cjs +11 -219
  46. package/dist/cjs/report/useQueryReport.native.js +11 -244
  47. package/dist/cjs/report/useQueryReport.native.js.map +1 -1
  48. package/dist/cjs/shared/measures.cjs +3 -5
  49. package/dist/cjs/shared/measures.native.js +3 -5
  50. package/dist/cjs/shared/measures.native.js.map +1 -1
  51. package/dist/cjs/views/ReportView.cjs +3 -256
  52. package/dist/cjs/views/ReportView.native.js +3 -382
  53. package/dist/cjs/views/ReportView.native.js.map +1 -1
  54. package/dist/cjs/views/report-controls.cjs +6 -6
  55. package/dist/cjs/views/report-controls.native.js +6 -6
  56. package/dist/cjs/views/report-controls.native.js.map +1 -1
  57. package/dist/esm/desk/DeskSurface.mjs.map +1 -1
  58. package/dist/esm/desk/DeskSurface.native.js.map +1 -1
  59. package/dist/esm/form/DocFormScreen.mjs +169 -226
  60. package/dist/esm/form/DocFormScreen.mjs.map +1 -1
  61. package/dist/esm/form/DocFormScreen.native.js +184 -261
  62. package/dist/esm/form/DocFormScreen.native.js.map +1 -1
  63. package/dist/esm/i18n/en.mjs +1 -0
  64. package/dist/esm/i18n/en.mjs.map +1 -1
  65. package/dist/esm/i18n/en.native.js +1 -0
  66. package/dist/esm/i18n/en.native.js.map +1 -1
  67. package/dist/esm/list/ListBulkActionsBar.mjs +31 -19
  68. package/dist/esm/list/ListBulkActionsBar.mjs.map +1 -1
  69. package/dist/esm/list/ListBulkActionsBar.native.js +37 -25
  70. package/dist/esm/list/ListBulkActionsBar.native.js.map +1 -1
  71. package/dist/esm/list/sidebar-section.mjs +16 -8
  72. package/dist/esm/list/sidebar-section.mjs.map +1 -1
  73. package/dist/esm/list/sidebar-section.native.js +17 -8
  74. package/dist/esm/list/sidebar-section.native.js.map +1 -1
  75. package/dist/esm/report/DoctypeReportView.mjs +21 -375
  76. package/dist/esm/report/DoctypeReportView.mjs.map +1 -1
  77. package/dist/esm/report/DoctypeReportView.native.js +24 -456
  78. package/dist/esm/report/DoctypeReportView.native.js.map +1 -1
  79. package/dist/esm/report/QueryReportScreen.mjs +26 -16
  80. package/dist/esm/report/QueryReportScreen.mjs.map +1 -1
  81. package/dist/esm/report/QueryReportScreen.native.js +26 -16
  82. package/dist/esm/report/QueryReportScreen.native.js.map +1 -1
  83. package/dist/esm/report/queryReportColumns.mjs +1 -103
  84. package/dist/esm/report/queryReportColumns.mjs.map +1 -1
  85. package/dist/esm/report/queryReportColumns.native.js +1 -117
  86. package/dist/esm/report/queryReportColumns.native.js.map +1 -1
  87. package/dist/esm/report/queryReportFilterBar.mjs +1 -2
  88. package/dist/esm/report/queryReportFilterBar.mjs.map +1 -1
  89. package/dist/esm/report/queryReportFilterBar.native.js +1 -2
  90. package/dist/esm/report/queryReportFilterBar.native.js.map +1 -1
  91. package/dist/esm/report/queryReportUrlState.mjs +1 -1
  92. package/dist/esm/report/queryReportUrlState.native.js +1 -1
  93. package/dist/esm/report/report-grid.mjs +1 -425
  94. package/dist/esm/report/report-grid.mjs.map +1 -1
  95. package/dist/esm/report/report-grid.native.js +1 -468
  96. package/dist/esm/report/report-grid.native.js.map +1 -1
  97. package/dist/esm/report/report-toolbar.mjs +2 -228
  98. package/dist/esm/report/report-toolbar.mjs.map +1 -1
  99. package/dist/esm/report/report-toolbar.native.js +2 -264
  100. package/dist/esm/report/report-toolbar.native.js.map +1 -1
  101. package/dist/esm/report/reportShapes.mjs +2 -373
  102. package/dist/esm/report/reportShapes.mjs.map +1 -1
  103. package/dist/esm/report/reportShapes.native.js +2 -491
  104. package/dist/esm/report/reportShapes.native.js.map +1 -1
  105. package/dist/esm/report/reportViewModel.mjs +2 -342
  106. package/dist/esm/report/reportViewModel.mjs.map +1 -1
  107. package/dist/esm/report/reportViewModel.native.js +2 -610
  108. package/dist/esm/report/reportViewModel.native.js.map +1 -1
  109. package/dist/esm/report/savedReports.mjs +1 -105
  110. package/dist/esm/report/savedReports.mjs.map +1 -1
  111. package/dist/esm/report/savedReports.native.js +1 -132
  112. package/dist/esm/report/savedReports.native.js.map +1 -1
  113. package/dist/esm/report/useQueryReport.mjs +2 -210
  114. package/dist/esm/report/useQueryReport.mjs.map +1 -1
  115. package/dist/esm/report/useQueryReport.native.js +2 -235
  116. package/dist/esm/report/useQueryReport.native.js.map +1 -1
  117. package/dist/esm/shared/measures.mjs +1 -3
  118. package/dist/esm/shared/measures.mjs.map +1 -1
  119. package/dist/esm/shared/measures.native.js +1 -3
  120. package/dist/esm/shared/measures.native.js.map +1 -1
  121. package/dist/esm/views/ReportView.mjs +3 -256
  122. package/dist/esm/views/ReportView.mjs.map +1 -1
  123. package/dist/esm/views/ReportView.native.js +3 -382
  124. package/dist/esm/views/ReportView.native.js.map +1 -1
  125. package/dist/esm/views/report-controls.mjs +3 -3
  126. package/dist/esm/views/report-controls.mjs.map +1 -1
  127. package/dist/esm/views/report-controls.native.js +3 -3
  128. package/dist/esm/views/report-controls.native.js.map +1 -1
  129. package/dist/jsx/desk/DeskSurface.mjs.map +1 -1
  130. package/dist/jsx/desk/DeskSurface.native.js.map +1 -1
  131. package/dist/jsx/form/DocFormScreen.mjs +169 -226
  132. package/dist/jsx/form/DocFormScreen.mjs.map +1 -1
  133. package/dist/jsx/form/DocFormScreen.native.js +182 -259
  134. package/dist/jsx/form/DocFormScreen.native.js.map +1 -1
  135. package/dist/jsx/i18n/en.mjs +1 -0
  136. package/dist/jsx/i18n/en.mjs.map +1 -1
  137. package/dist/jsx/i18n/en.native.js +1 -0
  138. package/dist/jsx/i18n/en.native.js.map +1 -1
  139. package/dist/jsx/list/ListBulkActionsBar.mjs +31 -19
  140. package/dist/jsx/list/ListBulkActionsBar.mjs.map +1 -1
  141. package/dist/jsx/list/ListBulkActionsBar.native.js +43 -31
  142. package/dist/jsx/list/ListBulkActionsBar.native.js.map +1 -1
  143. package/dist/jsx/list/sidebar-section.mjs +16 -8
  144. package/dist/jsx/list/sidebar-section.mjs.map +1 -1
  145. package/dist/jsx/list/sidebar-section.native.js +16 -7
  146. package/dist/jsx/list/sidebar-section.native.js.map +1 -1
  147. package/dist/jsx/report/DoctypeReportView.mjs +21 -375
  148. package/dist/jsx/report/DoctypeReportView.mjs.map +1 -1
  149. package/dist/jsx/report/DoctypeReportView.native.js +23 -455
  150. package/dist/jsx/report/DoctypeReportView.native.js.map +1 -1
  151. package/dist/jsx/report/QueryReportScreen.mjs +26 -16
  152. package/dist/jsx/report/QueryReportScreen.mjs.map +1 -1
  153. package/dist/jsx/report/QueryReportScreen.native.js +33 -23
  154. package/dist/jsx/report/QueryReportScreen.native.js.map +1 -1
  155. package/dist/jsx/report/queryReportColumns.mjs +1 -103
  156. package/dist/jsx/report/queryReportColumns.mjs.map +1 -1
  157. package/dist/jsx/report/queryReportColumns.native.js +2 -129
  158. package/dist/jsx/report/queryReportColumns.native.js.map +1 -1
  159. package/dist/jsx/report/queryReportFilterBar.mjs +1 -2
  160. package/dist/jsx/report/queryReportFilterBar.mjs.map +1 -1
  161. package/dist/jsx/report/queryReportFilterBar.native.js +3 -4
  162. package/dist/jsx/report/queryReportFilterBar.native.js.map +1 -1
  163. package/dist/jsx/report/queryReportUrlState.mjs +1 -1
  164. package/dist/jsx/report/queryReportUrlState.native.js +2 -2
  165. package/dist/jsx/report/queryReportUrlState.native.js.map +1 -1
  166. package/dist/jsx/report/report-grid.mjs +1 -425
  167. package/dist/jsx/report/report-grid.mjs.map +1 -1
  168. package/dist/jsx/report/report-grid.native.js +1 -479
  169. package/dist/jsx/report/report-grid.native.js.map +1 -1
  170. package/dist/jsx/report/report-toolbar.mjs +2 -228
  171. package/dist/jsx/report/report-toolbar.mjs.map +1 -1
  172. package/dist/jsx/report/report-toolbar.native.js +1 -274
  173. package/dist/jsx/report/report-toolbar.native.js.map +1 -1
  174. package/dist/jsx/report/reportShapes.mjs +2 -373
  175. package/dist/jsx/report/reportShapes.mjs.map +1 -1
  176. package/dist/jsx/report/reportShapes.native.js +12 -501
  177. package/dist/jsx/report/reportShapes.native.js.map +1 -1
  178. package/dist/jsx/report/reportViewModel.mjs +2 -342
  179. package/dist/jsx/report/reportViewModel.mjs.map +1 -1
  180. package/dist/jsx/report/reportViewModel.native.js +19 -627
  181. package/dist/jsx/report/reportViewModel.native.js.map +1 -1
  182. package/dist/jsx/report/savedReports.mjs +1 -105
  183. package/dist/jsx/report/savedReports.mjs.map +1 -1
  184. package/dist/jsx/report/savedReports.native.js +1 -132
  185. package/dist/jsx/report/savedReports.native.js.map +1 -1
  186. package/dist/jsx/report/useQueryReport.mjs +2 -210
  187. package/dist/jsx/report/useQueryReport.mjs.map +1 -1
  188. package/dist/jsx/report/useQueryReport.native.js +11 -244
  189. package/dist/jsx/report/useQueryReport.native.js.map +1 -1
  190. package/dist/jsx/shared/measures.mjs +1 -3
  191. package/dist/jsx/shared/measures.mjs.map +1 -1
  192. package/dist/jsx/shared/measures.native.js +3 -5
  193. package/dist/jsx/shared/measures.native.js.map +1 -1
  194. package/dist/jsx/views/ReportView.mjs +3 -256
  195. package/dist/jsx/views/ReportView.mjs.map +1 -1
  196. package/dist/jsx/views/ReportView.native.js +3 -382
  197. package/dist/jsx/views/ReportView.native.js.map +1 -1
  198. package/dist/jsx/views/report-controls.mjs +3 -3
  199. package/dist/jsx/views/report-controls.mjs.map +1 -1
  200. package/dist/jsx/views/report-controls.native.js +6 -6
  201. package/dist/jsx/views/report-controls.native.js.map +1 -1
  202. package/package.json +12 -12
  203. package/src/backofficeScreens.designLaw.spec.ts +0 -5
  204. package/src/desk/DeskSurface.tsx +4 -7
  205. package/src/form/DocFormScreen.chrome.spec.ts +77 -0
  206. package/src/form/DocFormScreen.guest.screen.spec.tsx +175 -0
  207. package/src/form/DocFormScreen.rights.spec.tsx +7 -3
  208. package/src/form/DocFormScreen.tsx +214 -315
  209. package/src/i18n/en.ts +1 -0
  210. package/src/list/ListBulkActionsBar.rights.spec.tsx +134 -0
  211. package/src/list/ListBulkActionsBar.screen.spec.tsx +7 -1
  212. package/src/list/ListBulkActionsBar.tsx +76 -57
  213. package/src/list/ListFilterBar.screen.spec.tsx +14 -13
  214. package/src/list/sidebar-section.tsx +11 -3
  215. package/src/report/DoctypeReportView.tsx +28 -550
  216. package/src/report/QueryReportScreen.spec.tsx +75 -1
  217. package/src/report/QueryReportScreen.tsx +38 -22
  218. package/src/report/queryReportColumns.tsx +4 -123
  219. package/src/report/queryReportFilterBar.tsx +3 -5
  220. package/src/report/queryReportUrlState.ts +1 -1
  221. package/src/report/report-grid.tsx +4 -529
  222. package/src/report/report-toolbar.tsx +4 -308
  223. package/src/report/reportShapes.ts +25 -613
  224. package/src/report/reportShim.spec.ts +162 -0
  225. package/src/report/reportViewModel.ts +32 -605
  226. package/src/report/savedReports.ts +4 -161
  227. package/src/report/useQueryReport.ts +9 -347
  228. package/src/shared/measures.ts +3 -11
  229. package/src/views/ReportView.screen.spec.tsx +25 -20
  230. package/src/views/ReportView.tsx +7 -439
  231. package/src/views/report-controls.tsx +12 -20
  232. package/types/desk/DeskSurface.d.ts.map +1 -1
  233. package/types/form/DocFormScreen.d.ts +2 -2
  234. package/types/form/DocFormScreen.d.ts.map +1 -1
  235. package/types/i18n/en.d.ts +1 -0
  236. package/types/i18n/en.d.ts.map +1 -1
  237. package/types/list/ListBulkActionsBar.d.ts.map +1 -1
  238. package/types/list/sidebar-section.d.ts.map +1 -1
  239. package/types/report/DoctypeReportView.d.ts.map +1 -1
  240. package/types/report/QueryReportScreen.d.ts +1 -1
  241. package/types/report/QueryReportScreen.d.ts.map +1 -1
  242. package/types/report/queryReportColumns.d.ts +2 -11
  243. package/types/report/queryReportColumns.d.ts.map +1 -1
  244. package/types/report/queryReportFilterBar.d.ts +1 -1
  245. package/types/report/queryReportFilterBar.d.ts.map +1 -1
  246. package/types/report/queryReportUrlState.d.ts +1 -1
  247. package/types/report/queryReportUrlState.d.ts.map +1 -1
  248. package/types/report/report-grid.d.ts +2 -19
  249. package/types/report/report-grid.d.ts.map +1 -1
  250. package/types/report/report-toolbar.d.ts +2 -29
  251. package/types/report/report-toolbar.d.ts.map +1 -1
  252. package/types/report/reportShapes.d.ts +2 -115
  253. package/types/report/reportShapes.d.ts.map +1 -1
  254. package/types/report/reportViewModel.d.ts +2 -135
  255. package/types/report/reportViewModel.d.ts.map +1 -1
  256. package/types/report/savedReports.d.ts +2 -18
  257. package/types/report/savedReports.d.ts.map +1 -1
  258. package/types/report/useQueryReport.d.ts +2 -35
  259. package/types/report/useQueryReport.d.ts.map +1 -1
  260. package/types/shared/measures.d.ts +1 -2
  261. package/types/shared/measures.d.ts.map +1 -1
  262. package/types/views/ReportView.d.ts +3 -22
  263. package/types/views/ReportView.d.ts.map +1 -1
  264. package/types/views/report-controls.d.ts +2 -5
  265. package/types/views/report-controls.d.ts.map +1 -1
  266. package/src/report/report-grid.spec.tsx +0 -175
  267. package/src/report/report-toolbar.spec.tsx +0 -90
  268. package/src/report/reportShapes.spec.ts +0 -391
  269. package/src/report/reportViewModel.spec.ts +0 -393
@@ -1,62 +1,18 @@
1
- // Desk "report" view parity (audit punch #1). Replaces the old
2
- // FrappeReportBuilder composition (triple-stacked toolbars, 2-column
3
- // defaults, no filter row, no saved reports) with:
4
- // - desk default columns: name + status + title/image fields +
5
- // in_list_view / in_standard_filter fields + currency sources +
6
- // modified (reportViewModel, desk set_default_fields parity)
7
- // - a column add/remove picker; the choice persists per doctype on this
8
- // device (@multiplatform.one/store) while no saved report is selected
9
- // - a per-column FILTER ROW driving the standard frappe filter pipeline
10
- // (kind→operator mapping shared with the list filter vocabulary)
11
- // - ONE toolbar row (picker / group-by / saved reports / chips / export);
12
- // page size rides the footer pager like the list screen
13
- // - saved reports on the `Report` doctype (desk round-trip) selected via
14
- // the ?report= search param; fixture mode keeps them in memory
15
- // - live rows through the house collection channel: the FULL filtered
16
- // result stays client-side (totals + export need it), ordered
17
- // deterministically and windowed into pages client-side
18
- import { EmptyState, ScrollView, YStack, isWeb, notify } from "@multiplatform.one/components";
19
- import { Button } from "@multiplatform.one/forms";
20
- import {
21
- Order,
22
- useFrappeCollection,
23
- useLiveQuery,
24
- type UseFrappeCollectionConfig,
25
- } from "@multiplatform.one/frappe";
26
- import { downloadBlob } from "@multiplatform.one/platform";
27
- import { storage } from "@multiplatform.one/store";
28
- import { DataTablePagination } from "@multiplatform.one/table";
29
- import React, { useCallback, useEffect, useMemo, useState } from "react";
30
- import { useTranslation } from "react-i18next";
1
+ // The desk's report view (view=report): FrappeReportBuilder, the doctype
2
+ // report view frappe-ui exports, under the desk's navigation (MPO-15,
3
+ // GL-28). This file only wires the desk in: the `?f=` base filters, the
4
+ // `?report=` saved-report selection, the desk scope, and row and Link
5
+ // presses to the form route.
6
+ import { FrappeReportBuilder, parseReportFiltersParam } from "@multiplatform.one/frappe-ui";
7
+ import React, { useCallback, useMemo } from "react";
31
8
  import {
32
9
  useBackofficeLocation,
33
10
  useBackofficeNavigator,
34
11
  useBackofficePaths,
35
12
  } from "../config/BackofficeProvider";
36
- import { BACKOFFICE_I18N_NAMESPACE } from "../i18n";
37
13
  import { useDeskScope } from "../desk/scope";
38
14
  import type { FrappeFilters } from "../shared/types";
39
- import { useDoctypeMeta } from "../shared/useDoctypeMeta";
40
- import { useViewConnection } from "../views/connection";
41
15
  import { ViewError, ViewLoading } from "../views/ViewStates";
42
- import { ReportGrid } from "./report-grid";
43
- import { ReportToolbar } from "./report-toolbar";
44
- import { useSavedReports } from "./savedReports";
45
- import {
46
- DEFAULT_REPORT_PAGE_SIZE,
47
- REPORT_PAGE_SIZES,
48
- buildGridRows,
49
- buildReportCsv,
50
- columnFilterTuples,
51
- computeTotals,
52
- defaultReportFieldnames,
53
- orderReportRows,
54
- parseReportFiltersParam,
55
- reportColumnsFromMeta,
56
- resolveSelectedColumns,
57
- type ReportRow,
58
- type ReportSortState,
59
- } from "./reportViewModel";
60
16
 
61
17
  export interface DoctypeReportViewProps {
62
18
  /** Proper-cased doctype name (e.g. "ToDo", "Sales Order") */
@@ -71,54 +27,18 @@ export interface DoctypeReportViewProps {
71
27
  initialFilters?: FrappeFilters;
72
28
  }
73
29
 
74
- const EMPTY_FILTERS: FrappeFilters = [];
75
- const COLUMN_FILTER_DEBOUNCE_MS = 350;
76
-
77
- function columnsStorageKey(doctype: string): string {
78
- return `backoffice.report.columns.${doctype}`;
79
- }
80
-
81
- async function readStoredColumns(doctype: string): Promise<string[] | undefined> {
82
- try {
83
- const raw = await Promise.resolve(storage.getItem(columnsStorageKey(doctype)));
84
- if (!raw) return undefined;
85
- const parsed: unknown = JSON.parse(raw);
86
- if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")) {
87
- return parsed as string[];
88
- }
89
- } catch {
90
- // storage unavailable or corrupted — defaults win
91
- }
92
- return undefined;
93
- }
94
-
95
- function persistStoredColumns(doctype: string, fieldnames: readonly string[]): void {
96
- try {
97
- void Promise.resolve(storage.setItem(columnsStorageKey(doctype), JSON.stringify(fieldnames)));
98
- } catch {
99
- // storage unavailable — selection stays session-only
100
- }
101
- }
102
-
103
30
  export function DoctypeReportView({
104
31
  doctype,
105
32
  filtersParam,
106
33
  initialFilters,
107
34
  }: DoctypeReportViewProps) {
108
- const { t } = useTranslation(BACKOFFICE_I18N_NAMESPACE);
109
35
  const deskScope = useDeskScope();
110
36
  const navigator = useBackofficeNavigator();
111
37
  const paths = useBackofficePaths();
112
38
  const { params } = useBackofficeLocation();
113
- const connection = useViewConnection();
114
- const fixtureMode = Boolean(connection?.fixtures);
115
- const { meta, loading: metaLoading, error: metaError } = useDoctypeMeta(doctype);
116
- const savedReports = useSavedReports(doctype);
117
39
 
118
- // ── Saved-report selection (?report= search param) ──────────
119
- // The router may hand the param through raw — decode defensively (same
120
- // contract as the `?f=` parser: percent-decoding a decoded value is a
121
- // no-op for names without a literal %).
40
+ // The router may hand `?report=` through raw — decode defensively (same
41
+ // contract as the `?f=` parser).
122
42
  const rawReportParam = typeof params.report === "string" ? params.report : params.report?.[0];
123
43
  const currentReport = useMemo(() => {
124
44
  if (!rawReportParam) return null;
@@ -128,477 +48,35 @@ export function DoctypeReportView({
128
48
  return rawReportParam;
129
49
  }
130
50
  }, [rawReportParam]);
131
-
132
- // ── Columns: defaults → device-stored → explicit override ───
133
- const allColumns = useMemo(() => reportColumnsFromMeta(meta), [meta]);
134
- const defaultFieldnames = useMemo(() => defaultReportFieldnames(meta), [meta]);
135
- const [storedColumns, setStoredColumns] = useState<string[] | undefined>(undefined);
136
- const [selectedOverride, setSelectedOverride] = useState<string[] | null>(null);
137
-
138
- useEffect(() => {
139
- if (fixtureMode) return;
140
- let cancelled = false;
141
- void readStoredColumns(doctype).then((stored) => {
142
- if (!cancelled) setStoredColumns(stored);
143
- });
144
- return () => {
145
- cancelled = true;
146
- };
147
- }, [doctype, fixtureMode]);
148
-
149
- const selectedFieldnames = selectedOverride ?? storedColumns ?? defaultFieldnames;
150
- const selectedColumns = useMemo(
151
- () => resolveSelectedColumns(allColumns, selectedFieldnames),
152
- [allColumns, selectedFieldnames],
153
- );
154
-
155
- // ── Filters: ?f= base channel + the per-column filter row ───
156
51
  const urlFilters = useMemo(() => parseReportFiltersParam(filtersParam), [filtersParam]);
157
- const [filtersCleared, setFiltersCleared] = useState(false);
158
- const [reportFilters, setReportFilters] = useState<FrappeFilters | null>(null);
159
- const baseFilters = filtersCleared
160
- ? EMPTY_FILTERS
161
- : (reportFilters ?? urlFilters ?? initialFilters ?? EMPTY_FILTERS);
162
-
163
- const [filterDrafts, setFilterDrafts] = useState<Record<string, string>>({});
164
- const [committedFilters, setCommittedFilters] = useState<Record<string, string>>({});
165
- useEffect(() => {
166
- const handle = setTimeout(() => {
167
- setCommittedFilters((previous) =>
168
- JSON.stringify(previous) === JSON.stringify(filterDrafts) ? previous : filterDrafts,
169
- );
170
- }, COLUMN_FILTER_DEBOUNCE_MS);
171
- return () => clearTimeout(handle);
172
- }, [filterDrafts]);
173
-
174
- const columnTuples = useMemo(
175
- () => columnFilterTuples(selectedColumns, committedFilters),
176
- [selectedColumns, committedFilters],
177
- );
178
- // Desk scope (GL-5) ANDs into every report query, invisible to the
179
- // filter chrome and not clearable by the user.
180
- const effectiveFilters = useMemo(
181
- () => [...deskScope, ...baseFilters, ...columnTuples],
182
- [deskScope, baseFilters, columnTuples],
183
- );
184
-
185
- // ── Sort / group / paging ───────────────────────────────────
186
- const metaSort = useMemo<ReportSortState>(
187
- () => ({
188
- field: (meta?.sort_field || "modified").split(" ")[0],
189
- order: (meta?.sort_order || "DESC").toLowerCase() === "asc" ? "asc" : "desc",
190
- }),
191
- [meta?.sort_field, meta?.sort_order],
192
- );
193
- const [sortOverride, setSortOverride] = useState<ReportSortState | null>(null);
194
- const sort = sortOverride ?? metaSort;
195
- const [groupBy, setGroupBy] = useState<string | null>(null);
196
- const [pageSize, setPageSize] = useState<number>(DEFAULT_REPORT_PAGE_SIZE);
197
- const [page, setPage] = useState(0);
198
-
199
- // Doctype swap resets every view state (route component is reused).
200
- useEffect(() => {
201
- setSelectedOverride(null);
202
- setStoredColumns(undefined);
203
- setSortOverride(null);
204
- setGroupBy(null);
205
- setPage(0);
206
- setPageSize(DEFAULT_REPORT_PAGE_SIZE);
207
- setFilterDrafts({});
208
- setCommittedFilters({});
209
- setFiltersCleared(false);
210
- setReportFilters(null);
211
- }, [doctype]);
212
-
213
- useEffect(() => setFiltersCleared(false), [filtersParam]);
214
-
215
- const filtersKey = useMemo(() => JSON.stringify(effectiveFilters), [effectiveFilters]);
216
- useEffect(() => {
217
- setPage(0);
218
- }, [filtersKey, pageSize, groupBy]);
219
-
220
- // ── Apply a saved report's config when ?report= changes ─────
221
- useEffect(() => {
222
- if (!currentReport) {
223
- setReportFilters(null);
224
- return;
225
- }
226
- let cancelled = false;
227
- void savedReports.load(currentReport).then((config) => {
228
- if (cancelled || !config) return;
229
- setSelectedOverride(config.fieldnames.length > 0 ? config.fieldnames : null);
230
- setReportFilters(config.filters);
231
- setFilterDrafts({});
232
- setCommittedFilters({});
233
- setFiltersCleared(false);
234
- if (config.sort) setSortOverride(config.sort);
235
- if (config.pageSize && REPORT_PAGE_SIZES.some((size) => size === config.pageSize)) {
236
- setPageSize(config.pageSize);
237
- }
238
- setGroupBy(config.groupBy ?? null);
239
- });
240
- return () => {
241
- cancelled = true;
242
- };
243
- // savedReports.load changes identity with its report list; reloading the
244
- // config on list refreshes would clobber in-progress edits — key on the
245
- // selected report (and doctype via load's closure) only.
246
- // eslint-disable-next-line react-hooks/exhaustive-deps
247
- }, [currentReport, doctype]);
248
-
249
- // ── Column selection changes ────────────────────────────────
250
- const handleToggleColumn = useCallback(
251
- (fieldname: string) => {
252
- setSelectedOverride((previous) => {
253
- const current = previous ?? storedColumns ?? defaultFieldnames;
254
- const next = current.includes(fieldname)
255
- ? current.length === 1
256
- ? current
257
- : current.filter((entry) => entry !== fieldname)
258
- : [...current, fieldname];
259
- if (!fixtureMode && !currentReport) persistStoredColumns(doctype, next);
260
- return next;
261
- });
262
- },
263
- [storedColumns, defaultFieldnames, fixtureMode, currentReport, doctype],
264
- );
265
-
266
- const handleResetColumns = useCallback(() => {
267
- setSelectedOverride(defaultFieldnames);
268
- setStoredColumns(undefined);
269
- if (!fixtureMode) persistStoredColumns(doctype, defaultFieldnames);
270
- }, [defaultFieldnames, fixtureMode, doctype]);
271
-
272
- // ── Data: live collection over the full filtered result ─────
273
- const fetchFields = useMemo(() => {
274
- const fields = new Set<string>(["name", sort.field]);
275
- for (const column of selectedColumns) fields.add(column.fieldname);
276
- if (groupBy) fields.add(groupBy);
277
- return [...fields];
278
- }, [selectedColumns, sort.field, groupBy]);
279
52
 
280
- const [syncError, setSyncError] = useState<Error | null>(null);
281
- const handleSyncError = useCallback((error: unknown) => {
282
- setSyncError(error instanceof Error ? error : new Error(String(error)));
283
- }, []);
284
- const [retryGen, setRetryGen] = useState(0);
285
-
286
- // ViewConnection carries syncOptions as `unknown`; the shapes match the
287
- // collection config (same FrappeProvider source) — assert once here.
288
- const collectionConfig = useMemo(
289
- () =>
290
- connection && meta && retryGen >= 0
291
- ? (connection as unknown as UseFrappeCollectionConfig)
292
- : undefined,
293
- [connection, meta, retryGen],
294
- );
295
- const collection = useFrappeCollection<ReportRow & { doctype: string }>(
296
- collectionConfig,
297
- doctype,
298
- {
299
- fields: fetchFields,
300
- filters: effectiveFilters,
301
- orderBy: { field: sort.field, order: sort.order === "asc" ? Order.Asc : Order.Desc },
302
- onSyncError: handleSyncError,
303
- },
304
- );
305
- useEffect(() => setSyncError(null), [collection]);
306
- const { data: rowsData, isLoading: rowsLoading } = useLiveQuery(
307
- (q) => (collection ? q.from({ _report: collection }) : null),
308
- [collection],
309
- );
310
-
311
- const orderedRows = useMemo(
312
- () => orderReportRows((rowsData as ReportRow[] | undefined) ?? [], sort, groupBy),
313
- [rowsData, sort, groupBy],
314
- );
315
-
316
- const total = orderedRows.length;
317
- const pageCount = Math.max(1, Math.ceil(total / pageSize));
318
- const effectivePage = Math.min(page, pageCount - 1);
319
- const gridRows = useMemo(
320
- () => buildGridRows(orderedRows, effectivePage * pageSize, pageSize, groupBy),
321
- [orderedRows, effectivePage, pageSize, groupBy],
322
- );
323
- const totals = useMemo(
324
- () => computeTotals(selectedColumns, orderedRows),
325
- [selectedColumns, orderedRows],
326
- );
327
- const groupByColumn = groupBy
328
- ? allColumns.find((column) => column.fieldname === groupBy)
329
- : undefined;
330
-
331
- // ── Navigation ──────────────────────────────────────────────
332
- const handleOpenRow = useCallback(
333
- (name: string) => navigator.navigate(paths.form(doctype, name)),
334
- [navigator, paths, doctype],
335
- );
336
- const handleOpenLink = useCallback(
53
+ const openForm = useCallback(
337
54
  (targetDoctype: string, name: string) => navigator.navigate(paths.form(targetDoctype, name)),
338
55
  [navigator, paths],
339
56
  );
340
-
341
- const handleSelectReport = useCallback(
57
+ const selectReport = useCallback(
342
58
  (name: string | null) => {
343
59
  const base = paths.view(doctype, "report");
344
60
  navigator.replace(name ? `${base}?report=${encodeURIComponent(name)}` : base);
345
- if (!name) {
346
- setReportFilters(null);
347
- setSelectedOverride(null);
348
- setSortOverride(null);
349
- setGroupBy(null);
350
- }
351
61
  },
352
62
  [navigator, paths, doctype],
353
63
  );
354
-
355
- const handleClearBaseFilters = useCallback(() => {
356
- setFiltersCleared(true);
357
- if (urlFilters && !reportFilters) navigator.replace(paths.view(doctype, "report"));
358
- }, [urlFilters, reportFilters, navigator, paths, doctype]);
359
-
360
- // ── Saved-report actions ────────────────────────────────────
361
- const handleSaveReport = useCallback(
362
- (name: string) => {
363
- void savedReports
364
- .save(name, {
365
- fieldnames: [...selectedFieldnames],
366
- filters: effectiveFilters,
367
- sort,
368
- pageSize,
369
- groupBy,
370
- addTotals: totals.length > 0,
371
- })
372
- .then((savedName) => {
373
- notify({
374
- severity: "success",
375
- scope: "page",
376
- body: t("report.saveDone", {
377
- defaultValue: "Saved report “{{name}}”",
378
- name: savedName,
379
- }),
380
- });
381
- if (savedName !== currentReport) handleSelectReport(savedName);
382
- })
383
- .catch((error: unknown) => {
384
- notify({
385
- severity: "error",
386
- scope: "page",
387
- body:
388
- error instanceof Error
389
- ? error.message
390
- : t("report.saveFailed", "Could not save the report"),
391
- });
392
- });
393
- },
394
- [
395
- savedReports,
396
- selectedFieldnames,
397
- effectiveFilters,
398
- sort,
399
- pageSize,
400
- groupBy,
401
- totals.length,
402
- currentReport,
403
- handleSelectReport,
404
- t,
405
- ],
406
- );
407
-
408
- const handleDeleteReport = useCallback(
409
- (name: string) => {
410
- void savedReports
411
- .remove(name)
412
- .then(() => {
413
- notify({
414
- severity: "success",
415
- scope: "page",
416
- body: t("report.deleteDone", {
417
- defaultValue: "Deleted report “{{name}}”",
418
- name,
419
- }),
420
- });
421
- if (name === currentReport) handleSelectReport(null);
422
- })
423
- .catch((error: unknown) => {
424
- notify({
425
- severity: "error",
426
- scope: "page",
427
- body:
428
- error instanceof Error
429
- ? error.message
430
- : t("report.deleteFailed", "Could not delete the report"),
431
- });
432
- });
433
- },
434
- [savedReports, currentReport, handleSelectReport, t],
64
+ const clearUrlFilters = useCallback(() => {
65
+ if (urlFilters) navigator.replace(paths.view(doctype, "report"));
66
+ }, [urlFilters, navigator, paths, doctype]);
67
+
68
+ return (
69
+ <FrappeReportBuilder
70
+ doctype={doctype}
71
+ filters={urlFilters ?? initialFilters}
72
+ scopeFilters={deskScope}
73
+ onClearFilters={clearUrlFilters}
74
+ report={currentReport}
75
+ onReportChange={selectReport}
76
+ onOpenRow={(name) => openForm(doctype, name)}
77
+ onOpenLink={openForm}
78
+ loading={<ViewLoading />}
79
+ error={(error) => <ViewError doctype={doctype} error={error} />}
80
+ />
435
81
  );
436
-
437
- // ── Export: full filtered + ordered result as CSV ───────────
438
- const handleExport = useCallback(() => {
439
- if (!isWeb) {
440
- notify({
441
- severity: "warning",
442
- scope: "page",
443
- body: t(
444
- "report.exportUnsupported",
445
- "CSV export needs a browser download — not available on this platform yet",
446
- ),
447
- });
448
- return;
449
- }
450
- downloadBlob(
451
- new Blob([buildReportCsv(selectedColumns, orderedRows)], {
452
- type: "text/csv;charset=utf-8;",
453
- }),
454
- `${doctype}_report.csv`,
455
- );
456
- notify({
457
- severity: "success",
458
- scope: "page",
459
- body: t("report.exportDone", {
460
- count: orderedRows.length,
461
- defaultValue_one: "Exported {{count}} row to CSV",
462
- defaultValue_other: "Exported {{count}} rows to CSV",
463
- }),
464
- });
465
- }, [selectedColumns, orderedRows, doctype, t]);
466
-
467
- const handleColumnFilterChange = useCallback((fieldname: string, value: string) => {
468
- setFilterDrafts((previous) => {
469
- if (value === "") {
470
- if (!(fieldname in previous)) return previous;
471
- const { [fieldname]: _dropped, ...rest } = previous;
472
- return rest;
473
- }
474
- return { ...previous, [fieldname]: value };
475
- });
476
- }, []);
477
-
478
- const handleClearColumnFilters = useCallback(() => {
479
- setFilterDrafts({});
480
- setCommittedFilters({});
481
- }, []);
482
-
483
- const activeColumnFilterCount = columnTuples.length;
484
-
485
- // ── Render ──────────────────────────────────────────────────
486
- if (metaLoading && !meta) return <ViewLoading />;
487
- if (metaError && !meta) return <ViewError doctype={doctype} error={metaError} />;
488
-
489
- let body: React.ReactNode;
490
- if (syncError) {
491
- body = (
492
- <EmptyState
493
- intent="error"
494
- title={t("report.loadFailed", "Couldn't load report rows")}
495
- description={syncError.message}
496
- action={
497
- <Button
498
- size="$3"
499
- onPress={() => {
500
- setSyncError(null);
501
- setRetryGen((generation) => generation + 1);
502
- }}
503
- >
504
- {t("report.retry", "Retry")}
505
- </Button>
506
- }
507
- />
508
- );
509
- } else if (rowsLoading && total === 0) {
510
- body = <ViewLoading />;
511
- } else if (total === 0) {
512
- const hasFilters = effectiveFilters.length > 0;
513
- body = (
514
- <EmptyState
515
- title={t("report.emptyTitle", "No results")}
516
- description={
517
- hasFilters
518
- ? t("report.emptyFiltered", "No rows match the current filters")
519
- : t("report.emptyDescription", {
520
- defaultValue: "There are no {{doctype}} records yet",
521
- doctype,
522
- })
523
- }
524
- action={
525
- hasFilters ? (
526
- <Button
527
- size="$3"
528
- onPress={() => {
529
- handleClearColumnFilters();
530
- handleClearBaseFilters();
531
- }}
532
- >
533
- {t("report.clearAllFilters", "Clear all filters")}
534
- </Button>
535
- ) : undefined
536
- }
537
- />
538
- );
539
- } else {
540
- body = (
541
- <ReportGrid
542
- columns={selectedColumns}
543
- rows={gridRows}
544
- sort={sort}
545
- onSortChange={setSortOverride}
546
- columnFilters={filterDrafts}
547
- onColumnFilterChange={handleColumnFilterChange}
548
- onOpenRow={handleOpenRow}
549
- onOpenLink={handleOpenLink}
550
- totals={totals}
551
- groupByColumn={groupByColumn}
552
- />
553
- );
554
- }
555
-
556
- const content = (
557
- <YStack flex={1} minWidth={0}>
558
- <ReportToolbar
559
- columns={allColumns}
560
- selected={selectedFieldnames}
561
- onToggleColumn={handleToggleColumn}
562
- onResetColumns={handleResetColumns}
563
- groupBy={groupBy}
564
- onGroupByChange={setGroupBy}
565
- baseFilters={baseFilters}
566
- onClearBaseFilters={baseFilters.length > 0 ? handleClearBaseFilters : undefined}
567
- columnFilterCount={activeColumnFilterCount}
568
- onClearColumnFilters={handleClearColumnFilters}
569
- savedReports={savedReports.reports}
570
- currentReport={currentReport}
571
- onSelectReport={handleSelectReport}
572
- onSaveReport={handleSaveReport}
573
- onDeleteReport={handleDeleteReport}
574
- onExport={handleExport}
575
- exportDisabled={total === 0}
576
- />
577
- <YStack flex={1} minWidth={0} paddingHorizontal="$3">
578
- {body}
579
- </YStack>
580
- {total > 0 && !syncError && (
581
- <YStack paddingHorizontal="$3" paddingVertical="$2">
582
- <DataTablePagination
583
- pagination={{
584
- pageIndex: effectivePage,
585
- pageSize,
586
- totalCount: total,
587
- totalPages: pageCount,
588
- }}
589
- onPaginationChange={(next) => {
590
- setPage(next.pageIndex);
591
- setPageSize(next.pageSize);
592
- }}
593
- pageSizeOptions={[...REPORT_PAGE_SIZES]}
594
- compact
595
- />
596
- </YStack>
597
- )}
598
- </YStack>
599
- );
600
-
601
- // Native has no page-level scroll (DoctypeListScreen precedent) — wrap so
602
- // rows below the fold stay reachable. Web keeps the flex layout.
603
- return isWeb ? content : <ScrollView flex={1}>{content}</ScrollView>;
604
82
  }