@conduction/nextcloud-vue 2.58.0 → 2.60.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 (507) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +12 -2
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +1 -2
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +14 -1
  6. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  7. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +0 -3
  8. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  10. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -2
  11. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  12. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +39 -0
  13. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  14. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
  15. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +224 -9
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
  19. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
  21. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
  22. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
  23. package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
  24. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +58 -5
  25. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  26. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +13 -5
  27. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  28. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
  29. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
  30. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
  31. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
  32. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
  33. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
  34. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +9 -1
  35. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  36. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  37. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
  38. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  39. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
  40. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  41. package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js +6 -1
  42. package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
  43. package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
  44. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
  45. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
  46. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
  47. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
  48. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
  49. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
  50. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
  51. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
  52. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
  53. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
  54. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
  55. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
  56. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +116 -15
  57. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  58. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +27 -14
  59. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  60. package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js +13 -6
  61. package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue2.js.map +1 -1
  62. package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js +2 -0
  63. package/dist/esm/components/CnDateAxisView/CnDateAxisView.vue3.js.map +1 -1
  64. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +532 -29
  65. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  66. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +300 -142
  67. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
  69. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
  70. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
  71. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
  72. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
  73. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
  74. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
  75. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
  76. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
  77. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
  78. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
  79. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
  80. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  81. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +1 -2
  82. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  83. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +3 -1
  84. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  85. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +17 -17
  86. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  87. package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js +4 -21
  88. package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnFilesWidget/CnFilesWidget.vue3.js.map +1 -1
  90. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +0 -13
  91. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  92. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +8 -8
  93. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  94. package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js +3 -2
  96. package/dist/esm/components/CnFlowsPage/CnFlowsPage.vue3.js.map +1 -1
  97. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +169 -12
  98. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  99. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
  100. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  101. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
  102. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
  103. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
  104. package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
  105. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +199 -16
  106. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  107. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +20 -10
  108. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  109. package/dist/esm/components/CnIndexPage/manifestActionDispatch.js +31 -11
  110. package/dist/esm/components/CnIndexPage/manifestActionDispatch.js.map +1 -1
  111. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue2.js.map +1 -1
  112. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js +1 -2
  113. package/dist/esm/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue3.js.map +1 -1
  114. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  115. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js +0 -1
  116. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  117. package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js +2 -17
  118. package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue2.js.map +1 -1
  119. package/dist/esm/components/CnLinkButtonWidget/CnLinkButtonWidget.vue3.js.map +1 -1
  120. package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js +35 -11
  121. package/dist/esm/components/CnLogsPage/CnLogsPage.vue2.js.map +1 -1
  122. package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js +3 -2
  123. package/dist/esm/components/CnLogsPage/CnLogsPage.vue3.js.map +1 -1
  124. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
  125. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
  126. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
  127. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
  128. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
  129. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
  130. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
  131. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
  132. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
  133. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
  134. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
  135. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
  136. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +23 -5
  137. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  138. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +1 -1
  139. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  140. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
  141. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  142. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  143. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
  144. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
  145. package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
  146. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
  147. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
  148. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
  149. package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
  150. package/dist/esm/components/CnObjectList/CnObjectList.vue2.js +19 -3
  151. package/dist/esm/components/CnObjectList/CnObjectList.vue2.js.map +1 -1
  152. package/dist/esm/components/CnObjectList/CnObjectList.vue3.js.map +1 -1
  153. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +45 -15
  154. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  155. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
  156. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  157. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +24 -6
  158. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
  159. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +1 -1
  160. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
  161. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  162. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +0 -1
  163. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  164. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  165. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +4 -2
  166. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  167. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
  168. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  169. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
  170. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  171. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +100 -3
  172. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  173. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  174. package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +7 -2
  175. package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
  176. package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
  177. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
  178. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  179. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  180. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
  181. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
  182. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
  183. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
  184. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
  185. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
  186. package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js +5 -3
  187. package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue2.js.map +1 -1
  188. package/dist/esm/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue3.js.map +1 -1
  189. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
  190. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
  191. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
  192. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
  193. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
  194. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
  195. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
  196. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
  197. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
  198. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
  199. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
  200. package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
  201. package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
  202. package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
  203. package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
  204. package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
  205. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  206. package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
  207. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  208. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
  209. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  210. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
  211. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  212. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +3 -1
  213. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
  214. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +1 -0
  215. package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
  216. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
  217. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
  218. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
  219. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
  220. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
  221. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
  222. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
  223. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
  224. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
  225. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
  226. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
  227. package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
  228. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
  229. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  230. package/dist/esm/index.js +10 -0
  231. package/dist/esm/index.js.map +1 -1
  232. package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js +3 -1
  233. package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue2.js.map +1 -1
  234. package/dist/esm/integrations/builtin/analytics/CnAnalyticsCard.vue3.js.map +1 -1
  235. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js +3 -1
  236. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js.map +1 -1
  237. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js +2 -4
  238. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js.map +1 -1
  239. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js +6 -0
  240. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue2.js.map +1 -1
  241. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js +68 -53
  242. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksCard.vue3.js.map +1 -1
  243. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js +17 -0
  244. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue2.js.map +1 -1
  245. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js +21 -19
  246. package/dist/esm/integrations/builtin/bookmarks/CnBookmarksTab.vue3.js.map +1 -1
  247. package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js +3 -1
  248. package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue2.js.map +1 -1
  249. package/dist/esm/integrations/builtin/collectives/CnCollectivesCard.vue3.js.map +1 -1
  250. package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js +3 -1
  251. package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue2.js.map +1 -1
  252. package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js +2 -4
  253. package/dist/esm/integrations/builtin/collectives/CnCollectivesTab.vue3.js.map +1 -1
  254. package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js +2 -1
  255. package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue2.js.map +1 -1
  256. package/dist/esm/integrations/builtin/cospend/CnCospendCard.vue3.js.map +1 -1
  257. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js +2 -1
  258. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js.map +1 -1
  259. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js +1 -2
  260. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js.map +1 -1
  261. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +2 -1
  262. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  263. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  264. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +2 -1
  265. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  266. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js +1 -2
  267. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  268. package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js.map +1 -1
  269. package/dist/esm/integrations/builtin/email/CnEmailCard.vue3.js.map +1 -1
  270. package/dist/esm/integrations/builtin/email/CnEmailTab.vue2.js.map +1 -1
  271. package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js +1 -2
  272. package/dist/esm/integrations/builtin/email/CnEmailTab.vue3.js.map +1 -1
  273. package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js +2 -1
  274. package/dist/esm/integrations/builtin/flow/CnFlowCard.vue2.js.map +1 -1
  275. package/dist/esm/integrations/builtin/flow/CnFlowCard.vue3.js.map +1 -1
  276. package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js +2 -1
  277. package/dist/esm/integrations/builtin/flow/CnFlowTab.vue2.js.map +1 -1
  278. package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js +1 -2
  279. package/dist/esm/integrations/builtin/flow/CnFlowTab.vue3.js.map +1 -1
  280. package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js +3 -1
  281. package/dist/esm/integrations/builtin/forms/CnFormsCard.vue2.js.map +1 -1
  282. package/dist/esm/integrations/builtin/forms/CnFormsCard.vue3.js.map +1 -1
  283. package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js +3 -1
  284. package/dist/esm/integrations/builtin/forms/CnFormsTab.vue2.js.map +1 -1
  285. package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js +2 -4
  286. package/dist/esm/integrations/builtin/forms/CnFormsTab.vue3.js.map +1 -1
  287. package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js +3 -1
  288. package/dist/esm/integrations/builtin/maps/CnMapsCard.vue2.js.map +1 -1
  289. package/dist/esm/integrations/builtin/maps/CnMapsCard.vue3.js.map +1 -1
  290. package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js +3 -1
  291. package/dist/esm/integrations/builtin/maps/CnMapsTab.vue2.js.map +1 -1
  292. package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js +2 -4
  293. package/dist/esm/integrations/builtin/maps/CnMapsTab.vue3.js.map +1 -1
  294. package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js +3 -1
  295. package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue2.js.map +1 -1
  296. package/dist/esm/integrations/builtin/openproject/CnOpenprojectCard.vue3.js.map +1 -1
  297. package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js +3 -1
  298. package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue2.js.map +1 -1
  299. package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js +2 -4
  300. package/dist/esm/integrations/builtin/openproject/CnOpenprojectTab.vue3.js.map +1 -1
  301. package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js +2 -1
  302. package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue2.js.map +1 -1
  303. package/dist/esm/integrations/builtin/photos/CnPhotosCard.vue3.js.map +1 -1
  304. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js +2 -1
  305. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js.map +1 -1
  306. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js +1 -2
  307. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js.map +1 -1
  308. package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js +2 -1
  309. package/dist/esm/integrations/builtin/polls/CnPollsCard.vue2.js.map +1 -1
  310. package/dist/esm/integrations/builtin/polls/CnPollsCard.vue3.js.map +1 -1
  311. package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js +2 -1
  312. package/dist/esm/integrations/builtin/polls/CnPollsTab.vue2.js.map +1 -1
  313. package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js +1 -2
  314. package/dist/esm/integrations/builtin/polls/CnPollsTab.vue3.js.map +1 -1
  315. package/dist/esm/integrations/builtin/shares/CnSharesTab.vue2.js.map +1 -1
  316. package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js +3 -6
  317. package/dist/esm/integrations/builtin/shares/CnSharesTab.vue3.js.map +1 -1
  318. package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js +3 -1
  319. package/dist/esm/integrations/builtin/talk/CnTalkCard.vue2.js.map +1 -1
  320. package/dist/esm/integrations/builtin/talk/CnTalkCard.vue3.js.map +1 -1
  321. package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js +3 -1
  322. package/dist/esm/integrations/builtin/talk/CnTalkTab.vue2.js.map +1 -1
  323. package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js +2 -4
  324. package/dist/esm/integrations/builtin/talk/CnTalkTab.vue3.js.map +1 -1
  325. package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js +2 -1
  326. package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue2.js.map +1 -1
  327. package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerCard.vue3.js.map +1 -1
  328. package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js +2 -1
  329. package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue2.js.map +1 -1
  330. package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js +1 -2
  331. package/dist/esm/integrations/builtin/time-tracker/CnTimeTrackerTab.vue3.js.map +1 -1
  332. package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js +4 -1
  333. package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue2.js.map +1 -1
  334. package/dist/esm/integrations/builtin/xwiki/CnXwikiCard.vue3.js.map +1 -1
  335. package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js +6 -3
  336. package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue2.js.map +1 -1
  337. package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js +2 -4
  338. package/dist/esm/integrations/builtin/xwiki/CnXwikiTab.vue3.js.map +1 -1
  339. package/dist/esm/l10n/en.json.js +55 -1
  340. package/dist/esm/l10n/en.json.js.map +1 -1
  341. package/dist/esm/l10n/nl.json.js +55 -1
  342. package/dist/esm/l10n/nl.json.js.map +1 -1
  343. package/dist/esm/nextcloud-vue.css +998 -7
  344. package/dist/esm/utils/actionLink.js +5 -0
  345. package/dist/esm/utils/actionLink.js.map +1 -1
  346. package/dist/esm/utils/actionsDispatcher.js +16 -1
  347. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  348. package/dist/esm/utils/dateVariant.js +124 -0
  349. package/dist/esm/utils/dateVariant.js.map +1 -0
  350. package/dist/esm/utils/detailActionModel.js +260 -0
  351. package/dist/esm/utils/detailActionModel.js.map +1 -0
  352. package/dist/esm/utils/dueRule.js +84 -0
  353. package/dist/esm/utils/dueRule.js.map +1 -0
  354. package/dist/esm/utils/fetchAggregate.js +37 -1
  355. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  356. package/dist/esm/utils/fetchFilterCounts.js +186 -0
  357. package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
  358. package/dist/esm/utils/libraryWidgetKeys.js +5 -0
  359. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  360. package/dist/esm/utils/linkNavigation.js +6 -3
  361. package/dist/esm/utils/linkNavigation.js.map +1 -1
  362. package/dist/esm/utils/readPath.js +30 -0
  363. package/dist/esm/utils/readPath.js.map +1 -0
  364. package/dist/esm/utils/rowAuxClick.js +30 -5
  365. package/dist/esm/utils/rowAuxClick.js.map +1 -1
  366. package/dist/esm/utils/safeHref.js +18 -3
  367. package/dist/esm/utils/safeHref.js.map +1 -1
  368. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  369. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  370. package/dist/nextcloud-vue.cjs.js +15855 -9353
  371. package/dist/nextcloud-vue.cjs.js.map +2 -2
  372. package/dist/nextcloud-vue.css +998 -7
  373. package/l10n/en.json +55 -1
  374. package/l10n/nl.json +55 -1
  375. package/package.json +1 -1
  376. package/scripts/.jsdoc-baselines.json +3 -0
  377. package/src/components/CnActionsBar/CnActionsBar.vue +14 -5
  378. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -4
  379. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +1 -2
  380. package/src/components/CnAppNav/CnAppNav.md +26 -0
  381. package/src/components/CnAppNav/CnAppNav.vue +104 -1
  382. package/src/components/CnBannerWidget/CnBannerWidget.vue +365 -10
  383. package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
  384. package/src/components/CnBoardView/CnBoardView.vue +90 -5
  385. package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
  386. package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
  387. package/src/components/CnBrandStripe/index.js +4 -0
  388. package/src/components/CnCardGrid/CnCardGrid.vue +9 -1
  389. package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
  390. package/src/components/CnContextMenu/CnContextMenu.vue +6 -1
  391. package/src/components/CnConversationThread/CnConversationThread.md +51 -0
  392. package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
  393. package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
  394. package/src/components/CnConversationThread/index.js +4 -0
  395. package/src/components/CnDataTable/CnDataTable.md +1 -1
  396. package/src/components/CnDataTable/CnDataTable.vue +131 -17
  397. package/src/components/CnDateAxisView/CnDateAxisView.vue +15 -6
  398. package/src/components/CnDetailPage/CnDetailPage.md +17 -1
  399. package/src/components/CnDetailPage/CnDetailPage.vue +712 -49
  400. package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
  401. package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
  402. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
  403. package/src/components/CnDocumentReviewList/index.js +4 -0
  404. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +1 -2
  405. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +5 -3
  406. package/src/components/CnFilesWidget/CnFilesWidget.vue +4 -21
  407. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +8 -21
  408. package/src/components/CnFlowsPage/CnFlowsPage.vue +2 -1
  409. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +189 -14
  410. package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
  411. package/src/components/CnIndexPage/CnIndexPage.vue +210 -17
  412. package/src/components/CnIndexPage/manifestActionDispatch.js +31 -11
  413. package/src/components/CnIntegrationWidget/CnIntegrationWidgetEmpty.vue +1 -2
  414. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +0 -1
  415. package/src/components/CnLinkButtonWidget/CnLinkButtonWidget.vue +2 -17
  416. package/src/components/CnLogsPage/CnLogsPage.vue +37 -12
  417. package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
  418. package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
  419. package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
  420. package/src/components/CnNextStepCard/index.js +4 -0
  421. package/src/components/CnObjectCard/CnObjectCard.vue +24 -6
  422. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
  423. package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
  424. package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
  425. package/src/components/CnObjectList/CnObjectList.vue +19 -3
  426. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +47 -15
  427. package/src/components/CnObjectRow/CnObjectRow.vue +25 -7
  428. package/src/components/CnObjectSidebar/CnFilesTab.vue +0 -1
  429. package/src/components/CnPageRenderer/CnPageRenderer.vue +2 -0
  430. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
  431. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
  432. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +100 -3
  433. package/src/components/CnRowActions/CnRowActions.vue +7 -2
  434. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
  435. package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
  436. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
  437. package/src/components/CnSegmentedControl/index.js +4 -0
  438. package/src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue +5 -3
  439. package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
  440. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
  441. package/src/components/CnStackedBarWidget/index.js +14 -0
  442. package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
  443. package/src/components/CnStackedBarWidgetForm/index.js +4 -0
  444. package/src/components/CnTabs/CnTab.vue +35 -0
  445. package/src/components/CnTabs/CnTabs.vue +251 -13
  446. package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
  447. package/src/components/CnTasksWidget/CnTasksWidget.vue +4 -1
  448. package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
  449. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
  450. package/src/components/CnWeekStripWidget/index.js +14 -0
  451. package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
  452. package/src/components/CnWeekStripWidgetForm/index.js +4 -0
  453. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
  454. package/src/components/index.js +11 -0
  455. package/src/css/detail-page.css +47 -0
  456. package/src/css/table.css +26 -0
  457. package/src/index.js +9 -0
  458. package/src/integrations/builtin/analytics/CnAnalyticsCard.vue +3 -2
  459. package/src/integrations/builtin/analytics/CnAnalyticsTab.vue +5 -6
  460. package/src/integrations/builtin/bookmarks/CnBookmarksCard.vue +12 -1
  461. package/src/integrations/builtin/bookmarks/CnBookmarksTab.vue +23 -6
  462. package/src/integrations/builtin/bookmarks/__tests__/CnBookmarksTab.spec.js +7 -2
  463. package/src/integrations/builtin/collectives/CnCollectivesCard.vue +3 -2
  464. package/src/integrations/builtin/collectives/CnCollectivesTab.vue +5 -6
  465. package/src/integrations/builtin/cospend/CnCospendCard.vue +2 -2
  466. package/src/integrations/builtin/cospend/CnCospendTab.vue +3 -4
  467. package/src/integrations/builtin/deck/CnDeckCard.vue +2 -2
  468. package/src/integrations/builtin/deck/CnDeckTab.vue +3 -4
  469. package/src/integrations/builtin/email/CnEmailCard.vue +6 -2
  470. package/src/integrations/builtin/email/CnEmailTab.vue +7 -4
  471. package/src/integrations/builtin/flow/CnFlowCard.vue +2 -2
  472. package/src/integrations/builtin/flow/CnFlowTab.vue +3 -4
  473. package/src/integrations/builtin/forms/CnFormsCard.vue +3 -2
  474. package/src/integrations/builtin/forms/CnFormsTab.vue +5 -6
  475. package/src/integrations/builtin/maps/CnMapsCard.vue +3 -2
  476. package/src/integrations/builtin/maps/CnMapsTab.vue +5 -6
  477. package/src/integrations/builtin/maps/__tests__/CnMapsTab.spec.js +9 -0
  478. package/src/integrations/builtin/openproject/CnOpenprojectCard.vue +3 -2
  479. package/src/integrations/builtin/openproject/CnOpenprojectTab.vue +5 -6
  480. package/src/integrations/builtin/photos/CnPhotosCard.vue +2 -2
  481. package/src/integrations/builtin/photos/CnPhotosTab.vue +3 -4
  482. package/src/integrations/builtin/polls/CnPollsCard.vue +2 -2
  483. package/src/integrations/builtin/polls/CnPollsTab.vue +3 -4
  484. package/src/integrations/builtin/shares/CnSharesTab.vue +3 -6
  485. package/src/integrations/builtin/talk/CnTalkCard.vue +3 -2
  486. package/src/integrations/builtin/talk/CnTalkTab.vue +5 -6
  487. package/src/integrations/builtin/time-tracker/CnTimeTrackerCard.vue +2 -2
  488. package/src/integrations/builtin/time-tracker/CnTimeTrackerTab.vue +3 -4
  489. package/src/integrations/builtin/xwiki/CnXwikiCard.vue +4 -2
  490. package/src/integrations/builtin/xwiki/CnXwikiTab.vue +7 -7
  491. package/src/integrations/builtin/xwiki/__tests__/CnXwikiTab.spec.js +9 -0
  492. package/src/schemas/app-manifest-v2.schema.json +389 -1
  493. package/src/schemas/app-manifest.schema.json +2 -1
  494. package/src/utils/actionLink.js +5 -0
  495. package/src/utils/actionsDispatcher.js +15 -0
  496. package/src/utils/dateVariant.js +120 -0
  497. package/src/utils/detailActionModel.js +256 -0
  498. package/src/utils/dueRule.js +94 -0
  499. package/src/utils/fetchAggregate.js +36 -0
  500. package/src/utils/fetchFilterCounts.js +182 -0
  501. package/src/utils/index.js +1 -0
  502. package/src/utils/libraryWidgetKeys.js +5 -0
  503. package/src/utils/linkNavigation.js +6 -3
  504. package/src/utils/readPath.js +27 -0
  505. package/src/utils/rowAuxClick.js +29 -4
  506. package/src/utils/safeHref.js +18 -3
  507. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CnSpendAnalyticsWidget.vue2.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`. The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`. When `null` the injection (then the empty state)\n\t\t * is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\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 * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"CnSpendAnalyticsWidget.vue2.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`\n * (pure and synchronous: it runs for every row on every render). The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`, which must be pure and synchronous, since it is\n\t\t * called for every row on every render. When `null` the injection\n\t\t * (then the empty state) is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\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 * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnSpendAnalyticsWidget.vue3.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue?vue&type=template&id=9d12d4a2&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`. The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`. When `null` the injection (then the empty state)\n\t\t * is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\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 * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList"],"mappings":"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"}
