@conduction/nextcloud-vue 2.78.0 → 2.80.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 (259) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  8. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
  9. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
  19. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +85 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  40. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  41. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  42. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  43. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  44. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  46. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  50. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  51. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  52. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  54. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  56. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  57. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  58. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
  60. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  62. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  64. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  71. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  72. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  73. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  74. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  75. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  78. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  79. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  80. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  81. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  82. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  83. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  84. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  85. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  86. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  87. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  88. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  92. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  93. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  94. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  95. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  96. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  98. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  99. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  100. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  101. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  102. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  103. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  104. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  105. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  106. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  110. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
  112. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  113. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  114. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  115. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  116. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  117. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  118. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  119. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  120. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  121. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  122. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  123. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  124. package/dist/esm/composables/useAppManifest.js +8 -3
  125. package/dist/esm/composables/useAppManifest.js.map +1 -1
  126. package/dist/esm/composables/useFormDraft.js +140 -9
  127. package/dist/esm/composables/useFormDraft.js.map +1 -1
  128. package/dist/esm/index.js +5 -0
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  131. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  132. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  133. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  134. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  135. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  136. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  137. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  138. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  139. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  140. package/dist/esm/integrations/offline/index.js +2 -1
  141. package/dist/esm/integrations/offline/index.js.map +1 -1
  142. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  143. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  144. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  145. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  146. package/dist/esm/l10n/en.json.js +72 -1
  147. package/dist/esm/l10n/en.json.js.map +1 -1
  148. package/dist/esm/l10n/nl.json.js +120 -2
  149. package/dist/esm/l10n/nl.json.js.map +1 -1
  150. package/dist/esm/nextcloud-vue.css +1314 -74
  151. package/dist/esm/utils/boardDate.js +193 -0
  152. package/dist/esm/utils/boardDate.js.map +1 -0
  153. package/dist/esm/utils/detailActionModel.js +60 -3
  154. package/dist/esm/utils/detailActionModel.js.map +1 -1
  155. package/dist/esm/utils/dynamicProperties.js +5 -1
  156. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  157. package/dist/esm/utils/fillTemplate.js +114 -0
  158. package/dist/esm/utils/fillTemplate.js.map +1 -0
  159. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  160. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  161. package/dist/esm/utils/queryPresets.js +96 -0
  162. package/dist/esm/utils/queryPresets.js.map +1 -0
  163. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  164. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  165. package/dist/esm/utils/schema.js +21 -0
  166. package/dist/esm/utils/schema.js.map +1 -1
  167. package/dist/esm/utils/sentinelTokens.js +3 -3
  168. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  169. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  170. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  171. package/dist/esm/utils/widgetDispatch.js +7 -1
  172. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  173. package/dist/nextcloud-vue.cjs.js +14278 -8169
  174. package/dist/nextcloud-vue.cjs.js.map +2 -2
  175. package/dist/nextcloud-vue.css +1314 -74
  176. package/l10n/en.json +72 -1
  177. package/l10n/nl.json +120 -2
  178. package/package.json +1 -1
  179. package/scripts/build-validators.js +26 -0
  180. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  181. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  182. package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
  183. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  184. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  185. package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
  186. package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
  187. package/src/components/CnDataTable/CnDataTable.md +2 -0
  188. package/src/components/CnDataTable/CnDataTable.vue +82 -2
  189. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  190. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  191. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
  192. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  193. package/src/components/CnFactsWidget/index.js +29 -0
  194. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  195. package/src/components/CnFieldListWidget/index.js +28 -0
  196. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  197. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  198. package/src/components/CnFilesBrowser/index.js +1 -0
  199. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  200. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  201. package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
  202. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  203. package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
  204. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  205. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  206. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  207. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  208. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  209. package/src/components/CnRecordListWidget/index.js +30 -0
  210. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  211. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  212. package/src/components/CnSectionsWidget/index.js +34 -0
  213. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  214. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  215. package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
  216. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  217. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  218. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  219. package/src/components/index.js +5 -1
  220. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  221. package/src/composables/useAppManifest.js +8 -3
  222. package/src/composables/useFormDraft.js +139 -8
  223. package/src/css/form-field.css +14 -0
  224. package/src/css/index-page.css +71 -0
  225. package/src/css/kpi-card.css +32 -0
  226. package/src/css/look-board-detail.css +87 -0
  227. package/src/css/look-board-index.css +143 -14
  228. package/src/css/look-board.css +114 -2
  229. package/src/css/table.css +56 -0
  230. package/src/index.js +8 -0
  231. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  232. package/src/integrations/builtin/field-inspection.js +21 -0
  233. package/src/integrations/offline/checklistSubmission.js +137 -0
  234. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  235. package/src/integrations/offline/index.js +3 -0
  236. package/src/integrations/offline/offlineDb.js +6 -1
  237. package/src/integrations/offline/syncReplayService.js +8 -2
  238. package/src/schemas/app-manifest-v2.schema.json +149 -5
  239. package/src/utils/boardDate.js +189 -0
  240. package/src/utils/detailActionModel.js +59 -2
  241. package/src/utils/dynamicProperties.js +5 -1
  242. package/src/utils/fillTemplate.js +110 -0
  243. package/src/utils/libraryWidgetKeys.js +4 -0
  244. package/src/utils/queryPresets.js +93 -0
  245. package/src/utils/resolveFilterTokens.js +11 -0
  246. package/src/utils/schema.js +21 -0
  247. package/src/utils/sentinelTokens.js +3 -3
  248. package/src/utils/validateManifestV2.compiled.js +57 -2
  249. package/src/utils/widgetDispatch.js +7 -1
  250. package/dist/esm/_virtual/equal.js +0 -4
  251. package/dist/esm/_virtual/equal.js.map +0 -1
  252. package/dist/esm/_virtual/ucs2length.js +0 -4
  253. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  254. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  255. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  256. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  257. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  258. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  259. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CnFieldInspectionCard.vue3.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue?vue&type=template&id=471e815e&lang.js"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionCard — surface-aware widget for the `field-inspection`\n - integration leaf (offline field data-collection).\n -\n - Drives the generic offline core (src/integrations/offline): a\n - \"Synchronise day\" action fetches today's planned items via the standard OR\n - object API (planningFetch) into the IndexedDB cache, the planning list lets\n - the user open and complete a checklist offline, and saved answers are queued\n - as OR object mutations that replay on reconnect. The sync-state indicator\n - (online / all-synced / queued / conflict) reflects the pending queue.\n -\n - Generic by construction: the checklist items, the planned-items query and\n - the schema names all come from the integration's `offlineConfig` (resolved\n - from the registry by id) — never from app-specific code in this widget.\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<div class=\"cn-field-inspection\" data-testid=\"cn-field-inspection-card\">\n\t\t\t<div class=\"cn-field-inspection__sync\" :class=\"`cn-field-inspection__sync--${indicator.tone}`\" data-testid=\"cn-fi-sync-indicator\">\n\t\t\t\t<span class=\"cn-field-inspection__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span>{{ indicator.text }}</span>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-field-inspection__actions\">\n\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\tdata-testid=\"cn-fi-sync-day\"\n\t\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t\t@click=\"syncDay\">\n\t\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\t\tdata-testid=\"cn-fi-drain-queue\"\n\t\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t\t@click=\"drain\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 || stuckCount > 0\"\n\t\t\t\t\tdata-testid=\"cn-fi-open-queue\"\n\t\t\t\t\t@click=\"showQueue = !showQueue\">\n\t\t\t\t\t{{ showQueue\n\t\t\t\t\t\t? t('nextcloud-vue', 'Hide what is waiting')\n\t\t\t\t\t\t: t('nextcloud-vue', 'See what is waiting') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- 🔴 AN EXPIRED PLANNING USED TO READ AS A CURRENT ONE. This\n\t\t\t\t line only ever said \"Ready offline until\", in the past tense\n\t\t\t\t or not, so a planning downloaded three days ago looked the\n\t\t\t\t same as one downloaded an hour ago. In the field there is\n\t\t\t\t nothing else to check it against. -->\n\t\t\t\t<span v-if=\"planningMeta\" class=\"cn-field-inspection__meta\" data-testid=\"cn-fi-planning-meta\">\n\t\t\t\t\t{{ planningIsStale\n\t\t\t\t\t\t? t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: formatTime(planningMeta.syncedAt) })\n\t\t\t\t\t\t: t('nextcloud-vue', 'Ready offline until {time}', { time: formatTime(planningMeta.expiresAt) }) }}\n\t\t\t\t</span>\n\t\t\t</div>\n\n\t\t\t<!-- The card links to the queue rather than owning a second copy\n\t\t\t of it: one component, one set of statuses, one set of words. -->\n\t\t\t<CnOfflineQueue v-if=\"showQueue\" :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\n\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t\t<NcEmptyContent v-else-if=\"!activeItem && plannedItems.length === 0\"\n\t\t\t\tdata-testid=\"cn-fi-empty\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'No items planned')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.')\" />\n\n\t\t\t<!-- Planning list -->\n\t\t\t<ul v-else-if=\"!activeItem\" class=\"cn-field-inspection__list\" data-testid=\"cn-fi-list\">\n\t\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection__row\">\n\t\t\t\t\t<button type=\"button\"\n\t\t\t\t\t\tclass=\"cn-field-inspection__row-btn\"\n\t\t\t\t\t\t:data-testid=\"`cn-fi-open-${itemId(item)}`\"\n\t\t\t\t\t\t@click=\"openItem(item)\">\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t\t\t</button>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<!-- Checklist completion for one item -->\n\t\t\t<div v-else class=\"cn-field-inspection__checklist\" data-testid=\"cn-fi-checklist\">\n\t\t\t\t<div class=\"cn-field-inspection__checklist-head\">\n\t\t\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-fi-back\" @click=\"closeItem\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<strong>{{ itemTitle(activeItem) }}</strong>\n\t\t\t\t</div>\n\n\t\t\t\t<p v-if=\"activeTemplate\" class=\"cn-field-inspection__progress\" data-testid=\"cn-fi-progress\">\n\t\t\t\t\t{{ t('nextcloud-vue', '{done} of {total} questions completed', { done: progress.done, total: progress.total }) }}\n\t\t\t\t</p>\n\n\t\t\t\t<form v-if=\"activeTemplate\" @submit.prevent=\"saveChecklist\">\n\t\t\t\t\t<fieldset v-for=\"q in activeTemplate.items\" :key=\"q.questionId\" class=\"cn-field-inspection__question\">\n\t\t\t\t\t\t<legend>\n\t\t\t\t\t\t\t{{ q.text }}<span v-if=\"q.required\" class=\"cn-field-inspection__required\" aria-hidden=\"true\"> *</span>\n\t\t\t\t\t\t</legend>\n\t\t\t\t\t\t<select v-if=\"q.type === 'yes_no'\"\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\">\n\t\t\t\t\t\t\t<option value=\"\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '— choose —') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"yes\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Yes') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"no\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'No') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"na\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'N/A') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<textarea v-else\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\"\n\t\t\t\t\t\t\trows=\"2\" />\n\t\t\t\t\t\t<span v-if=\"errorFor(q.questionId)\" class=\"cn-field-inspection__error\" :data-testid=\"`cn-fi-error-${q.questionId}`\">\n\t\t\t\t\t\t\t{{ errorFor(q.questionId) }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</fieldset>\n\n\t\t\t\t\t<!-- `type=\"submit\"` (native HTML type) + `variant=\"primary\"` (style) —\n\t\t\t\t\t @nextcloud/vue 9 split what Vue-2-era code conflated: `type` is now\n\t\t\t\t\t the native button type (`ButtonType`), and the old style-variant\n\t\t\t\t\t prop is `variant`. This line used to pass BOTH wrong: `native-type`\n\t\t\t\t\t (not a real prop — the button silently defaulted to `type=\"button\"`\n\t\t\t\t\t and never fired the form's native submit) and `variant=\"primary\"`\n\t\t\t\t\t (an invalid `ButtonType`, so the visual primary style never applied\n\t\t\t\t\t either). See CnFormPage.vue's inline comment for the submit-button\n\t\t\t\t\t half of this bug in detail. -->\n\t\t\t\t\t<NcButton type=\"submit\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"cn-fi-save\"\n\t\t\t\t\t\t:disabled=\"saving\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Save answers offline') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</form>\n\n\t\t\t\t<NcEmptyContent v-else\n\t\t\t\t\tdata-testid=\"cn-fi-no-template\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'Checklist not available offline')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'Synchronise the day while online to download this checklist.')\" />\n\t\t\t</div>\n\t\t</div>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { checklistProgress, classifyGps, syncIndicator, validateChecklistAnswers } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnFieldInspectionCard — surface-aware offline field data-collection widget.\n *\n * Renders the offline planning list and inline checklist completion across the\n * dashboard / detail surfaces, driven entirely by the generic offline core and\n * the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionCard',\n\n\tcomponents: { CnDetailCard, CnOfflineQueue, NcButton, NcLoadingIcon, NcEmptyContent },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/* eslint-disable vue/no-unused-properties -- every integration surface is handed `surface` by its host; this card renders one layout and does not branch on it */\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/** Object context `{ register, schema, objectId }` forwarded by the host page. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** OpenRegister register id (slug or uuid) — falls back to integrationContext. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid) — falls back to integrationContext. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Field inspections') },\n\t\t/** Optional Material Design Icon component for the card header. */\n\t\ticon: { type: [Object, Function], default: () => ClipboardCheckOutline },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tplanningMeta: null,\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** Whether the queue list is open under the card. */\n\t\t\tshowQueue: false,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\tsaving: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t\tactiveItem: null,\n\t\t\tactiveTemplate: null,\n\t\t\tanswers: {},\n\t\t\terrors: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry, falling back to the canonical defaults. A consuming app's\n\t\t * override supplies its own schema names / filter fields here. Read from\n\t\t * the global registry (not a static import) to avoid an import cycle.\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective register (prop wins, else integrationContext).\n\t\t *\n\t\t * @return {string} The register id.\n\t\t */\n\t\teffectiveRegister() {\n\t\t\treturn this.register || this.integrationContext.register || ''\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else context/prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || this.integrationContext.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.effectiveRegister,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\n\t\tprogress() {\n\t\t\treturn checklistProgress(this.activeTemplate, this.answers)\n\t\t},\n\t},\n\n\twatch: {\n\t\tintegrationContext: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners (auto-drain on reconnect) + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\tformatTime(iso) {\n\t\t\tif (!iso) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn new Date(iso).toLocaleString()\n\t\t},\n\n\t\terrorFor(questionId) {\n\t\t\tconst e = this.errors.find((x) => x.questionId === questionId)\n\t\t\treturn e ? e.message : ''\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.effectiveRegister || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning (planned items + referenced templates) via\n\t\t * the standard OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.effectiveRegister, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open one planned item and load its checklist template from cache.\n\t\t *\n\t\t * @param {object} item The planned item.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync openItem(item) {\n\t\t\tthis.activeItem = item\n\t\t\tthis.errors = []\n\t\t\tconst cfg = this.config\n\t\t\tconst templateRef = item?.[cfg.templateRefField]\n\t\t\tthis.activeTemplate = templateRef\n\t\t\t\t? await getCachedObject(this.effectiveRegister, cfg.referenceSchema, 'references', String(templateRef))\n\t\t\t\t: null\n\t\t\tconst initial = {}\n\t\t\tfor (const q of (this.activeTemplate?.items || [])) {\n\t\t\t\tinitial[q.questionId] = { answer: '', evidenceRefs: [] }\n\t\t\t}\n\t\t\tthis.answers = initial\n\t\t},\n\n\t\tcloseItem() {\n\t\t\tthis.activeItem = null\n\t\t\tthis.activeTemplate = null\n\t\t\tthis.answers = {}\n\t\t\tthis.errors = []\n\t\t},\n\n\t\t/**\n\t\t * Capture the current GPS fix (tagged on every answer).\n\t\t *\n\t\t * @return {Promise<object|null>} The GPS reading or null.\n\t\t */\n\t\tasync captureGps() {\n\t\t\tif (typeof navigator === 'undefined' || !navigator.geolocation) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn await new Promise((resolve) => {\n\t\t\t\tnavigator.geolocation.getCurrentPosition(\n\t\t\t\t\t(pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude, accuracy: pos.coords.accuracy }),\n\t\t\t\t\t() => resolve(null),\n\t\t\t\t\t{ timeout: 8000 },\n\t\t\t\t)\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Validate answers, then queue a `create` mutation for the result.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync saveChecklist() {\n\t\t\tconst result = validateChecklistAnswers(this.activeTemplate, this.answers)\n\t\t\tthis.errors = result.errors\n\t\t\tif (result.valid === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst gps = await this.captureGps()\n\t\t\t\tconst gpsClass = classifyGps(gps)\n\t\t\t\tconst now = new Date().toISOString()\n\t\t\t\tconst items = this.activeTemplate.items.map((q) => ({\n\t\t\t\t\tquestionId: q.questionId,\n\t\t\t\t\tanswer: this.answers[q.questionId].answer,\n\t\t\t\t\tevidenceRefs: this.answers[q.questionId].evidenceRefs,\n\t\t\t\t\tansweredAt: now,\n\t\t\t\t\tgpsAtAnswer: gps ? { ...gps, source: gpsClass.source } : { source: 'sensorless' },\n\t\t\t\t}))\n\t\t\t\tconst payload = {\n\t\t\t\t\tinspectionRef: this.itemId(this.activeItem),\n\t\t\t\t\tchecklistTemplateRef: this.activeTemplate.id ?? this.activeTemplate['@self']?.id,\n\t\t\t\t\titems,\n\t\t\t\t}\n\t\t\t\tawait enqueueMutation({\n\t\t\t\t\tdeviceId: this.deviceId,\n\t\t\t\t\toperationType: 'create',\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: cfg.resultSchema,\n\t\t\t\t\tpayload,\n\t\t\t\t})\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t\tthis.closeItem()\n\t\t\t\tif (this.offline === false) {\n\t\t\t\t\tawait this.drain()\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] saveChecklist failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 4px 0;\n}\n\n.cn-field-inspection__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection__sync--success .cn-field-inspection__dot { background: var(--color-success); }\n\n.cn-field-inspection__sync--warning .cn-field-inspection__dot { background: var(--color-warning); }\n\n.cn-field-inspection__sync--error .cn-field-inspection__dot { background: var(--color-error); }\n\n.cn-field-inspection__actions {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection__meta {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-field-inspection__row-btn {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\twidth: 100%;\n\tpadding: 12px 14px;\n\tmin-height: 44px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n\tcolor: var(--color-main-text);\n}\n\n.cn-field-inspection__row-btn:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-field-inspection__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__checklist-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-field-inspection__progress {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__question {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tmargin-bottom: 12px;\n}\n\n.cn-field-inspection__input {\n\twidth: 100%;\n\tmin-height: 44px;\n\tmargin-top: 6px;\n}\n\n.cn-field-inspection__required { color: var(--color-error); }\n\n.cn-field-inspection__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.85em;\n\tdisplay: block;\n\tmargin-top: 4px;\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_normalizeClass","_toDisplayString","_createVNode","_createTextVNode","_createElementBlock","_openBlock","_Fragment","_renderList","_vModelSelect","_vModelText"],"mappings":"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"}