1
+ {"version":3,"file":"CnSpendAnalyticsWidget.vue3.js","sources":["../../../../src/components/CnSpendAnalyticsWidget/CnSpendAnalyticsWidget.vue?vue&type=template&id=9d12d4a2&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-spend-analytics-widget\">\n\t\t<header class=\"cn-spend-analytics-widget__header\">\n\t\t\t<span class=\"cn-spend-analytics-widget__title\">{{ t('nextcloud-vue', 'Spend analytics') }}</span>\n\t\t\t<select\n\t\t\t\tclass=\"cn-spend-analytics-widget__period\"\n\t\t\t\t:value=\"period\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Reporting period')\"\n\t\t\t\t@change=\"onPeriodChange($event.target.value)\">\n\t\t\t\t<option v-for=\"opt in periodOptions\" :key=\"opt.value\" :value=\"opt.value\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</header>\n\n\t\t<div class=\"cn-spend-analytics-widget__body\">\n\t\t\t<div v-if=\"loading\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'Loading spend data…') }}\n\t\t\t</div>\n\n\t\t\t<!-- No data source wired at all. -->\n\t\t\t<div v-else-if=\"!hasSource\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data source available') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq absent → empty-state naming the source. -->\n\t\t\t<div v-else-if=\"financeMissing\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend data — financeq is not installed') }}\n\t\t\t</div>\n\n\t\t\t<!-- financeq present but no data in window. -->\n\t\t\t<div v-else-if=\"financeEmpty\" class=\"cn-spend-analytics-widget__state\">\n\t\t\t\t{{ t('nextcloud-vue', 'No spend recorded for the selected period') }}\n\t\t\t</div>\n\n\t\t\t<template v-else>\n\t\t\t\t<section class=\"cn-spend-analytics-widget__cards\">\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Total spend') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\" :aria-label=\"totalAriaLabel\">\n\t\t\t\t\t\t\t{{ formattedTotal }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"cn-spend-analytics-widget__card\">\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-label\">{{ t('nextcloud-vue', 'Categories') }}</span>\n\t\t\t\t\t\t<span class=\"cn-spend-analytics-widget__card-value\">{{ finance.byCategory.length }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"viewMode === 'trend'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Spend trend') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"row in finance.trend\" :key=\"row.month\" class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t<span>{{ row.month }}</span>\n\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-categories'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top categories') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in finance.byCategory\" :key=\"row.category\">\n\t\t\t\t\t\t\t<a v-if=\"categoryLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"categoryLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.category }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.amount) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-else-if=\"viewMode === 'top-vendors'\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Top vendors') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"vendorMissing\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor data — procest is not installed') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"vendorEmpty\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'No vendor commitments for the selected period') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<ul v-else class=\"cn-spend-analytics-widget__rows\">\n\t\t\t\t\t\t<li v-for=\"(row, i) in vendor.topVendors\" :key=\"row.vendor\">\n\t\t\t\t\t\t\t<a v-if=\"vendorLinks[i]\" class=\"cn-spend-analytics-widget__row\" :href=\"vendorLinks[i]\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div v-else class=\"cn-spend-analytics-widget__row cn-spend-analytics-widget__row--static\">\n\t\t\t\t\t\t\t\t<span>{{ row.vendor }}</span>\n\t\t\t\t\t\t\t\t<span>{{ formatAmount(row.committed) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\n\t\t\t\t<section v-if=\"aiEnabled\" class=\"cn-spend-analytics-widget__panel\">\n\t\t\t\t\t<h3 class=\"cn-spend-analytics-widget__panel-title\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight') }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div v-if=\"aiUnavailable\" class=\"cn-spend-analytics-widget__inline-empty\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'AI insight unavailable — the source is not configured') }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<p v-else-if=\"aiNarrative\" class=\"cn-spend-analytics-widget__ai-text\">\n\t\t\t\t\t\t{{ aiNarrative }}\n\t\t\t\t\t</p>\n\t\t\t\t\t<button\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-spend-analytics-widget__ai-btn\"\n\t\t\t\t\t\t@click=\"requestNarrative\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Generate insight') }}\n\t\t\t\t\t</button>\n\t\t\t\t</section>\n\t\t\t</template>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\n\nconst VIEW_MODES = ['summary', 'trend', 'top-vendors', 'top-categories']\nconst PERIODS = ['month', 'quarter', 'ytd', 'fy']\nconst DEBOUNCE_MS = 200\n\n/**\n * CnSpendAnalyticsWidget — dashboard renderer for a `spend-analytics`\n * placement. Renders summary / trend / top-vendors / top-categories views over\n * spend data with graceful per-source empty states and row drill-through.\n *\n * Data origin (decoupling seam): all data flows through a consumer-supplied\n * `dataSource` prop OR the `cnSpendAnalyticsSource` injection — an object\n * exposing `fetchSummary(args)`, `fetchVendorCommitments(args)`,\n * `fetchNarrative(args)`, and optionally `resolveDeepLink(app, kind, id)`\n * (pure and synchronous: it runs for every row on every render). The\n * renderer imports NO financeq / procest / launchpad service module and never\n * calls axios itself; the consuming app (typically over GraphQL) owns the\n * transport. The `requires.graphql` hint lives only in the registry entry, not\n * in any manifest dependency.\n *\n * Graceful degradation: with no source the widget shows \"No spend data source\n * available\"; the source signals per-sibling availability via\n * `{ available, empty }` envelopes so a partial fleet still renders\n * (\"financeq is not installed\" / \"procest is not installed\").\n *\n * @spec openspec/changes/cn-widget-library/specs/cn-widget-library/spec.md\n */\nexport default {\n\tname: 'CnSpendAnalyticsWidget',\n\n\tinject: {\n\t\tcnSpendAnalyticsSource: {\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content `{period, viewMode, filters, aiInsights}`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Consumer-supplied data source overriding the\n\t\t * `cnSpendAnalyticsSource` injection. Must expose `fetchSummary`,\n\t\t * `fetchVendorCommitments`, and `fetchNarrative`; may expose\n\t\t * `resolveDeepLink`, which must be pure and synchronous, since it is\n\t\t * called for every row on every render. When `null` the injection\n\t\t * (then the empty state) is used.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tdataSource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tperiod: PERIODS.includes(this.content && this.content.period) ? this.content.period : 'quarter',\n\t\t\tloading: false,\n\t\t\tfinance: { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] },\n\t\t\tvendor: { available: false, empty: false, topVendors: [] },\n\t\t\taiNarrative: '',\n\t\t\taiUnavailable: false,\n\t\t\tdebounceTimer: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The active data source (prop wins over injection).\n\t\t *\n\t\t * @return {object|null} the resolved source, or `null`.\n\t\t */\n\t\tsource() {\n\t\t\tconst candidate = this.dataSource || this.cnSpendAnalyticsSource\n\t\t\treturn candidate && typeof candidate.fetchSummary === 'function' ? candidate : null\n\t\t},\n\n\t\t/**\n\t\t * Whether a usable spend source is configured.\n\t\t *\n\t\t * @return {boolean} true when a source exists.\n\t\t */\n\t\thasSource() {\n\t\t\treturn this.source !== null\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-category row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcategoryLinks() {\n\t\t\treturn (this.finance.byCategory || []).map((row) => this.deepLink('financeq', 'category', row.category))\n\t\t},\n\n\t\t/**\n\t\t * Deep link of each top-vendor row, by index ('' when unresolved).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tvendorLinks() {\n\t\t\treturn (this.vendor.topVendors || []).map((row) => this.deepLink('procest', 'vendor', row.vendor))\n\t\t},\n\n\t\t/**\n\t\t * The validated view mode.\n\t\t *\n\t\t * @return {string} the view mode.\n\t\t */\n\t\tviewMode() {\n\t\t\treturn VIEW_MODES.includes(this.content && this.content.viewMode) ? this.content.viewMode : 'summary'\n\t\t},\n\n\t\t/**\n\t\t * Whether the AI narrative panel is enabled.\n\t\t *\n\t\t * @return {boolean} the AI-enabled flag.\n\t\t */\n\t\taiEnabled() {\n\t\t\treturn !!(this.content && this.content.aiInsights && this.content.aiInsights.enabled === true)\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is absent.\n\t\t *\n\t\t * @return {boolean} true when finance is unavailable.\n\t\t */\n\t\tfinanceMissing() {\n\t\t\treturn this.finance.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether financeq is present but has no data in the window.\n\t\t *\n\t\t * @return {boolean} true when finance is available but empty.\n\t\t */\n\t\tfinanceEmpty() {\n\t\t\treturn this.finance.available === true && this.finance.empty === true\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is absent.\n\t\t *\n\t\t * @return {boolean} true when vendor data is unavailable.\n\t\t */\n\t\tvendorMissing() {\n\t\t\treturn this.vendor.available === false\n\t\t},\n\n\t\t/**\n\t\t * Whether procest is present but has no vendor data in the window.\n\t\t *\n\t\t * @return {boolean} true when vendor is available but empty.\n\t\t */\n\t\tvendorEmpty() {\n\t\t\treturn this.vendor.available === true && this.vendor.empty === true\n\t\t},\n\n\t\t/**\n\t\t * The reporting-period dropdown options.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>} the options.\n\t\t */\n\t\tperiodOptions() {\n\t\t\treturn [\n\t\t\t\t{ value: 'month', label: t('nextcloud-vue', 'This month') },\n\t\t\t\t{ value: 'quarter', label: t('nextcloud-vue', 'This quarter') },\n\t\t\t\t{ value: 'ytd', label: t('nextcloud-vue', 'Year to date') },\n\t\t\t\t{ value: 'fy', label: t('nextcloud-vue', 'Fiscal year') },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The formatted total-spend amount.\n\t\t *\n\t\t * @return {string} the formatted total.\n\t\t */\n\t\tformattedTotal() {\n\t\t\treturn this.formatAmount(this.finance.total)\n\t\t},\n\n\t\t/**\n\t\t * The accessible label for the total-spend value.\n\t\t *\n\t\t * @return {string} the aria label.\n\t\t */\n\t\ttotalAriaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Total spend: {amount}').replace('{amount}', this.formattedTotal)\n\t\t},\n\t},\n\n\twatch: {\n\t\tviewMode() {\n\t\t\tthis.scheduleFetch()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchData()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceTimer) {\n\t\t\tclearTimeout(this.debounceTimer)\n\t\t}\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Handle a reporting-period change.\n\t\t *\n\t\t * @param {string} value the new period.\n\t\t * @return {void}\n\t\t */\n\t\tonPeriodChange(value) {\n\t\t\tif (PERIODS.includes(value)) {\n\t\t\t\tthis.period = value\n\t\t\t\tthis.scheduleFetch()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Debounce period / view changes before re-querying.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleFetch() {\n\t\t\tif (this.debounceTimer) {\n\t\t\t\tclearTimeout(this.debounceTimer)\n\t\t\t}\n\t\t\tthis.debounceTimer = setTimeout(() => {\n\t\t\t\tthis.fetchData()\n\t\t\t}, DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Fetch finance (always) + vendor (top-vendors view only) data in\n\t\t * parallel through the source. A missing source or a failure degrades\n\t\t * to the relevant empty state without throwing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchData() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tconst filters = (this.content && this.content.filters) || {}\n\t\t\ttry {\n\t\t\t\tconst tasks = [\n\t\t\t\t\tthis.source.fetchSummary({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tcategoryIds: Array.isArray(filters.categoryIds) ? filters.categoryIds : [],\n\t\t\t\t\t\tdepartmentIds: Array.isArray(filters.departmentIds) ? filters.departmentIds : [],\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t\tif (this.viewMode === 'top-vendors' && typeof this.source.fetchVendorCommitments === 'function') {\n\t\t\t\t\ttasks.push(this.source.fetchVendorCommitments({\n\t\t\t\t\t\tperiod: this.period,\n\t\t\t\t\t\tvendorIds: Array.isArray(filters.vendorIds) ? filters.vendorIds : [],\n\t\t\t\t\t}))\n\t\t\t\t}\n\t\t\t\tconst [finance, vendor] = await Promise.all(tasks)\n\t\t\t\tif (finance) {\n\t\t\t\t\tthis.finance = finance\n\t\t\t\t}\n\t\t\t\tif (vendor) {\n\t\t\t\t\tthis.vendor = vendor\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.finance = { available: false, empty: false, total: 0, currency: 'EUR', byCategory: [], trend: [] }\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 * Request an AI narrative through the source. When the source lacks\n\t\t * `fetchNarrative` or reports it unavailable, the panel disables\n\t\t * gracefully.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync requestNarrative() {\n\t\t\tif (!this.source || typeof this.source.fetchNarrative !== 'function') {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await this.source.fetchNarrative({\n\t\t\t\t\tsummary: { total: this.finance.total, currency: this.finance.currency, period: this.period },\n\t\t\t\t}) || {}\n\t\t\t\tif (result.available) {\n\t\t\t\t\tthis.aiNarrative = result.narrative\n\t\t\t\t\tthis.aiUnavailable = false\n\t\t\t\t} else {\n\t\t\t\t\tthis.aiUnavailable = true\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.aiUnavailable = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The owning sibling app's URL for a row. The source may provide\n\t\t * `resolveDeepLink`; absent that, the row renders as plain text.\n\t\t *\n\t\t * @param {string} app the owning app id.\n\t\t * @param {string} kind the entity kind.\n\t\t * @param {string} id the entity id.\n\t\t * @return {string} the URL, or '' when unresolved.\n\t\t */\n\t\tdeepLink(app, kind, id) {\n\t\t\tif (this.source && typeof this.source.resolveDeepLink === 'function') {\n\t\t\t\tconst href = this.source.resolveDeepLink(app, kind, id)\n\t\t\t\tif (typeof href === 'string' && href !== '') {\n\t\t\t\t\treturn href\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * Format a monetary amount respecting the browser locale.\n\t\t *\n\t\t * @param {number} amount the amount.\n\t\t * @return {string} the locale-formatted currency string.\n\t\t */\n\t\tformatAmount(amount) {\n\t\t\tconst value = Number(amount) || 0\n\t\t\tconst locale = (typeof navigator !== 'undefined' && navigator.language) || 'en'\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(this.finance.currency),\n\t\t\t\t}).format(value)\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-spend-analytics-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tgap: 8px;\n\tpadding: 8px;\n\toverflow: hidden;\n}\n\n.cn-spend-analytics-widget__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.cn-spend-analytics-widget__title {\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__period {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tpadding: 2px 6px;\n}\n\n.cn-spend-analytics-widget__body {\n\tflex: 1;\n\toverflow: auto;\n}\n\n.cn-spend-analytics-widget__cards {\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-spend-analytics-widget__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tflex: 1;\n}\n\n.cn-spend-analytics-widget__card-label {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-spend-analytics-widget__card-value {\n\tfont-size: 1.4em;\n\tfont-weight: bold;\n}\n\n.cn-spend-analytics-widget__panel-title {\n\tfont-size: 1em;\n\tmargin: 8px 0 4px;\n}\n\n.cn-spend-analytics-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-spend-analytics-widget__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-spend-analytics-widget__row--static {\n\tcursor: default;\n}\n\n.cn-spend-analytics-widget__row:hover,\n.cn-spend-analytics-widget__row:focus {\n\tbackground: var(--color-background-hover);\n\toutline: none;\n}\n\n.cn-spend-analytics-widget__state {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 64px;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 12px;\n\ttext-align: center;\n}\n\n.cn-spend-analytics-widget__inline-empty {\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 6px;\n\ttext-align: left;\n}\n\n.cn-spend-analytics-widget__ai-text {\n\twhite-space: pre-wrap;\n}\n\n.cn-spend-analytics-widget__ai-btn {\n\tborder: 1px solid var(--color-border);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tcolor: var(--color-main-text);\n\tpadding: 4px 12px;\n\tcursor: pointer;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import script from './CnStackedBarWidget.vue2.js';
2
+ export { rampColor } from './CnStackedBarWidget.vue2.js';
3
+ import { render } from './CnStackedBarWidget.vue3.js';
4
+
5
+ script.render = render;
6
+ script.__scopeId = "data-v-c0bac1a2";
7
+ script.__file = "src/components/CnStackedBarWidget/CnStackedBarWidget.vue";
8
+ const __sfc_main = script;
9
+
10
+ export { __sfc_main as default };
11
+ //# sourceMappingURL=CnStackedBarWidget.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnStackedBarWidget.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
@@ -0,0 +1,298 @@
1
+ import { translate } from '@nextcloud/l10n';
2
+ import { NcLoadingIcon } from '@nextcloud/vue';
3
+ import { fetchGroupedCounts } from '../../utils/fetchAggregate.js';
4
+
5
+ /** The lightest step keeps this share of the primary colour, so it still stands out from the page. */
6
+ const LIGHTEST_PRIMARY_SHARE = 70;
7
+
8
+ /**
9
+ * The colour of step `index` out of `count`, on one ramp that runs from a
10
+ * light tint of the primary colour, through the primary colour, to the text
11
+ * colour. Steps differ in LIGHTNESS, so they stay apart for somebody who
12
+ * cannot tell hues apart, and the ramp follows the theme because it is built
13
+ * from Nextcloud's own variables.
14
+ *
15
+ * @param {number} index The step, 0-based.
16
+ * @param {number} count The number of steps.
17
+ * @return {string} A CSS colour.
18
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
19
+ */
20
+ function rampColor(index, count) {
21
+ if (count <= 1) {
22
+ return 'var(--color-primary-element)'
23
+ }
24
+ const position = index / (count - 1);
25
+ if (position < 0.5) {
26
+ const share = Math.round(LIGHTEST_PRIMARY_SHARE + (position / 0.5) * (100 - LIGHTEST_PRIMARY_SHARE));
27
+ return `color-mix(in srgb, var(--color-primary-element) ${share}%, var(--color-main-background))`
28
+ }
29
+ const textShare = Math.round(((position - 0.5) / 0.5) * 100);
30
+ if (textShare === 0) {
31
+ return 'var(--color-primary-element)'
32
+ }
33
+ if (textShare === 100) {
34
+ return 'var(--color-main-text)'
35
+ }
36
+ return `color-mix(in srgb, var(--color-main-text) ${textShare}%, var(--color-primary-element))`
37
+ }
38
+
39
+ /**
40
+ * CnStackedBarWidget shows how a total divides over a few groups: ONE bar cut
41
+ * into segments, with a legend below it that names every segment and gives
42
+ * its count. Use it for "my cases per step" or "requests per channel".
43
+ *
44
+ * Segments come from one grouped count request against an OpenRegister
45
+ * source (`source.groupBy`), or from static `segments`. `order` fixes the
46
+ * order of the segments, and `labels` gives a group key a readable name.
47
+ * Colours come from one ramp built on the theme's primary colour; the steps
48
+ * differ in lightness, never in hue alone. Resolved by its registry type key
49
+ * `stacked-bar`.
50
+ *
51
+ * ```json
52
+ * {
53
+ * "widgetKey": "stacked-bar",
54
+ * "props": {
55
+ * "content": {
56
+ * "source": { "register": "dossiq", "schema": "case", "groupBy": "status", "filter": { "assignee": "@me" } },
57
+ * "order": ["received", "in_progress", "decision", "publish"],
58
+ * "labels": { "received": "Received", "in_progress": "In progress", "decision": "Decision", "publish": "Publish" }
59
+ * }
60
+ * }
61
+ * }
62
+ * ```
63
+ */
64
+ var script = {
65
+ name: 'CnStackedBarWidget',
66
+
67
+ components: { NcLoadingIcon },
68
+
69
+ inject: {
70
+ /**
71
+ * Host translate function provided by CnAppRoot. Segment labels and
72
+ * the empty text run through it. Identity by default.
73
+ */
74
+ cnTranslate: { default: () => (key) => key },
75
+ },
76
+
77
+ props: {
78
+ /**
79
+ * The widget's configuration. `source` names the OpenRegister
80
+ * register and schema, the `groupBy` field and an optional `filter`
81
+ * (the shared @-token grammar). `segments` are static entries
82
+ * `{ key?, label, value }` used when there is no source. `order` lists
83
+ * group keys in the order they render; groups it does not list follow
84
+ * in the order they arrive, and a listed key without records renders
85
+ * with a count of 0. `labels` maps a group key to its label.
86
+ * `emptyText` is shown when there is nothing to count.
87
+ *
88
+ * @type {{source?: {register?: string, schema?: string, groupBy?: string, filter?: object}, segments?: Array<{key?: string, label: string, value: number}>, order?: Array<string>, labels?: {[key: string]: string}, emptyText?: string}}
89
+ */
90
+ content: {
91
+ type: Object,
92
+ default: () => ({}),
93
+ },
94
+
95
+ /**
96
+ * Translate function. Falls back to the injected `cnTranslate`. Pass it
97
+ * when mounting the widget outside a CnAppRoot.
98
+ *
99
+ * @type {((key: string) => string)|null}
100
+ */
101
+ translate: {
102
+ type: Function,
103
+ default: null,
104
+ },
105
+ },
106
+
107
+ data() {
108
+ return {
109
+ groups: [],
110
+ loading: false,
111
+ error: '',
112
+ }
113
+ },
114
+
115
+ computed: {
116
+ /**
117
+ * The translate function in use.
118
+ *
119
+ * @return {(key: string) => string}
120
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
121
+ */
122
+ effectiveTranslate() {
123
+ return this.translate ?? this.cnTranslate
124
+ },
125
+
126
+ /**
127
+ * Whether the segments come from OpenRegister.
128
+ *
129
+ * @return {boolean}
130
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
131
+ */
132
+ hasSource() {
133
+ const s = this.content.source || {};
134
+ return Boolean(s.register && s.schema && s.groupBy)
135
+ },
136
+
137
+ /**
138
+ * The raw groups, from the source or the static list, as `{ key, label, count }`.
139
+ *
140
+ * @return {Array<{key: string, label: string, count: number}>}
141
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
142
+ */
143
+ rawGroups() {
144
+ if (this.hasSource) {
145
+ return this.groups.map((group) => ({ key: group.key, label: group.key, count: group.count }))
146
+ }
147
+ const list = Array.isArray(this.content.segments) ? this.content.segments : [];
148
+ return list
149
+ .filter((segment) => segment && typeof segment === 'object')
150
+ .map((segment, index) => {
151
+ const count = Number(segment.value);
152
+ return {
153
+ key: String(segment.key ?? segment.label ?? index),
154
+ label: String(segment.label ?? segment.key ?? ''),
155
+ count: Number.isFinite(count) && count > 0 ? count : 0,
156
+ }
157
+ })
158
+ },
159
+
160
+ /**
161
+ * The segments in render order, each with its label and colour.
162
+ *
163
+ * @return {Array<{key: string, label: string, count: number, color: string}>}
164
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
165
+ */
166
+ segments() {
167
+ const order = Array.isArray(this.content.order) ? this.content.order.map(String) : [];
168
+ const labels = (this.content.labels && typeof this.content.labels === 'object') ? this.content.labels : {};
169
+ const byKey = {};
170
+ for (const group of this.rawGroups) {
171
+ byKey[group.key] = group;
172
+ }
173
+ const ordered = [];
174
+ for (const key of order) {
175
+ ordered.push(byKey[key] || { key, label: key, count: 0 });
176
+ }
177
+ for (const group of this.rawGroups) {
178
+ if (!order.includes(group.key)) {
179
+ ordered.push(group);
180
+ }
181
+ }
182
+ return ordered.map((group, index) => ({
183
+ key: group.key,
184
+ label: this.effectiveTranslate(typeof labels[group.key] === 'string' ? labels[group.key] : group.label),
185
+ count: group.count,
186
+ color: rampColor(index, ordered.length),
187
+ }))
188
+ },
189
+
190
+ /**
191
+ * The segments that take up room in the bar.
192
+ *
193
+ * @return {Array<object>}
194
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
195
+ */
196
+ barSegments() {
197
+ return this.segments.filter((segment) => segment.count > 0)
198
+ },
199
+
200
+ /**
201
+ * The sum of all counts.
202
+ *
203
+ * @return {number}
204
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
205
+ */
206
+ total() {
207
+ return this.segments.reduce((sum, segment) => sum + segment.count, 0)
208
+ },
209
+
210
+ /**
211
+ * The text shown when there is nothing to count.
212
+ *
213
+ * @return {string}
214
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
215
+ */
216
+ emptyLabel() {
217
+ const text = this.content.emptyText;
218
+ return (typeof text === 'string' && text !== '')
219
+ ? this.effectiveTranslate(text)
220
+ : translate('nextcloud-vue', 'Nothing to show yet')
221
+ },
222
+
223
+ /**
224
+ * @return {string} The loading text.
225
+ *
226
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
227
+ */
228
+ loadingLabel() {
229
+ return translate('nextcloud-vue', 'Loading…')
230
+ },
231
+
232
+ /**
233
+ * @return {string} The text shown when the counts could not be loaded.
234
+ *
235
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
236
+ */
237
+ errorLabel() {
238
+ return translate('nextcloud-vue', 'The numbers could not be loaded.')
239
+ },
240
+
241
+ /**
242
+ * Changes whenever a new fetch is needed.
243
+ *
244
+ * @return {string}
245
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
246
+ */
247
+ sourceKey() {
248
+ return JSON.stringify(this.content.source || {})
249
+ },
250
+ },
251
+
252
+ watch: {
253
+ /** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget */
254
+ sourceKey() {
255
+ this.fetchGroups();
256
+ },
257
+ },
258
+
259
+ mounted() {
260
+ this.fetchGroups();
261
+ },
262
+
263
+ methods: {
264
+ /**
265
+ * Load the count per group in one request.
266
+ *
267
+ * @return {Promise<void>}
268
+ * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget
269
+ */
270
+ async fetchGroups() {
271
+ if (!this.hasSource) {
272
+ this.groups = [];
273
+ this.error = '';
274
+ return
275
+ }
276
+ const requestKey = this.sourceKey;
277
+ this.loading = true;
278
+ this.error = '';
279
+ try {
280
+ const groups = await fetchGroupedCounts(this.content.source);
281
+ if (requestKey !== this.sourceKey) {
282
+ return
283
+ }
284
+ this.groups = groups;
285
+ } catch (e) {
286
+ // eslint-disable-next-line no-console
287
+ console.warn('[CnStackedBarWidget] failed to load counts:', e);
288
+ this.error = (e && e.message) || 'error';
289
+ this.groups = [];
290
+ } finally {
291
+ this.loading = false;
292
+ }
293
+ },
294
+ },
295
+ };
296
+
297
+ export { script as default, rampColor };
298
+ //# sourceMappingURL=CnStackedBarWidget.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnStackedBarWidget.vue2.js","sources":["../../../../src/components/CnStackedBarWidget/CnStackedBarWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-stacked-bar\" data-testid=\"cn-stacked-bar\">\n\t\t<div v-if=\"loading\" class=\"cn-stacked-bar__state\" role=\"status\">\n\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t\t<span>{{ loadingLabel }}</span>\n\t\t</div>\n\t\t<p v-else-if=\"error\" class=\"cn-stacked-bar__state\" role=\"status\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<p v-else-if=\"total === 0\" class=\"cn-stacked-bar__state\" data-testid=\"cn-stacked-bar-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<template v-else>\n\t\t\t<!-- The bar is a picture of the legend below it. The legend holds\n\t\t\t every label and number, so the bar itself is hidden from\n\t\t\t assistive technology rather than described twice. -->\n\t\t\t<div class=\"cn-stacked-bar__bar\" aria-hidden=\"true\">\n\t\t\t\t<span\n\t\t\t\t\tv-for=\"segment in barSegments\"\n\t\t\t\t\t:key=\"segment.key\"\n\t\t\t\t\tclass=\"cn-stacked-bar__segment\"\n\t\t\t\t\t:style=\"{ flexGrow: segment.count, backgroundColor: segment.color }\" />\n\t\t\t</div>\n\t\t\t<ul class=\"cn-stacked-bar__legend\">\n\t\t\t\t<li\n\t\t\t\t\tv-for=\"segment in segments\"\n\t\t\t\t\t:key=\"segment.key\"\n\t\t\t\t\tclass=\"cn-stacked-bar__legend-item\"\n\t\t\t\t\tdata-testid=\"cn-stacked-bar-legend-item\">\n\t\t\t\t\t<span class=\"cn-stacked-bar__label\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclass=\"cn-stacked-bar__dot\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t:style=\"{ backgroundColor: segment.color }\" />\n\t\t\t\t\t\t{{ segment.label }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<!-- The leading space keeps label and count two words for a\n\t\t\t\t\t screen reader; it is not drawn in this flex column. -->\n\t\t\t\t\t<strong class=\"cn-stacked-bar__count\">{{ ' ' + segment.count }}</strong>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { fetchGroupedCounts } from '../../utils/fetchAggregate.js'\n\n/** The lightest step keeps this share of the primary colour, so it still stands out from the page. */\nconst LIGHTEST_PRIMARY_SHARE = 70\n\n/**\n * The colour of step `index` out of `count`, on one ramp that runs from a\n * light tint of the primary colour, through the primary colour, to the text\n * colour. Steps differ in LIGHTNESS, so they stay apart for somebody who\n * cannot tell hues apart, and the ramp follows the theme because it is built\n * from Nextcloud's own variables.\n *\n * @param {number} index The step, 0-based.\n * @param {number} count The number of steps.\n * @return {string} A CSS colour.\n * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n */\nexport function rampColor(index, count) {\n\tif (count <= 1) {\n\t\treturn 'var(--color-primary-element)'\n\t}\n\tconst position = index / (count - 1)\n\tif (position < 0.5) {\n\t\tconst share = Math.round(LIGHTEST_PRIMARY_SHARE + (position / 0.5) * (100 - LIGHTEST_PRIMARY_SHARE))\n\t\treturn `color-mix(in srgb, var(--color-primary-element) ${share}%, var(--color-main-background))`\n\t}\n\tconst textShare = Math.round(((position - 0.5) / 0.5) * 100)\n\tif (textShare === 0) {\n\t\treturn 'var(--color-primary-element)'\n\t}\n\tif (textShare === 100) {\n\t\treturn 'var(--color-main-text)'\n\t}\n\treturn `color-mix(in srgb, var(--color-main-text) ${textShare}%, var(--color-primary-element))`\n}\n\n/**\n * CnStackedBarWidget shows how a total divides over a few groups: ONE bar cut\n * into segments, with a legend below it that names every segment and gives\n * its count. Use it for \"my cases per step\" or \"requests per channel\".\n *\n * Segments come from one grouped count request against an OpenRegister\n * source (`source.groupBy`), or from static `segments`. `order` fixes the\n * order of the segments, and `labels` gives a group key a readable name.\n * Colours come from one ramp built on the theme's primary colour; the steps\n * differ in lightness, never in hue alone. Resolved by its registry type key\n * `stacked-bar`.\n *\n * ```json\n * {\n * \"widgetKey\": \"stacked-bar\",\n * \"props\": {\n * \"content\": {\n * \"source\": { \"register\": \"dossiq\", \"schema\": \"case\", \"groupBy\": \"status\", \"filter\": { \"assignee\": \"@me\" } },\n * \"order\": [\"received\", \"in_progress\", \"decision\", \"publish\"],\n * \"labels\": { \"received\": \"Received\", \"in_progress\": \"In progress\", \"decision\": \"Decision\", \"publish\": \"Publish\" }\n * }\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnStackedBarWidget',\n\n\tcomponents: { NcLoadingIcon },\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot. Segment labels and\n\t\t * the empty text run through it. Identity by default.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's configuration. `source` names the OpenRegister\n\t\t * register and schema, the `groupBy` field and an optional `filter`\n\t\t * (the shared @-token grammar). `segments` are static entries\n\t\t * `{ key?, label, value }` used when there is no source. `order` lists\n\t\t * group keys in the order they render; groups it does not list follow\n\t\t * in the order they arrive, and a listed key without records renders\n\t\t * with a count of 0. `labels` maps a group key to its label.\n\t\t * `emptyText` is shown when there is nothing to count.\n\t\t *\n\t\t * @type {{source?: {register?: string, schema?: string, groupBy?: string, filter?: object}, segments?: Array<{key?: string, label: string, value: number}>, order?: Array<string>, labels?: {[key: string]: string}, emptyText?: string}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`. Pass it\n\t\t * when mounting the widget outside a CnAppRoot.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tgroups: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function in use.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * Whether the segments come from OpenRegister.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\thasSource() {\n\t\t\tconst s = this.content.source || {}\n\t\t\treturn Boolean(s.register && s.schema && s.groupBy)\n\t\t},\n\n\t\t/**\n\t\t * The raw groups, from the source or the static list, as `{ key, label, count }`.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, count: number}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\trawGroups() {\n\t\t\tif (this.hasSource) {\n\t\t\t\treturn this.groups.map((group) => ({ key: group.key, label: group.key, count: group.count }))\n\t\t\t}\n\t\t\tconst list = Array.isArray(this.content.segments) ? this.content.segments : []\n\t\t\treturn list\n\t\t\t\t.filter((segment) => segment && typeof segment === 'object')\n\t\t\t\t.map((segment, index) => {\n\t\t\t\t\tconst count = Number(segment.value)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(segment.key ?? segment.label ?? index),\n\t\t\t\t\t\tlabel: String(segment.label ?? segment.key ?? ''),\n\t\t\t\t\t\tcount: Number.isFinite(count) && count > 0 ? count : 0,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The segments in render order, each with its label and colour.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, count: number, color: string}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tsegments() {\n\t\t\tconst order = Array.isArray(this.content.order) ? this.content.order.map(String) : []\n\t\t\tconst labels = (this.content.labels && typeof this.content.labels === 'object') ? this.content.labels : {}\n\t\t\tconst byKey = {}\n\t\t\tfor (const group of this.rawGroups) {\n\t\t\t\tbyKey[group.key] = group\n\t\t\t}\n\t\t\tconst ordered = []\n\t\t\tfor (const key of order) {\n\t\t\t\tordered.push(byKey[key] || { key, label: key, count: 0 })\n\t\t\t}\n\t\t\tfor (const group of this.rawGroups) {\n\t\t\t\tif (!order.includes(group.key)) {\n\t\t\t\t\tordered.push(group)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ordered.map((group, index) => ({\n\t\t\t\tkey: group.key,\n\t\t\t\tlabel: this.effectiveTranslate(typeof labels[group.key] === 'string' ? labels[group.key] : group.label),\n\t\t\t\tcount: group.count,\n\t\t\t\tcolor: rampColor(index, ordered.length),\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The segments that take up room in the bar.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tbarSegments() {\n\t\t\treturn this.segments.filter((segment) => segment.count > 0)\n\t\t},\n\n\t\t/**\n\t\t * The sum of all counts.\n\t\t *\n\t\t * @return {number}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\ttotal() {\n\t\t\treturn this.segments.reduce((sum, segment) => sum + segment.count, 0)\n\t\t},\n\n\t\t/**\n\t\t * The text shown when there is nothing to count.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\temptyLabel() {\n\t\t\tconst text = this.content.emptyText\n\t\t\treturn (typeof text === 'string' && text !== '')\n\t\t\t\t? this.effectiveTranslate(text)\n\t\t\t\t: t('nextcloud-vue', 'Nothing to show yet')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The loading text.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The text shown when the counts could not be loaded.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\terrorLabel() {\n\t\t\treturn t('nextcloud-vue', 'The numbers could not be loaded.')\n\t\t},\n\n\t\t/**\n\t\t * Changes whenever a new fetch is needed.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify(this.content.source || {})\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget */\n\t\tsourceKey() {\n\t\t\tthis.fetchGroups()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchGroups()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Load the count per group in one request.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-stacked-bar-widget\n\t\t */\n\t\tasync fetchGroups() {\n\t\t\tif (!this.hasSource) {\n\t\t\t\tthis.groups = []\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst requestKey = this.sourceKey\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst groups = await fetchGroupedCounts(this.content.source)\n\t\t\t\tif (requestKey !== this.sourceKey) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.groups = groups\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnStackedBarWidget] failed to load counts:', e)\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.groups = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-stacked-bar {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\twidth: 100%;\n}\n\n.cn-stacked-bar__state {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-stacked-bar__bar {\n\tdisplay: flex;\n\tgap: 3px;\n\theight: 12px;\n}\n\n.cn-stacked-bar__segment {\n\tflex-basis: 0;\n\tmin-width: 4px;\n\tborder-radius: 6px;\n}\n\n.cn-stacked-bar__legend {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n\tgap: 12px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-stacked-bar__legend-item {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-stacked-bar__label {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tfont-size: 0.95em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow-wrap: anywhere;\n}\n\n.cn-stacked-bar__dot {\n\tflex: none;\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 3px;\n}\n\n.cn-stacked-bar__count {\n\tfont-size: 1.5em;\n\tfont-weight: 700;\n\tline-height: 1.2;\n\tcolor: var(--color-main-text);\n}\n</style>\n"],"names":["t"],"mappings":"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"}