1
+ {"version":3,"file":"CnFieldInspectionCard.vue3.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue?vue&type=template&id=471e815e&lang.js"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionCard — surface-aware widget for the `field-inspection`\n - integration leaf (offline field data-collection).\n -\n - Drives the generic offline core (src/integrations/offline): a\n - \"Synchronise day\" action fetches today's planned items via the standard OR\n - object API (planningFetch) into the IndexedDB cache, the planning list lets\n - the user open and complete a checklist offline, and saved answers are queued\n - as OR object mutations that replay on reconnect. The sync-state indicator\n - (online / all-synced / queued / conflict) reflects the pending queue.\n -\n - Generic by construction: the checklist items, the planned-items query and\n - the schema names all come from the integration's `offlineConfig` (resolved\n - from the registry by id) — never from app-specific code in this widget.\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<div class=\"cn-field-inspection\" data-testid=\"cn-field-inspection-card\">\n\t\t\t<div class=\"cn-field-inspection__sync\" :class=\"`cn-field-inspection__sync--${indicator.tone}`\" data-testid=\"cn-fi-sync-indicator\">\n\t\t\t\t<span class=\"cn-field-inspection__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span>{{ indicator.text }}</span>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-field-inspection__actions\">\n\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\tdata-testid=\"cn-fi-sync-day\"\n\t\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t\t@click=\"syncDay\">\n\t\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\t\tdata-testid=\"cn-fi-drain-queue\"\n\t\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t\t@click=\"drain\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 || stuckCount > 0\"\n\t\t\t\t\tdata-testid=\"cn-fi-open-queue\"\n\t\t\t\t\t@click=\"showQueue = !showQueue\">\n\t\t\t\t\t{{ showQueue\n\t\t\t\t\t\t? t('nextcloud-vue', 'Hide what is waiting')\n\t\t\t\t\t\t: t('nextcloud-vue', 'See what is waiting') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- 🔴 AN EXPIRED PLANNING USED TO READ AS A CURRENT ONE. This\n\t\t\t\t line only ever said \"Ready offline until\", in the past tense\n\t\t\t\t or not, so a planning downloaded three days ago looked the\n\t\t\t\t same as one downloaded an hour ago. In the field there is\n\t\t\t\t nothing else to check it against. -->\n\t\t\t\t<span v-if=\"planningMeta\" class=\"cn-field-inspection__meta\" data-testid=\"cn-fi-planning-meta\">\n\t\t\t\t\t{{ planningIsStale\n\t\t\t\t\t\t? t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: formatTime(planningMeta.syncedAt) })\n\t\t\t\t\t\t: t('nextcloud-vue', 'Ready offline until {time}', { time: formatTime(planningMeta.expiresAt) }) }}\n\t\t\t\t</span>\n\t\t\t</div>\n\n\t\t\t<!-- The card links to the queue rather than owning a second copy\n\t\t\t of it: one component, one set of statuses, one set of words. -->\n\t\t\t<CnOfflineQueue v-if=\"showQueue\" :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\n\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t\t<NcEmptyContent v-else-if=\"!activeItem && plannedItems.length === 0\"\n\t\t\t\tdata-testid=\"cn-fi-empty\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'No items planned')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.')\" />\n\n\t\t\t<!-- Planning list -->\n\t\t\t<ul v-else-if=\"!activeItem\" class=\"cn-field-inspection__list\" data-testid=\"cn-fi-list\">\n\t\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection__row\">\n\t\t\t\t\t<button type=\"button\"\n\t\t\t\t\t\tclass=\"cn-field-inspection__row-btn\"\n\t\t\t\t\t\t:data-testid=\"`cn-fi-open-${itemId(item)}`\"\n\t\t\t\t\t\t@click=\"openItem(item)\">\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t\t\t</button>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<!-- Checklist completion for one item -->\n\t\t\t<div v-else class=\"cn-field-inspection__checklist\" data-testid=\"cn-fi-checklist\">\n\t\t\t\t<div class=\"cn-field-inspection__checklist-head\">\n\t\t\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-fi-back\" @click=\"closeItem\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<strong>{{ itemTitle(activeItem) }}</strong>\n\t\t\t\t</div>\n\n\t\t\t\t<p v-if=\"activeTemplate\" class=\"cn-field-inspection__progress\" data-testid=\"cn-fi-progress\">\n\t\t\t\t\t{{ t('nextcloud-vue', '{done} of {total} questions completed', { done: progress.done, total: progress.total }) }}\n\t\t\t\t</p>\n\n\t\t\t\t<form v-if=\"activeTemplate\" @submit.prevent=\"saveChecklist\">\n\t\t\t\t\t<fieldset v-for=\"q in activeTemplate.items\" :key=\"q.questionId\" class=\"cn-field-inspection__question\">\n\t\t\t\t\t\t<legend>\n\t\t\t\t\t\t\t{{ q.text }}<span v-if=\"q.required\" class=\"cn-field-inspection__required\" aria-hidden=\"true\"> *</span>\n\t\t\t\t\t\t</legend>\n\t\t\t\t\t\t<select v-if=\"q.type === 'yes_no'\"\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\">\n\t\t\t\t\t\t\t<option value=\"\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '— choose —') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"yes\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Yes') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"no\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'No') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"na\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'N/A') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<textarea v-else\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\"\n\t\t\t\t\t\t\trows=\"2\" />\n\t\t\t\t\t\t<span v-if=\"errorFor(q.questionId)\" class=\"cn-field-inspection__error\" :data-testid=\"`cn-fi-error-${q.questionId}`\">\n\t\t\t\t\t\t\t{{ errorFor(q.questionId) }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</fieldset>\n\n\t\t\t\t\t<!-- `type=\"submit\"` (native HTML type) + `variant=\"primary\"` (style) —\n\t\t\t\t\t @nextcloud/vue 9 split what Vue-2-era code conflated: `type` is now\n\t\t\t\t\t the native button type (`ButtonType`), and the old style-variant\n\t\t\t\t\t prop is `variant`. This line used to pass BOTH wrong: `native-type`\n\t\t\t\t\t (not a real prop — the button silently defaulted to `type=\"button\"`\n\t\t\t\t\t and never fired the form's native submit) and `variant=\"primary\"`\n\t\t\t\t\t (an invalid `ButtonType`, so the visual primary style never applied\n\t\t\t\t\t either). See CnFormPage.vue's inline comment for the submit-button\n\t\t\t\t\t half of this bug in detail. -->\n\t\t\t\t\t<p v-if=\"saveError\"\n\t\t\t\t\t\tclass=\"cn-field-inspection__error\"\n\t\t\t\t\t\trole=\"alert\"\n\t\t\t\t\t\tdata-testid=\"cn-fi-save-error\">\n\t\t\t\t\t\t{{ saveError }}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<NcButton type=\"submit\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"cn-fi-save\"\n\t\t\t\t\t\t:disabled=\"saving\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Save answers offline') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</form>\n\n\t\t\t\t<NcEmptyContent v-else\n\t\t\t\t\tdata-testid=\"cn-fi-no-template\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'Checklist not available offline')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'Synchronise the day while online to download this checklist.')\" />\n\t\t\t</div>\n\t\t</div>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { buildChecklistSubmission } from '../../offline/checklistSubmission.js'\nimport { checklistProgress, classifyGps, normaliseChecklistTemplate, syncIndicator, validateChecklistAnswers } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnFieldInspectionCard — surface-aware offline field data-collection widget.\n *\n * Renders the offline planning list and inline checklist completion across the\n * dashboard / detail surfaces, driven entirely by the generic offline core and\n * the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionCard',\n\n\tcomponents: { CnDetailCard, CnOfflineQueue, NcButton, NcLoadingIcon, NcEmptyContent },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/* eslint-disable vue/no-unused-properties -- every integration surface is handed `surface` by its host; this card renders one layout and does not branch on it */\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/** Object context `{ register, schema, objectId }` forwarded by the host page. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** OpenRegister register id (slug or uuid) — falls back to integrationContext. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid) — falls back to integrationContext. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Field inspections') },\n\t\t/** Optional Material Design Icon component for the card header. */\n\t\ticon: { type: [Object, Function], default: () => ClipboardCheckOutline },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tplanningMeta: null,\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** Whether the queue list is open under the card. */\n\t\t\tshowQueue: false,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\tsaving: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t\tactiveItem: null,\n\t\t\tactiveTemplate: null,\n\t\t\tanswers: {},\n\t\t\terrors: [],\n\t\t\t/** Why the last save could not be queued, in words. */\n\t\t\tsaveError: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry, falling back to the canonical defaults. A consuming app's\n\t\t * override supplies its own schema names / filter fields here. Read from\n\t\t * the global registry (not a static import) to avoid an import cycle.\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective register (prop wins, else integrationContext).\n\t\t *\n\t\t * @return {string} The register id.\n\t\t */\n\t\teffectiveRegister() {\n\t\t\treturn this.register || this.integrationContext.register || ''\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else context/prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || this.integrationContext.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.effectiveRegister,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\n\t\tprogress() {\n\t\t\treturn checklistProgress(this.activeTemplate, this.answers)\n\t\t},\n\t},\n\n\twatch: {\n\t\tintegrationContext: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners (auto-drain on reconnect) + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\tformatTime(iso) {\n\t\t\tif (!iso) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn new Date(iso).toLocaleString()\n\t\t},\n\n\t\terrorFor(questionId) {\n\t\t\tconst e = this.errors.find((x) => x.questionId === questionId)\n\t\t\treturn e ? e.message : ''\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.effectiveRegister || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning (planned items + referenced templates) via\n\t\t * the standard OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.effectiveRegister, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open one planned item and load its checklist template from cache.\n\t\t *\n\t\t * @param {object} item The planned item.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync openItem(item) {\n\t\t\tthis.activeItem = item\n\t\t\tthis.errors = []\n\t\t\tconst cfg = this.config\n\t\t\tconst templateRef = item?.[cfg.templateRefField]\n\t\t\t// The app's template shape (sections, its own item keys and type\n\t\t\t// words) is read into the leaf's flat item list here, once.\n\t\t\tthis.activeTemplate = templateRef\n\t\t\t\t? normaliseChecklistTemplate(\n\t\t\t\t\t\tawait getCachedObject(this.effectiveRegister, cfg.referenceSchema, 'references', String(templateRef)),\n\t\t\t\t\t\tcfg,\n\t\t\t\t\t)\n\t\t\t\t: null\n\t\t\tconst initial = {}\n\t\t\tfor (const q of (this.activeTemplate?.items || [])) {\n\t\t\t\tinitial[q.questionId] = { answer: '', evidenceRefs: [] }\n\t\t\t}\n\t\t\tthis.answers = initial\n\t\t},\n\n\t\tcloseItem() {\n\t\t\tthis.activeItem = null\n\t\t\tthis.activeTemplate = null\n\t\t\tthis.answers = {}\n\t\t\tthis.errors = []\n\t\t\tthis.saveError = ''\n\t\t},\n\n\t\t/**\n\t\t * Capture the current GPS fix (tagged on every answer).\n\t\t *\n\t\t * @return {Promise<object|null>} The GPS reading or null.\n\t\t */\n\t\tasync captureGps() {\n\t\t\tif (typeof navigator === 'undefined' || !navigator.geolocation) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn await new Promise((resolve) => {\n\t\t\t\tnavigator.geolocation.getCurrentPosition(\n\t\t\t\t\t(pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude, accuracy: pos.coords.accuracy }),\n\t\t\t\t\t() => resolve(null),\n\t\t\t\t\t{ timeout: 8000 },\n\t\t\t\t)\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Validate answers, then queue the run: an object create on\n\t\t * `resultSchema`, or, when the config names a `resultEndpoint`, one\n\t\t * `submit` to that endpoint.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync saveChecklist() {\n\t\t\tconst result = validateChecklistAnswers(this.activeTemplate, this.answers)\n\t\t\tthis.errors = result.errors\n\t\t\tif (result.valid === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst gps = await this.captureGps()\n\t\t\t\tconst submission = buildChecklistSubmission(cfg, {\n\t\t\t\t\tplannedItem: this.activeItem,\n\t\t\t\t\ttemplate: this.activeTemplate,\n\t\t\t\t\tanswers: this.answers,\n\t\t\t\t\tgps,\n\t\t\t\t\tgpsSource: classifyGps(gps).source,\n\t\t\t\t\tcapturedAt: new Date().toISOString(),\n\t\t\t\t\tcapturedOffline: this.offline,\n\t\t\t\t})\n\t\t\t\tawait enqueueMutation({\n\t\t\t\t\tdeviceId: this.deviceId,\n\t\t\t\t\toperationType: submission.operationType,\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: submission.schema,\n\t\t\t\t\tendpoint: submission.endpoint,\n\t\t\t\t\tpayload: submission.payload,\n\t\t\t\t})\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t\tthis.closeItem()\n\t\t\t\tif (this.offline === false) {\n\t\t\t\t\tawait this.drain()\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tthis.saveError = e?.message || t('nextcloud-vue', 'The answers could not be saved on this device.')\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] saveChecklist failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 4px 0;\n}\n\n.cn-field-inspection__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection__sync--success .cn-field-inspection__dot { background: var(--color-success); }\n\n.cn-field-inspection__sync--warning .cn-field-inspection__dot { background: var(--color-warning); }\n\n.cn-field-inspection__sync--error .cn-field-inspection__dot { background: var(--color-error); }\n\n.cn-field-inspection__actions {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection__meta {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-field-inspection__row-btn {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\twidth: 100%;\n\tpadding: 12px 14px;\n\tmin-height: 44px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n\tcolor: var(--color-main-text);\n}\n\n.cn-field-inspection__row-btn:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-field-inspection__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__checklist-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-field-inspection__progress {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__question {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tmargin-bottom: 12px;\n}\n\n.cn-field-inspection__input {\n\twidth: 100%;\n\tmin-height: 44px;\n\tmargin-top: 6px;\n}\n\n.cn-field-inspection__required { color: var(--color-error); }\n\n.cn-field-inspection__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.85em;\n\tdisplay: block;\n\tmargin-top: 4px;\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_normalizeClass","_toDisplayString","_createVNode","_createTextVNode","_createElementBlock","_openBlock","_Fragment","_renderList","_vModelSelect","_vModelText"],"mappings":"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"}
@@ -57,6 +57,27 @@ const DEFAULT_FIELD_INSPECTION_CONFIG = {
57
57
  // Schema holding conflict records. Empty keeps a conflict on the device,
58
58
  // and the queue row says so rather than pretending it was filed.
59
59
  conflictSchema: '',
60
+ // How a template's items are read (normaliseChecklistTemplate). Empty
61
+ // `sectionsField` reads a flat `items[]`; set it when the items sit inside
62
+ // ordered sections, each with its own `items[]`.
63
+ sectionsField: '',
64
+ itemKeyField: 'questionId',
65
+ itemTextField: 'text',
66
+ itemTypeField: 'type',
67
+ // The app's own item type words onto the leaf's (`yes_no`,
68
+ // `photo_required`; anything else renders as free text).
69
+ itemTypeMap: {},
70
+ // An item whose `photoRequiredField` equals `photoRequiredValue` needs a
71
+ // photo as well as an answer. Empty switches the gate off.
72
+ photoRequiredField: '',
73
+ photoRequiredValue: '',
74
+ // Where a finished run goes (checklistSubmission). Empty queues an object
75
+ // create on `resultSchema`. Set, it is a path under the Nextcloud root with
76
+ // `{field}` placeholders from the planned item, and the run replays as one
77
+ // POST there, naming the template and the planned item under these keys.
78
+ resultEndpoint: '',
79
+ resultTemplateParam: 'templateId',
80
+ resultPlannedItemParam: 'plannedItem',
60
81
  };
61
82
 
62
83
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"field-inspection.js","sources":["../../../../src/integrations/builtin/field-inspection.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Built-in `field-inspection` integration registration (offline field\n * data-collection leaf).\n *\n * Surfaces the generic offline data-collection core (IndexedDB cache, mutation\n * queue, replay-on-reconnect, sync-state indicator — see\n * `src/integrations/offline/`) as a registry leaf with a sidebar tab and a\n * surface-aware widget. The checklist fields and the \"today's planned items\"\n * query are NOT hardcoded: they come from the integration's `offlineConfig`,\n * which a consuming app supplies when it registers an override of this id (or\n * relies on the per-object `integrationContext` for register/schema).\n *\n * Default registration uses the canonical `fieldInspection` / `checklistResult`\n * schema names; a consuming app (e.g. procest) registers the SAME id first with\n * its own `offlineConfig` to point the leaf at its schemas — AD-13 first-wins\n * collision policy keeps the consumer's config.\n *\n * @module integrations/builtin/field-inspection\n */\n\nimport { translate as t } from '@nextcloud/l10n'\nimport CnFieldInspectionCard from './field-inspection/CnFieldInspectionCard.vue'\nimport CnFieldInspectionTab from './field-inspection/CnFieldInspectionTab.vue'\n\n/**\n * Default offline configuration for the field-inspection leaf.\n *\n * A consuming app overrides any of these by registering the `field-inspection`\n * id with its own `offlineConfig` before `registerBuiltinIntegrations()` runs.\n *\n * @type {object}\n */\nexport const DEFAULT_FIELD_INSPECTION_CONFIG = {\n\t// Schema holding the planned items (the inspections to do today).\n\tplannedSchema: 'fieldInspection',\n\t// Schema holding the checklist templates referenced by planned items.\n\treferenceSchema: 'inspectionChecklist',\n\t// Property on a planned item that references its checklist template.\n\ttemplateRefField: 'checklistTemplateRef',\n\t// Schema the completed checklist result is written back to (the queued create).\n\tresultSchema: 'checklistResult',\n\t// Planning filter: property holding the assignee + the scheduled-date field.\n\tassigneeField: 'inspectorRef',\n\tdateField: 'scheduledAt',\n\t// Property on a planned item used as its display title.\n\ttitleField: 'caseRef',\n\t// Register the leaf writes its own bookkeeping to. Empty falls back to the\n\t// register each queued operation already names.\n\tregister: '',\n\t// Schema holding queue records, for a consuming app that wants the queue\n\t// itself readable server-side. Empty writes nothing.\n\tqueueSchema: '',\n\t// Schema holding conflict records. Empty keeps a conflict on the device,\n\t// and the queue row says so rather than pretending it was filed.\n\tconflictSchema: '',\n}\n\n/**\n * `field-inspection` integration descriptor.\n *\n * @type {object}\n */\nexport const fieldInspectionIntegration = {\n\tid: 'field-inspection',\n\tlabel: t('nextcloud-vue', 'Field inspections'),\n\tappName: t('nextcloud-vue', 'Field inspections'),\n\ticon: 'ClipboardCheckOutline',\n\torder: 68,\n\tgroup: 'workflow',\n\treferenceType: 'field-inspection',\n\taccentColor: '#21468B',\n\tofflineConfig: DEFAULT_FIELD_INSPECTION_CONFIG,\n\ttab: CnFieldInspectionTab,\n\twidget: CnFieldInspectionCard,\n\tdefaultSize: { w: 4, h: 4 },\n}\n\nexport default fieldInspectionIntegration\n"],"names":["t","CnFieldInspectionTab","CnFieldInspectionCard"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,+BAA+B,GAAG;AAC/C;AACA,CAAC,aAAa,EAAE,iBAAiB;AACjC;AACA,CAAC,eAAe,EAAE,qBAAqB;AACvC;AACA,CAAC,gBAAgB,EAAE,sBAAsB;AACzC;AACA,CAAC,YAAY,EAAE,iBAAiB;AAChC;AACA,CAAC,aAAa,EAAE,cAAc;AAC9B,CAAC,SAAS,EAAE,aAAa;AACzB;AACA,CAAC,UAAU,EAAE,SAAS;AACtB;AACA;AACA,CAAC,QAAQ,EAAE,EAAE;AACb;AACA;AACA,CAAC,WAAW,EAAE,EAAE;AAChB;AACA;AACA,CAAC,cAAc,EAAE,EAAE;AACnB,EAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,0BAA0B,GAAG;AAC1C,CAAC,EAAE,EAAE,kBAAkB;AACvB,CAAC,KAAK,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AAC/C,CAAC,OAAO,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AACjD,CAAC,IAAI,EAAE,uBAAuB;AAC9B,CAAC,KAAK,EAAE,EAAE;AACV,CAAC,KAAK,EAAE,UAAU;AAClB,CAAC,aAAa,EAAE,kBAAkB;AAClC,CAAC,WAAW,EAAE,SAAS;AACvB,CAAC,aAAa,EAAE,+BAA+B;AAC/C,CAAC,GAAG,EAAEC,UAAoB;AAC1B,CAAC,MAAM,EAAEC,YAAqB;AAC9B,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AAC5B;;;;"}
1
+ {"version":3,"file":"field-inspection.js","sources":["../../../../src/integrations/builtin/field-inspection.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Built-in `field-inspection` integration registration (offline field\n * data-collection leaf).\n *\n * Surfaces the generic offline data-collection core (IndexedDB cache, mutation\n * queue, replay-on-reconnect, sync-state indicator — see\n * `src/integrations/offline/`) as a registry leaf with a sidebar tab and a\n * surface-aware widget. The checklist fields and the \"today's planned items\"\n * query are NOT hardcoded: they come from the integration's `offlineConfig`,\n * which a consuming app supplies when it registers an override of this id (or\n * relies on the per-object `integrationContext` for register/schema).\n *\n * Default registration uses the canonical `fieldInspection` / `checklistResult`\n * schema names; a consuming app (e.g. procest) registers the SAME id first with\n * its own `offlineConfig` to point the leaf at its schemas — AD-13 first-wins\n * collision policy keeps the consumer's config.\n *\n * @module integrations/builtin/field-inspection\n */\n\nimport { translate as t } from '@nextcloud/l10n'\nimport CnFieldInspectionCard from './field-inspection/CnFieldInspectionCard.vue'\nimport CnFieldInspectionTab from './field-inspection/CnFieldInspectionTab.vue'\n\n/**\n * Default offline configuration for the field-inspection leaf.\n *\n * A consuming app overrides any of these by registering the `field-inspection`\n * id with its own `offlineConfig` before `registerBuiltinIntegrations()` runs.\n *\n * @type {object}\n */\nexport const DEFAULT_FIELD_INSPECTION_CONFIG = {\n\t// Schema holding the planned items (the inspections to do today).\n\tplannedSchema: 'fieldInspection',\n\t// Schema holding the checklist templates referenced by planned items.\n\treferenceSchema: 'inspectionChecklist',\n\t// Property on a planned item that references its checklist template.\n\ttemplateRefField: 'checklistTemplateRef',\n\t// Schema the completed checklist result is written back to (the queued create).\n\tresultSchema: 'checklistResult',\n\t// Planning filter: property holding the assignee + the scheduled-date field.\n\tassigneeField: 'inspectorRef',\n\tdateField: 'scheduledAt',\n\t// Property on a planned item used as its display title.\n\ttitleField: 'caseRef',\n\t// Register the leaf writes its own bookkeeping to. Empty falls back to the\n\t// register each queued operation already names.\n\tregister: '',\n\t// Schema holding queue records, for a consuming app that wants the queue\n\t// itself readable server-side. Empty writes nothing.\n\tqueueSchema: '',\n\t// Schema holding conflict records. Empty keeps a conflict on the device,\n\t// and the queue row says so rather than pretending it was filed.\n\tconflictSchema: '',\n\t// How a template's items are read (normaliseChecklistTemplate). Empty\n\t// `sectionsField` reads a flat `items[]`; set it when the items sit inside\n\t// ordered sections, each with its own `items[]`.\n\tsectionsField: '',\n\titemKeyField: 'questionId',\n\titemTextField: 'text',\n\titemTypeField: 'type',\n\t// The app's own item type words onto the leaf's (`yes_no`,\n\t// `photo_required`; anything else renders as free text).\n\titemTypeMap: {},\n\t// An item whose `photoRequiredField` equals `photoRequiredValue` needs a\n\t// photo as well as an answer. Empty switches the gate off.\n\tphotoRequiredField: '',\n\tphotoRequiredValue: '',\n\t// Where a finished run goes (checklistSubmission). Empty queues an object\n\t// create on `resultSchema`. Set, it is a path under the Nextcloud root with\n\t// `{field}` placeholders from the planned item, and the run replays as one\n\t// POST there, naming the template and the planned item under these keys.\n\tresultEndpoint: '',\n\tresultTemplateParam: 'templateId',\n\tresultPlannedItemParam: 'plannedItem',\n}\n\n/**\n * `field-inspection` integration descriptor.\n *\n * @type {object}\n */\nexport const fieldInspectionIntegration = {\n\tid: 'field-inspection',\n\tlabel: t('nextcloud-vue', 'Field inspections'),\n\tappName: t('nextcloud-vue', 'Field inspections'),\n\ticon: 'ClipboardCheckOutline',\n\torder: 68,\n\tgroup: 'workflow',\n\treferenceType: 'field-inspection',\n\taccentColor: '#21468B',\n\tofflineConfig: DEFAULT_FIELD_INSPECTION_CONFIG,\n\ttab: CnFieldInspectionTab,\n\twidget: CnFieldInspectionCard,\n\tdefaultSize: { w: 4, h: 4 },\n}\n\nexport default fieldInspectionIntegration\n"],"names":["t","CnFieldInspectionTab","CnFieldInspectionCard"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,+BAA+B,GAAG;AAC/C;AACA,CAAC,aAAa,EAAE,iBAAiB;AACjC;AACA,CAAC,eAAe,EAAE,qBAAqB;AACvC;AACA,CAAC,gBAAgB,EAAE,sBAAsB;AACzC;AACA,CAAC,YAAY,EAAE,iBAAiB;AAChC;AACA,CAAC,aAAa,EAAE,cAAc;AAC9B,CAAC,SAAS,EAAE,aAAa;AACzB;AACA,CAAC,UAAU,EAAE,SAAS;AACtB;AACA;AACA,CAAC,QAAQ,EAAE,EAAE;AACb;AACA;AACA,CAAC,WAAW,EAAE,EAAE;AAChB;AACA;AACA,CAAC,cAAc,EAAE,EAAE;AACnB;AACA;AACA;AACA,CAAC,aAAa,EAAE,EAAE;AAClB,CAAC,YAAY,EAAE,YAAY;AAC3B,CAAC,aAAa,EAAE,MAAM;AACtB,CAAC,aAAa,EAAE,MAAM;AACtB;AACA;AACA,CAAC,WAAW,EAAE,EAAE;AAChB;AACA;AACA,CAAC,kBAAkB,EAAE,EAAE;AACvB,CAAC,kBAAkB,EAAE,EAAE;AACvB;AACA;AACA;AACA;AACA,CAAC,cAAc,EAAE,EAAE;AACnB,CAAC,mBAAmB,EAAE,YAAY;AAClC,CAAC,sBAAsB,EAAE,aAAa;AACtC,EAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,0BAA0B,GAAG;AAC1C,CAAC,EAAE,EAAE,kBAAkB;AACvB,CAAC,KAAK,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AAC/C,CAAC,OAAO,EAAEA,SAAC,CAAC,eAAe,EAAE,mBAAmB,CAAC;AACjD,CAAC,IAAI,EAAE,uBAAuB;AAC9B,CAAC,KAAK,EAAE,EAAE;AACV,CAAC,KAAK,EAAE,UAAU;AAClB,CAAC,aAAa,EAAE,kBAAkB;AAClC,CAAC,WAAW,EAAE,SAAS;AACvB,CAAC,aAAa,EAAE,+BAA+B;AAC/C,CAAC,GAAG,EAAEC,UAAoB;AAC1B,CAAC,MAAM,EAAEC,YAAqB;AAC9B,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AAC5B;;;;"}
@@ -0,0 +1,141 @@
1
+ import { translate } from '@nextcloud/l10n';
2
+
3
+ /**
4
+ * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
5
+ * SPDX-License-Identifier: EUPL-1.2
6
+ *
7
+ * What a finished offline checklist run turns into in the queue.
8
+ *
9
+ * Two shapes, chosen by the leaf's `offlineConfig`:
10
+ *
11
+ * - `resultEndpoint` empty: an object create on `resultSchema`, the shape the
12
+ * leaf has always queued (`inspectionRef`, `checklistTemplateRef`,
13
+ * `items[]` with `questionId` / `answer` / `evidenceRefs`).
14
+ * - `resultEndpoint` set: a `submit` operation that replays the run as ONE
15
+ * POST to an app's own endpoint, for an app that stores a run as something
16
+ * other than a plain object (a completed task, say) and checks the run
17
+ * server side before it stores anything. The endpoint is a path under the
18
+ * Nextcloud root with `{field}` placeholders filled from the planned item,
19
+ * e.g. `/apps/myapp/api/cases/{caseRef}/checklist-run`.
20
+ *
21
+ * Pure: no axios, no IndexedDB, no DOM. The card queues what this returns and
22
+ * the replay service sends it.
23
+ *
24
+ * @module integrations/offline/checklistSubmission
25
+ */
26
+
27
+
28
+ /**
29
+ * Fill a `{field}` endpoint template from a planned item.
30
+ *
31
+ * Each value is URL-encoded, so a planned item can never steer the request to
32
+ * another path. A placeholder the item does not fill is an error rather than
33
+ * an empty path segment: a run sent to the wrong case is worse than one that
34
+ * waits.
35
+ *
36
+ * @param {string} template The endpoint template.
37
+ * @param {object} plannedItem The planned item the run belongs to.
38
+ *
39
+ * @return {string} The filled path.
40
+ *
41
+ * @throws {Error} When a placeholder has no value on the item.
42
+ */
43
+ function fillEndpoint(template, plannedItem) {
44
+ return String(template).replace(/\{([A-Za-z0-9_.]+)\}/g, (match, field) => {
45
+ const value = field.split('.').reduce((node, key) => (node === null || node === undefined ? undefined : node[key]), plannedItem);
46
+ if (value === undefined || value === null || String(value) === '') {
47
+ throw new Error(translate('nextcloud-vue', 'This planned item has no {field}, so the answers have nowhere to go.', { field }))
48
+ }
49
+ return encodeURIComponent(String(value))
50
+ })
51
+ }
52
+
53
+ /**
54
+ * The id of an object, wherever OpenRegister carries it.
55
+ *
56
+ * @param {object|null} object The object.
57
+ *
58
+ * @return {string} The id, or an empty string.
59
+ */
60
+ function idOf(object) {
61
+ return String(object?.id ?? object?.['@self']?.id ?? object?.uuid ?? '')
62
+ }
63
+
64
+ /**
65
+ * Build the queue operation for a finished checklist run.
66
+ *
67
+ * @param {object} config The leaf's offline config.
68
+ * @param {object} run The run.
69
+ * @param {object} run.plannedItem The planned item.
70
+ * @param {object} run.template The normalised template (`items[]`).
71
+ * @param {object} run.answers Map of questionId to `{ answer, evidenceRefs }`.
72
+ * @param {object|null} [run.gps] The GPS fix at save, or null.
73
+ * @param {string} [run.gpsSource] `sensor` or `sensorless`.
74
+ * @param {string} [run.capturedAt] ISO time of the save.
75
+ * @param {boolean} [run.capturedOffline] Whether the device was offline at save.
76
+ *
77
+ * @return {{ operationType: string, schema: string, endpoint: (string|null), payload: object }}
78
+ * The operation fields for `enqueueMutation`.
79
+ *
80
+ * @throws {Error} When the endpoint cannot be filled from the planned item.
81
+ */
82
+ function buildChecklistSubmission(config, run) {
83
+ const capturedAt = run.capturedAt || new Date().toISOString();
84
+ const gpsAtAnswer = run.gps ? { ...run.gps, source: run.gpsSource || 'sensor' } : { source: 'sensorless' };
85
+ const items = Array.isArray(run.template?.items) ? run.template.items : [];
86
+ const answers = run.answers ?? {};
87
+ const plannedItemId = idOf(run.plannedItem);
88
+ const templateId = idOf(run.template);
89
+
90
+ if (!config.resultEndpoint) {
91
+ return {
92
+ operationType: 'create',
93
+ schema: config.resultSchema,
94
+ endpoint: null,
95
+ payload: {
96
+ inspectionRef: plannedItemId,
97
+ checklistTemplateRef: templateId,
98
+ items: items.map((q) => ({
99
+ questionId: q.questionId,
100
+ answer: answers[q.questionId]?.answer,
101
+ evidenceRefs: answers[q.questionId]?.evidenceRefs ?? [],
102
+ answeredAt: capturedAt,
103
+ gpsAtAnswer,
104
+ })),
105
+ },
106
+ }
107
+ }
108
+
109
+ const payload = {
110
+ [config.resultTemplateParam || 'templateId']: templateId,
111
+ [config.resultPlannedItemParam || 'plannedItem']: plannedItemId,
112
+ capturedAt,
113
+ capturedOffline: run.capturedOffline === true,
114
+ items: items
115
+ .filter((q) => {
116
+ const entry = answers[q.questionId] ?? {};
117
+ const hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== '';
118
+ return hasAnswer || (Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0)
119
+ })
120
+ .map((q) => ({
121
+ itemId: q.questionId,
122
+ value: answers[q.questionId]?.answer ?? '',
123
+ photos: answers[q.questionId]?.evidenceRefs ?? [],
124
+ answeredAt: capturedAt,
125
+ gpsAtAnswer,
126
+ })),
127
+ };
128
+ if (run.gps) {
129
+ payload.location = { ...run.gps, source: run.gpsSource || 'sensor' };
130
+ }
131
+
132
+ return {
133
+ operationType: 'submit',
134
+ schema: config.resultSchema || '',
135
+ endpoint: fillEndpoint(config.resultEndpoint, run.plannedItem),
136
+ payload,
137
+ }
138
+ }
139
+
140
+ export { buildChecklistSubmission, fillEndpoint };
141
+ //# sourceMappingURL=checklistSubmission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checklistSubmission.js","sources":["../../../../src/integrations/offline/checklistSubmission.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * What a finished offline checklist run turns into in the queue.\n *\n * Two shapes, chosen by the leaf's `offlineConfig`:\n *\n * - `resultEndpoint` empty: an object create on `resultSchema`, the shape the\n * leaf has always queued (`inspectionRef`, `checklistTemplateRef`,\n * `items[]` with `questionId` / `answer` / `evidenceRefs`).\n * - `resultEndpoint` set: a `submit` operation that replays the run as ONE\n * POST to an app's own endpoint, for an app that stores a run as something\n * other than a plain object (a completed task, say) and checks the run\n * server side before it stores anything. The endpoint is a path under the\n * Nextcloud root with `{field}` placeholders filled from the planned item,\n * e.g. `/apps/myapp/api/cases/{caseRef}/checklist-run`.\n *\n * Pure: no axios, no IndexedDB, no DOM. The card queues what this returns and\n * the replay service sends it.\n *\n * @module integrations/offline/checklistSubmission\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/**\n * Fill a `{field}` endpoint template from a planned item.\n *\n * Each value is URL-encoded, so a planned item can never steer the request to\n * another path. A placeholder the item does not fill is an error rather than\n * an empty path segment: a run sent to the wrong case is worse than one that\n * waits.\n *\n * @param {string} template The endpoint template.\n * @param {object} plannedItem The planned item the run belongs to.\n *\n * @return {string} The filled path.\n *\n * @throws {Error} When a placeholder has no value on the item.\n */\nexport function fillEndpoint(template, plannedItem) {\n\treturn String(template).replace(/\\{([A-Za-z0-9_.]+)\\}/g, (match, field) => {\n\t\tconst value = field.split('.').reduce((node, key) => (node === null || node === undefined ? undefined : node[key]), plannedItem)\n\t\tif (value === undefined || value === null || String(value) === '') {\n\t\t\tthrow new Error(t('nextcloud-vue', 'This planned item has no {field}, so the answers have nowhere to go.', { field }))\n\t\t}\n\t\treturn encodeURIComponent(String(value))\n\t})\n}\n\n/**\n * The id of an object, wherever OpenRegister carries it.\n *\n * @param {object|null} object The object.\n *\n * @return {string} The id, or an empty string.\n */\nfunction idOf(object) {\n\treturn String(object?.id ?? object?.['@self']?.id ?? object?.uuid ?? '')\n}\n\n/**\n * Build the queue operation for a finished checklist run.\n *\n * @param {object} config The leaf's offline config.\n * @param {object} run The run.\n * @param {object} run.plannedItem The planned item.\n * @param {object} run.template The normalised template (`items[]`).\n * @param {object} run.answers Map of questionId to `{ answer, evidenceRefs }`.\n * @param {object|null} [run.gps] The GPS fix at save, or null.\n * @param {string} [run.gpsSource] `sensor` or `sensorless`.\n * @param {string} [run.capturedAt] ISO time of the save.\n * @param {boolean} [run.capturedOffline] Whether the device was offline at save.\n *\n * @return {{ operationType: string, schema: string, endpoint: (string|null), payload: object }}\n * The operation fields for `enqueueMutation`.\n *\n * @throws {Error} When the endpoint cannot be filled from the planned item.\n */\nexport function buildChecklistSubmission(config, run) {\n\tconst capturedAt = run.capturedAt || new Date().toISOString()\n\tconst gpsAtAnswer = run.gps ? { ...run.gps, source: run.gpsSource || 'sensor' } : { source: 'sensorless' }\n\tconst items = Array.isArray(run.template?.items) ? run.template.items : []\n\tconst answers = run.answers ?? {}\n\tconst plannedItemId = idOf(run.plannedItem)\n\tconst templateId = idOf(run.template)\n\n\tif (!config.resultEndpoint) {\n\t\treturn {\n\t\t\toperationType: 'create',\n\t\t\tschema: config.resultSchema,\n\t\t\tendpoint: null,\n\t\t\tpayload: {\n\t\t\t\tinspectionRef: plannedItemId,\n\t\t\t\tchecklistTemplateRef: templateId,\n\t\t\t\titems: items.map((q) => ({\n\t\t\t\t\tquestionId: q.questionId,\n\t\t\t\t\tanswer: answers[q.questionId]?.answer,\n\t\t\t\t\tevidenceRefs: answers[q.questionId]?.evidenceRefs ?? [],\n\t\t\t\t\tansweredAt: capturedAt,\n\t\t\t\t\tgpsAtAnswer,\n\t\t\t\t})),\n\t\t\t},\n\t\t}\n\t}\n\n\tconst payload = {\n\t\t[config.resultTemplateParam || 'templateId']: templateId,\n\t\t[config.resultPlannedItemParam || 'plannedItem']: plannedItemId,\n\t\tcapturedAt,\n\t\tcapturedOffline: run.capturedOffline === true,\n\t\titems: items\n\t\t\t.filter((q) => {\n\t\t\t\tconst entry = answers[q.questionId] ?? {}\n\t\t\t\tconst hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== ''\n\t\t\t\treturn hasAnswer || (Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0)\n\t\t\t})\n\t\t\t.map((q) => ({\n\t\t\t\titemId: q.questionId,\n\t\t\t\tvalue: answers[q.questionId]?.answer ?? '',\n\t\t\t\tphotos: answers[q.questionId]?.evidenceRefs ?? [],\n\t\t\t\tansweredAt: capturedAt,\n\t\t\t\tgpsAtAnswer,\n\t\t\t})),\n\t}\n\tif (run.gps) {\n\t\tpayload.location = { ...run.gps, source: run.gpsSource || 'sensor' }\n\t}\n\n\treturn {\n\t\toperationType: 'submit',\n\t\tschema: config.resultSchema || '',\n\t\tendpoint: fillEndpoint(config.resultEndpoint, run.plannedItem),\n\t\tpayload,\n\t}\n}\n"],"names":["t"],"mappings":"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"}
@@ -47,11 +47,71 @@ function classifyGps(fix, available = true) {
47
47
  return { quality: 'good', source: 'sensor', warning: null }
48
48
  }
49
49
 
50
+ /**
51
+ * Read a checklist template into the leaf's flat item contract.
52
+ *
53
+ * A consuming app's template does not have to carry `items[]` with
54
+ * `questionId` / `text` / `type`. Its `offlineConfig` names where those live:
55
+ * `sectionsField` when the items sit inside ordered sections, and the
56
+ * `item*Field` keys for the item's key, text and type. `itemTypeMap` translates
57
+ * the app's own type words onto the leaf's (`yes_no`, `photo_required`, or any
58
+ * other word, which renders as free text). `photoRequiredField` and
59
+ * `photoRequiredValue` mark an item whose answer also needs a photo.
60
+ *
61
+ * The template's own keys are kept, so the id and version stay readable; only
62
+ * `items` is replaced by the normalised list.
63
+ *
64
+ * @param {object|null} template The template as cached.
65
+ * @param {object} [config] The leaf's offline config.
66
+ *
67
+ * @return {object|null} The template with flat `items[]`, or null.
68
+ */
69
+ function normaliseChecklistTemplate(template, config = {}) {
70
+ if (template === null || typeof template !== 'object') {
71
+ return null
72
+ }
73
+
74
+ const keyField = config.itemKeyField || 'questionId';
75
+ const textField = config.itemTextField || 'text';
76
+ const typeField = config.itemTypeField || 'type';
77
+ const typeMap = (config.itemTypeMap && typeof config.itemTypeMap === 'object') ? config.itemTypeMap : {};
78
+
79
+ const sections = config.sectionsField ? template[config.sectionsField] : null;
80
+ const raw = Array.isArray(sections)
81
+ ? sections.flatMap((section) => (Array.isArray(section?.items) ? section.items : []))
82
+ : (Array.isArray(template.items) ? template.items : []);
83
+
84
+ const items = [];
85
+ for (const item of raw) {
86
+ if (item === null || typeof item !== 'object') {
87
+ continue
88
+ }
89
+ const questionId = String(item[keyField] ?? '');
90
+ if (questionId === '') {
91
+ continue
92
+ }
93
+ const ownType = item[typeField];
94
+ const photoRequired = Boolean(config.photoRequiredField)
95
+ && item[config.photoRequiredField] === config.photoRequiredValue;
96
+ items.push({
97
+ ...item,
98
+ questionId,
99
+ text: item[textField] ?? item.text ?? questionId,
100
+ type: typeMap[ownType] ?? ownType ?? 'text',
101
+ required: item.required === true,
102
+ photoRequired,
103
+ });
104
+ }
105
+
106
+ return { ...template, items }
107
+ }
108
+
50
109
  /**
51
110
  * Validate a set of checklist answers against the template's required items.
52
111
  *
53
- * A `required` item must have a non-empty answer; a `photo_required` item must
54
- * additionally have at least one evidence reference. Returns the per-question
112
+ * A `required` item must have a non-empty answer; a `photo_required` item, and
113
+ * any item flagged `photoRequired` by {@link normaliseChecklistTemplate}, must
114
+ * have at least one evidence reference. Returns the per-question
55
115
  * blocking errors so the UI can prevent save (and the engine never queues an
56
116
  * invalid result).
57
117
  *
@@ -67,7 +127,7 @@ function validateChecklistAnswers(template, answersByQuestion) {
67
127
  const errors = [];
68
128
 
69
129
  for (const item of items) {
70
- if (item?.required !== true) {
130
+ if (item?.required !== true && item?.photoRequired !== true) {
71
131
  continue
72
132
  }
73
133
  const entry = answers[item.questionId] ?? {};
@@ -81,7 +141,12 @@ function validateChecklistAnswers(template, answersByQuestion) {
81
141
  continue
82
142
  }
83
143
 
84
- if (answer === undefined || answer === null || String(answer).trim() === '') {
144
+ if (item.photoRequired === true && evidenceRefs.length === 0) {
145
+ errors.push({ questionId: item.questionId, message: translate('nextcloud-vue', 'Photo required for this question') });
146
+ continue
147
+ }
148
+
149
+ if (item.required === true && (answer === undefined || answer === null || String(answer).trim() === '')) {
85
150
  errors.push({ questionId: item.questionId, message: translate('nextcloud-vue', 'This question is required') });
86
151
  }
87
152
  }
@@ -160,5 +225,5 @@ function syncIndicator(pendingCount, online, stuckCount = 0) {
160
225
  return { tone: 'success', text: translate('nextcloud-vue', 'All changes synced') }
161
226
  }
162
227
 
163
- export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, syncIndicator, validateChecklistAnswers };
228
+ export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, normaliseChecklistTemplate, syncIndicator, validateChecklistAnswers };
164
229
  //# sourceMappingURL=fieldCollectionHelpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fieldCollectionHelpers.js","sources":["../../../../src/integrations/offline/fieldCollectionHelpers.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Pure helpers for the generic offline data-collection workflow.\n *\n * Checklist required-field validation, N/M progress counting, GPS-accuracy\n * classification and the user-facing sync-status copy. These are DOM-free and\n * carry no app-specific field names: the checklist shape (`items[]` with\n * `questionId` / `type` / `required`) is the generic contract a consuming app's\n * checklist schema/config supplies. Extracted from procest's\n * `fieldInspectionHelpers` so any app can reuse them.\n *\n * @module integrations/offline/fieldCollectionHelpers\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** Accuracy worse than this (metres) triggers the \"poor signal\" warning. */\nexport const GPS_POOR_ACCURACY_M = 50\n\n/**\n * Classify a GPS fix into good / poor / sensorless and supply warning copy.\n *\n * @param {object|null} fix The Geolocation reading.\n * @param {number} [fix.accuracy] Accuracy in metres.\n * @param {boolean} [available] Whether the sensor produced any fix.\n *\n * @return {{ quality: ('good'|'poor'|'sensorless'), source: ('sensor'|'sensorless'), warning: (string|null) }}\n */\nexport function classifyGps(fix, available = true) {\n\tif (available === false || fix === null || fix === undefined) {\n\t\treturn { quality: 'sensorless', source: 'sensorless', warning: null }\n\t}\n\n\tconst accuracy = Number(fix.accuracy ?? Number.POSITIVE_INFINITY)\n\tif (accuracy > GPS_POOR_ACCURACY_M) {\n\t\tconst rounded = Number.isFinite(accuracy) ? Math.round(accuracy) : '?'\n\t\treturn {\n\t\t\tquality: 'poor',\n\t\t\tsource: 'sensor',\n\t\t\twarning: t('nextcloud-vue', 'Location imprecise (±{m}m). Wait for a better signal, or add the address by hand.', { m: rounded }),\n\t\t}\n\t}\n\n\treturn { quality: 'good', source: 'sensor', warning: null }\n}\n\n/**\n * Validate a set of checklist answers against the template's required items.\n *\n * A `required` item must have a non-empty answer; a `photo_required` item must\n * additionally have at least one evidence reference. Returns the per-question\n * blocking errors so the UI can prevent save (and the engine never queues an\n * invalid result).\n *\n * @param {object} template The checklist template (`items[]`).\n * @param {Array} template.items Items (`questionId`, `type`, `required`).\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ valid: boolean, errors: Array<{ questionId: string, message: string }> }}\n */\nexport function validateChecklistAnswers(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tconst errors = []\n\n\tfor (const item of items) {\n\t\tif (item?.required !== true) {\n\t\t\tcontinue\n\t\t}\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst answer = entry.answer\n\t\tconst evidenceRefs = Array.isArray(entry.evidenceRefs) ? entry.evidenceRefs : []\n\n\t\tif (item.type === 'photo_required') {\n\t\t\tif (evidenceRefs.length === 0) {\n\t\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'Photo required for this question') })\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif (answer === undefined || answer === null || String(answer).trim() === '') {\n\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'This question is required') })\n\t\t}\n\t}\n\n\treturn { valid: errors.length === 0, errors }\n}\n\n/**\n * Count completed items in a checklist for the N/M progress indicator.\n *\n * @param {object} template The checklist template.\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ done: number, total: number }} Completed and total item counts.\n */\nexport function checklistProgress(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tlet done = 0\n\n\tfor (const item of items) {\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== ''\n\t\tconst hasEvidence = Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0\n\t\tif (item.type === 'photo_required' ? hasEvidence : hasAnswer) {\n\t\t\tdone += 1\n\t\t}\n\t}\n\n\treturn { done, total: items.length }\n}\n\n/**\n * Human-readable sync-status copy for the green/amber/red indicator.\n *\n * @param {number} pendingCount The number of pending operations.\n * @param {boolean} online Whether the device is online.\n * @param {number} [stuckCount] How many will not send again without somebody acting.\n *\n * @return {{ tone: ('success'|'warning'|'error'), text: string }} Indicator state.\n */\nexport function syncIndicator(pendingCount, online, stuckCount = 0) {\n\t// 🔴 STUCK WORK IS NAMED BEFORE ANYTHING ELSE, INCLUDING BEFORE BEING\n\t// OFFLINE. `pendingCount` counts pending, conflict and syncing; a `failed`\n\t// operation is in none of them, so a device holding a stranded inspection\n\t// used to reach the last line here and report \"All changes synced\" in\n\t// green. The one surface that could have told an inspector their morning\n\t// had not left the device said the opposite.\n\t//\n\t// Being offline is temporary and expected in the field. Being stuck is\n\t// neither, so it outranks it.\n\tif (stuckCount > 0) {\n\t\treturn {\n\t\t\ttone: 'error',\n\t\t\ttext: t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{n} changes are stuck and will not send on their own',\n\t\t\t\t{ n: stuckCount },\n\t\t\t),\n\t\t}\n\t}\n\n\t// Being offline is not a fault. A field device spends its day out of\n\t// signal by design, and red said something had gone wrong every hour of\n\t// every working day, which is how a palette stops being read at all. Red\n\t// is now reserved for the one state that needs somebody: stuck work.\n\tif (online === false) {\n\t\treturn {\n\t\t\ttone: 'warning',\n\t\t\ttext: t('nextcloud-vue', 'No connection. {n} changes are waiting and will send when you are back.', { n: pendingCount }),\n\t\t}\n\t}\n\tif (pendingCount > 0) {\n\t\treturn { tone: 'warning', text: t('nextcloud-vue', '{n} changes waiting for sync', { n: pendingCount }) }\n\t}\n\treturn { tone: 'success', text: t('nextcloud-vue', 'All changes synced') }\n}\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"fieldCollectionHelpers.js","sources":["../../../../src/integrations/offline/fieldCollectionHelpers.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Pure helpers for the generic offline data-collection workflow.\n *\n * Checklist required-field validation, N/M progress counting, GPS-accuracy\n * classification and the user-facing sync-status copy. These are DOM-free and\n * carry no app-specific field names: the checklist shape (`items[]` with\n * `questionId` / `type` / `required`) is the generic contract a consuming app's\n * checklist schema/config supplies. Extracted from procest's\n * `fieldInspectionHelpers` so any app can reuse them.\n *\n * @module integrations/offline/fieldCollectionHelpers\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** Accuracy worse than this (metres) triggers the \"poor signal\" warning. */\nexport const GPS_POOR_ACCURACY_M = 50\n\n/**\n * Classify a GPS fix into good / poor / sensorless and supply warning copy.\n *\n * @param {object|null} fix The Geolocation reading.\n * @param {number} [fix.accuracy] Accuracy in metres.\n * @param {boolean} [available] Whether the sensor produced any fix.\n *\n * @return {{ quality: ('good'|'poor'|'sensorless'), source: ('sensor'|'sensorless'), warning: (string|null) }}\n */\nexport function classifyGps(fix, available = true) {\n\tif (available === false || fix === null || fix === undefined) {\n\t\treturn { quality: 'sensorless', source: 'sensorless', warning: null }\n\t}\n\n\tconst accuracy = Number(fix.accuracy ?? Number.POSITIVE_INFINITY)\n\tif (accuracy > GPS_POOR_ACCURACY_M) {\n\t\tconst rounded = Number.isFinite(accuracy) ? Math.round(accuracy) : '?'\n\t\treturn {\n\t\t\tquality: 'poor',\n\t\t\tsource: 'sensor',\n\t\t\twarning: t('nextcloud-vue', 'Location imprecise (±{m}m). Wait for a better signal, or add the address by hand.', { m: rounded }),\n\t\t}\n\t}\n\n\treturn { quality: 'good', source: 'sensor', warning: null }\n}\n\n/**\n * Read a checklist template into the leaf's flat item contract.\n *\n * A consuming app's template does not have to carry `items[]` with\n * `questionId` / `text` / `type`. Its `offlineConfig` names where those live:\n * `sectionsField` when the items sit inside ordered sections, and the\n * `item*Field` keys for the item's key, text and type. `itemTypeMap` translates\n * the app's own type words onto the leaf's (`yes_no`, `photo_required`, or any\n * other word, which renders as free text). `photoRequiredField` and\n * `photoRequiredValue` mark an item whose answer also needs a photo.\n *\n * The template's own keys are kept, so the id and version stay readable; only\n * `items` is replaced by the normalised list.\n *\n * @param {object|null} template The template as cached.\n * @param {object} [config] The leaf's offline config.\n *\n * @return {object|null} The template with flat `items[]`, or null.\n */\nexport function normaliseChecklistTemplate(template, config = {}) {\n\tif (template === null || typeof template !== 'object') {\n\t\treturn null\n\t}\n\n\tconst keyField = config.itemKeyField || 'questionId'\n\tconst textField = config.itemTextField || 'text'\n\tconst typeField = config.itemTypeField || 'type'\n\tconst typeMap = (config.itemTypeMap && typeof config.itemTypeMap === 'object') ? config.itemTypeMap : {}\n\n\tconst sections = config.sectionsField ? template[config.sectionsField] : null\n\tconst raw = Array.isArray(sections)\n\t\t? sections.flatMap((section) => (Array.isArray(section?.items) ? section.items : []))\n\t\t: (Array.isArray(template.items) ? template.items : [])\n\n\tconst items = []\n\tfor (const item of raw) {\n\t\tif (item === null || typeof item !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tconst questionId = String(item[keyField] ?? '')\n\t\tif (questionId === '') {\n\t\t\tcontinue\n\t\t}\n\t\tconst ownType = item[typeField]\n\t\tconst photoRequired = Boolean(config.photoRequiredField)\n\t\t\t&& item[config.photoRequiredField] === config.photoRequiredValue\n\t\titems.push({\n\t\t\t...item,\n\t\t\tquestionId,\n\t\t\ttext: item[textField] ?? item.text ?? questionId,\n\t\t\ttype: typeMap[ownType] ?? ownType ?? 'text',\n\t\t\trequired: item.required === true,\n\t\t\tphotoRequired,\n\t\t})\n\t}\n\n\treturn { ...template, items }\n}\n\n/**\n * Validate a set of checklist answers against the template's required items.\n *\n * A `required` item must have a non-empty answer; a `photo_required` item, and\n * any item flagged `photoRequired` by {@link normaliseChecklistTemplate}, must\n * have at least one evidence reference. Returns the per-question\n * blocking errors so the UI can prevent save (and the engine never queues an\n * invalid result).\n *\n * @param {object} template The checklist template (`items[]`).\n * @param {Array} template.items Items (`questionId`, `type`, `required`).\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ valid: boolean, errors: Array<{ questionId: string, message: string }> }}\n */\nexport function validateChecklistAnswers(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tconst errors = []\n\n\tfor (const item of items) {\n\t\tif (item?.required !== true && item?.photoRequired !== true) {\n\t\t\tcontinue\n\t\t}\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst answer = entry.answer\n\t\tconst evidenceRefs = Array.isArray(entry.evidenceRefs) ? entry.evidenceRefs : []\n\n\t\tif (item.type === 'photo_required') {\n\t\t\tif (evidenceRefs.length === 0) {\n\t\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'Photo required for this question') })\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif (item.photoRequired === true && evidenceRefs.length === 0) {\n\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'Photo required for this question') })\n\t\t\tcontinue\n\t\t}\n\n\t\tif (item.required === true && (answer === undefined || answer === null || String(answer).trim() === '')) {\n\t\t\terrors.push({ questionId: item.questionId, message: t('nextcloud-vue', 'This question is required') })\n\t\t}\n\t}\n\n\treturn { valid: errors.length === 0, errors }\n}\n\n/**\n * Count completed items in a checklist for the N/M progress indicator.\n *\n * @param {object} template The checklist template.\n * @param {object} answersByQuestion Map of questionId → { answer, evidenceRefs }.\n *\n * @return {{ done: number, total: number }} Completed and total item counts.\n */\nexport function checklistProgress(template, answersByQuestion) {\n\tconst items = Array.isArray(template?.items) ? template.items : []\n\tconst answers = answersByQuestion ?? {}\n\tlet done = 0\n\n\tfor (const item of items) {\n\t\tconst entry = answers[item.questionId] ?? {}\n\t\tconst hasAnswer = entry.answer !== undefined && entry.answer !== null && String(entry.answer).trim() !== ''\n\t\tconst hasEvidence = Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0\n\t\tif (item.type === 'photo_required' ? hasEvidence : hasAnswer) {\n\t\t\tdone += 1\n\t\t}\n\t}\n\n\treturn { done, total: items.length }\n}\n\n/**\n * Human-readable sync-status copy for the green/amber/red indicator.\n *\n * @param {number} pendingCount The number of pending operations.\n * @param {boolean} online Whether the device is online.\n * @param {number} [stuckCount] How many will not send again without somebody acting.\n *\n * @return {{ tone: ('success'|'warning'|'error'), text: string }} Indicator state.\n */\nexport function syncIndicator(pendingCount, online, stuckCount = 0) {\n\t// 🔴 STUCK WORK IS NAMED BEFORE ANYTHING ELSE, INCLUDING BEFORE BEING\n\t// OFFLINE. `pendingCount` counts pending, conflict and syncing; a `failed`\n\t// operation is in none of them, so a device holding a stranded inspection\n\t// used to reach the last line here and report \"All changes synced\" in\n\t// green. The one surface that could have told an inspector their morning\n\t// had not left the device said the opposite.\n\t//\n\t// Being offline is temporary and expected in the field. Being stuck is\n\t// neither, so it outranks it.\n\tif (stuckCount > 0) {\n\t\treturn {\n\t\t\ttone: 'error',\n\t\t\ttext: t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{n} changes are stuck and will not send on their own',\n\t\t\t\t{ n: stuckCount },\n\t\t\t),\n\t\t}\n\t}\n\n\t// Being offline is not a fault. A field device spends its day out of\n\t// signal by design, and red said something had gone wrong every hour of\n\t// every working day, which is how a palette stops being read at all. Red\n\t// is now reserved for the one state that needs somebody: stuck work.\n\tif (online === false) {\n\t\treturn {\n\t\t\ttone: 'warning',\n\t\t\ttext: t('nextcloud-vue', 'No connection. {n} changes are waiting and will send when you are back.', { n: pendingCount }),\n\t\t}\n\t}\n\tif (pendingCount > 0) {\n\t\treturn { tone: 'warning', text: t('nextcloud-vue', '{n} changes waiting for sync', { n: pendingCount }) }\n\t}\n\treturn { tone: 'success', text: t('nextcloud-vue', 'All changes synced') }\n}\n"],"names":["t"],"mappings":"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"}
@@ -2,7 +2,8 @@ export { CONFLICT_RECORD_PREFIX, __resetDbForTests, __setDexie, applyConflictRes
2
2
  export { BACKOFF_SCHEDULE_MS, MAX_ATTEMPTS, TERMINAL_STATUSES, canRetry, classifyConflict, delayForAttempt, diffVersions, isConflictRetryable, nextState, orderForReplay, resolveConflictChoice } from './syncQueueEngine.js';
3
3
  export { drainQueue, replayOperation } from './syncReplayService.js';
4
4
  export { buildPlanningQuery, fetchPlanning, fetchReferences, toDayString } from './planningFetch.js';
5
- export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, syncIndicator, validateChecklistAnswers } from './fieldCollectionHelpers.js';
5
+ export { GPS_POOR_ACCURACY_M, checklistProgress, classifyGps, normaliseChecklistTemplate, syncIndicator, validateChecklistAnswers } from './fieldCollectionHelpers.js';
6
+ export { buildChecklistSubmission, fillEndpoint } from './checklistSubmission.js';
6
7
  export { CACHE_PREFIX, NEVER_CACHED, cacheNameFor, isCacheable, respondTo, staleCacheNames } from '../../offline/serviceWorker.js';
7
8
  export { registerOfflineWorker, unregisterOfflineWorker } from '../../offline/registerOfflineWorker.js';
8
9
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/integrations/offline/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Generic offline data-collection core — public surface.\n *\n * Reusable offline-sync infrastructure extracted from procest's\n * `mobiel-inspectie-offline` PWA so any Conduction app can collect field data\n * offline by registering a checklist/planning schema instead of re-implementing\n * the IndexedDB cache, mutation queue, replay loop and sync-state indicator.\n *\n * @module integrations/offline\n */\n\nexport {\n\t__resetDbForTests,\n\t__setDexie,\n\tapplyConflictResolution,\n\tcacheKey,\n\tCONFLICT_RECORD_PREFIX,\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetDb,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tlistQueue,\n\topenDb,\n\trecordConflict,\n\trequeueOperation,\n\tresolveDeviceId,\n\tstorePlanning,\n} from './offlineDb.js'\n\nexport {\n\tBACKOFF_SCHEDULE_MS,\n\tcanRetry,\n\tclassifyConflict,\n\tdelayForAttempt,\n\tdiffVersions,\n\tisConflictRetryable,\n\tMAX_ATTEMPTS,\n\tnextState,\n\torderForReplay,\n\tresolveConflictChoice,\n\tTERMINAL_STATUSES,\n} from './syncQueueEngine.js'\n\nexport { drainQueue, replayOperation } from './syncReplayService.js'\n\nexport {\n\tbuildPlanningQuery,\n\tfetchPlanning,\n\tfetchReferences,\n\ttoDayString,\n} from './planningFetch.js'\n\nexport {\n\tchecklistProgress,\n\tclassifyGps,\n\tGPS_POOR_ACCURACY_M,\n\tsyncIndicator,\n\tvalidateChecklistAnswers,\n} from './fieldCollectionHelpers.js'\n\nexport {\n\tCACHE_PREFIX,\n\tcacheNameFor,\n\tisCacheable,\n\tNEVER_CACHED,\n\trespondTo,\n\tstaleCacheNames,\n} from '../../offline/serviceWorker.js'\n\nexport {\n\tregisterOfflineWorker,\n\tunregisterOfflineWorker,\n} from '../../offline/registerOfflineWorker.js'\n"],"names":[],"mappings":";;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/integrations/offline/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Generic offline data-collection core — public surface.\n *\n * Reusable offline-sync infrastructure extracted from procest's\n * `mobiel-inspectie-offline` PWA so any Conduction app can collect field data\n * offline by registering a checklist/planning schema instead of re-implementing\n * the IndexedDB cache, mutation queue, replay loop and sync-state indicator.\n *\n * @module integrations/offline\n */\n\nexport {\n\t__resetDbForTests,\n\t__setDexie,\n\tapplyConflictResolution,\n\tcacheKey,\n\tCONFLICT_RECORD_PREFIX,\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetDb,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tlistQueue,\n\topenDb,\n\trecordConflict,\n\trequeueOperation,\n\tresolveDeviceId,\n\tstorePlanning,\n} from './offlineDb.js'\n\nexport {\n\tBACKOFF_SCHEDULE_MS,\n\tcanRetry,\n\tclassifyConflict,\n\tdelayForAttempt,\n\tdiffVersions,\n\tisConflictRetryable,\n\tMAX_ATTEMPTS,\n\tnextState,\n\torderForReplay,\n\tresolveConflictChoice,\n\tTERMINAL_STATUSES,\n} from './syncQueueEngine.js'\n\nexport { drainQueue, replayOperation } from './syncReplayService.js'\n\nexport {\n\tbuildPlanningQuery,\n\tfetchPlanning,\n\tfetchReferences,\n\ttoDayString,\n} from './planningFetch.js'\n\nexport {\n\tchecklistProgress,\n\tclassifyGps,\n\tGPS_POOR_ACCURACY_M,\n\tnormaliseChecklistTemplate,\n\tsyncIndicator,\n\tvalidateChecklistAnswers,\n} from './fieldCollectionHelpers.js'\n\nexport { buildChecklistSubmission, fillEndpoint } from './checklistSubmission.js'\n\nexport {\n\tCACHE_PREFIX,\n\tcacheNameFor,\n\tisCacheable,\n\tNEVER_CACHED,\n\trespondTo,\n\tstaleCacheNames,\n} from '../../offline/serviceWorker.js'\n\nexport {\n\tregisterOfflineWorker,\n\tunregisterOfflineWorker,\n} from '../../offline/registerOfflineWorker.js'\n"],"names":[],"mappings":";;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
@@ -258,10 +258,12 @@ async function getPlanningMeta(register, schema, collection = 'planning') {
258
258
  *
259
259
  * @param {object} operation The mutation operation.
260
260
  * @param {string} operation.deviceId Owning device id (IDOR scope).
261
- * @param {string} operation.operationType create / update / delete.
261
+ * @param {string} operation.operationType create / update / delete / submit.
262
262
  * @param {string} operation.register Target register slug or id.
263
263
  * @param {string} operation.schema Target schema slug or id.
264
264
  * @param {string} [operation.targetId] Existing object id (update/delete).
265
+ * @param {string} [operation.endpoint] Path under the Nextcloud root a
266
+ * `submit` row is POSTed to.
265
267
  * @param {object} [operation.payload] The object body (create/update).
266
268
  *
267
269
  * @return {Promise<string>} The queued operation id.
@@ -276,6 +278,9 @@ async function enqueueMutation(operation) {
276
278
  register: operation.register,
277
279
  schema: operation.schema,
278
280
  targetId: operation.targetId ?? null,
281
+ // A `submit` operation replays to an app's own endpoint instead of the
282
+ // object API (see checklistSubmission.js); every other row has none.
283
+ endpoint: operation.endpoint ?? null,
279
284
  payload: operation.payload ?? {},
280
285
  queuedAt: operation.queuedAt || new Date().toISOString(),
281
286
  attemptCount: 0,