@ngbplatform/ui 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. package/vite-public-assets.js +68 -0
@@ -0,0 +1,220 @@
1
+ import { computed, ref, watch, type CSSProperties, type ComputedRef, type Ref } from 'vue';
2
+ import { loadJson, saveJson } from '../../utils/storage';
3
+ import type { RegisterColumn } from './registerTypes';
4
+
5
+ type PersistedColumns = {
6
+ order?: string[];
7
+ widths?: Record<string, number>;
8
+ visible?: string[];
9
+ };
10
+
11
+ type UseRegisterColumnStateArgs = {
12
+ columns: ComputedRef<RegisterColumn[]>;
13
+ visibleColumnKeys: ComputedRef<string[] | undefined>;
14
+ storageKey: ComputedRef<string | undefined>;
15
+ showStatusColumn: ComputedRef<boolean>;
16
+ statusColWidth: ComputedRef<number>;
17
+ emitVisibleColumnKeys: (value: string[]) => void;
18
+ };
19
+
20
+ type RegisterColumnState = {
21
+ localOrder: Ref<string[]>;
22
+ localWidths: Ref<Record<string, number>>;
23
+ visibleColumns: ComputedRef<RegisterColumn[]>;
24
+ gridTemplateColumns: ComputedRef<string>;
25
+ colWidth: (column: RegisterColumn) => number;
26
+ stickyStyle: (column: RegisterColumn) => CSSProperties;
27
+ cellStickyStyle: (column: RegisterColumn) => CSSProperties;
28
+ setVisible: (keys: string[]) => void;
29
+ };
30
+
31
+ function isAccidentalVisible(value: string[] | null) {
32
+ return !!value && value.length === 1 && value[0] === 'display';
33
+ }
34
+
35
+ export function useRegisterColumnState(args: UseRegisterColumnStateArgs): RegisterColumnState {
36
+ const localOrder = ref<string[]>(args.columns.value.map((column) => column.key));
37
+ const localWidths = ref<Record<string, number>>({});
38
+ const localVisible = ref<string[] | null>(args.visibleColumnKeys.value ? [...args.visibleColumnKeys.value] : null);
39
+ const persistEnabled = ref(false);
40
+ const derivedFromPartialColumns = ref(args.columns.value.length <= 1);
41
+
42
+ function ensureStateAfterColumnsReady() {
43
+ const columns = args.columns.value;
44
+ if (columns.length <= 1) return;
45
+
46
+ const keys = columns.map((column) => column.key);
47
+ if (derivedFromPartialColumns.value) {
48
+ if (localOrder.value.length <= 1 && localOrder.value[0] === 'display') {
49
+ localOrder.value = [...keys];
50
+ }
51
+ if (isAccidentalVisible(localVisible.value)) {
52
+ localVisible.value = null;
53
+ }
54
+ derivedFromPartialColumns.value = false;
55
+ }
56
+
57
+ if (isAccidentalVisible(localVisible.value) && keys.length > 1) {
58
+ localVisible.value = null;
59
+ }
60
+
61
+ if (!persistEnabled.value) persistEnabled.value = true;
62
+ }
63
+
64
+ function resetLocalStateFromColumns() {
65
+ const columns = args.columns.value;
66
+ localOrder.value = columns.map((column) => column.key);
67
+ localWidths.value = {};
68
+ localVisible.value = args.visibleColumnKeys.value ? [...args.visibleColumnKeys.value] : null;
69
+ derivedFromPartialColumns.value = columns.length <= 1;
70
+ }
71
+
72
+ function hydrateFromStorage() {
73
+ const storageKey = args.storageKey.value;
74
+ if (!storageKey) return;
75
+
76
+ const saved = loadJson<PersistedColumns | string[] | null>(storageKey, null);
77
+ if (!saved) return;
78
+
79
+ if (Array.isArray(saved)) {
80
+ localVisible.value = [...saved];
81
+ return;
82
+ }
83
+
84
+ if (saved.order?.length) localOrder.value = saved.order;
85
+ if (saved.widths) localWidths.value = saved.widths;
86
+ if (Array.isArray(saved.visible)) localVisible.value = [...saved.visible];
87
+ }
88
+
89
+ watch(
90
+ args.storageKey,
91
+ (next, prev) => {
92
+ if (!next) return;
93
+ if (prev && next === prev) return;
94
+ resetLocalStateFromColumns();
95
+ hydrateFromStorage();
96
+ ensureStateAfterColumnsReady();
97
+ },
98
+ { immediate: true },
99
+ );
100
+
101
+ watch(
102
+ () => args.columns.value.map((column) => column.key).join('|'),
103
+ () => {
104
+ ensureStateAfterColumnsReady();
105
+ },
106
+ { immediate: true },
107
+ );
108
+
109
+ watch(
110
+ [localOrder, localWidths, localVisible],
111
+ () => {
112
+ const storageKey = args.storageKey.value;
113
+ if (!storageKey || !persistEnabled.value) return;
114
+
115
+ saveJson<PersistedColumns>(storageKey, {
116
+ order: localOrder.value,
117
+ widths: localWidths.value,
118
+ visible: localVisible.value ?? undefined,
119
+ });
120
+ },
121
+ { deep: true },
122
+ );
123
+
124
+ watch(
125
+ args.visibleColumnKeys,
126
+ (value) => {
127
+ if (!value) return;
128
+ localVisible.value = [...value];
129
+ },
130
+ { deep: true },
131
+ );
132
+
133
+ const orderedColumns = computed(() => {
134
+ const byKey = new Map(args.columns.value.map((column) => [column.key, column] as const));
135
+ const result: RegisterColumn[] = [];
136
+
137
+ for (const key of localOrder.value) {
138
+ const column = byKey.get(key);
139
+ if (column) result.push(column);
140
+ }
141
+
142
+ for (const column of args.columns.value) {
143
+ if (!result.find((entry) => entry.key === column.key)) result.push(column);
144
+ }
145
+
146
+ return result;
147
+ });
148
+
149
+ const visibleColumns = computed(() => {
150
+ const keys = localVisible.value ?? args.visibleColumnKeys.value ?? args.columns.value.map((column) => column.key);
151
+ const visibleSet = new Set(keys);
152
+ return orderedColumns.value.filter((column) => visibleSet.has(column.key));
153
+ });
154
+
155
+ function setVisible(keys: string[]) {
156
+ localVisible.value = keys;
157
+ args.emitVisibleColumnKeys(keys);
158
+ }
159
+
160
+ function colWidth(column: RegisterColumn) {
161
+ const width = localWidths.value[column.key] ?? column.width ?? 140;
162
+ const minWidth = column.minWidth ?? 80;
163
+ return Math.max(minWidth, width);
164
+ }
165
+
166
+ const gridTemplateColumns = computed(() => {
167
+ const columns = [
168
+ ...(args.showStatusColumn.value ? [`${args.statusColWidth.value}px`] : []),
169
+ ...visibleColumns.value.map((column) => `${colWidth(column)}px`),
170
+ ];
171
+ return columns.join(' ');
172
+ });
173
+
174
+ const stickyLeftOffsets = computed(() => {
175
+ let left = args.showStatusColumn.value ? args.statusColWidth.value : 0;
176
+ const offsets: Record<string, number> = {};
177
+
178
+ for (const column of visibleColumns.value) {
179
+ if (column.pinned === 'left') {
180
+ offsets[column.key] = left;
181
+ left += colWidth(column);
182
+ }
183
+ }
184
+
185
+ return offsets;
186
+ });
187
+
188
+ function stickyStyle(column: RegisterColumn): CSSProperties {
189
+ if (column.pinned !== 'left') return {};
190
+ const left = stickyLeftOffsets.value[column.key] ?? (args.showStatusColumn.value ? args.statusColWidth.value : 0);
191
+ return {
192
+ position: 'sticky',
193
+ left: `${left}px`,
194
+ zIndex: 2,
195
+ background: 'var(--ngb-grid-header)',
196
+ };
197
+ }
198
+
199
+ function cellStickyStyle(column: RegisterColumn): CSSProperties {
200
+ if (column.pinned !== 'left') return {};
201
+ const left = stickyLeftOffsets.value[column.key] ?? (args.showStatusColumn.value ? args.statusColWidth.value : 0);
202
+ return {
203
+ position: 'sticky',
204
+ left: `${left}px`,
205
+ zIndex: 1,
206
+ background: 'inherit',
207
+ };
208
+ }
209
+
210
+ return {
211
+ localOrder,
212
+ localWidths,
213
+ visibleColumns,
214
+ gridTemplateColumns,
215
+ colWidth,
216
+ stickyStyle,
217
+ cellStickyStyle,
218
+ setVisible,
219
+ };
220
+ }
@@ -0,0 +1,209 @@
1
+ import { computed, ref, watch, type ComputedRef } from 'vue';
2
+ import type { DisplayRow, RegisterDataRow, RegisterSortSpec, RowStatus } from './registerTypes';
3
+ import { isDisplayDataRow } from './registerTypes';
4
+
5
+ type UseRegisterRowsArgs = {
6
+ rows: ComputedRef<RegisterDataRow[]>;
7
+ groupBy: ComputedRef<string[]>;
8
+ sortBy: ComputedRef<RegisterSortSpec[]>;
9
+ defaultExpanded: ComputedRef<boolean>;
10
+ };
11
+
12
+ function compareRows(left: RegisterDataRow, right: RegisterDataRow, sortBy: RegisterSortSpec[]): number {
13
+ for (const sort of sortBy) {
14
+ const leftValue = left[sort.key];
15
+ const rightValue = right[sort.key];
16
+
17
+ if (leftValue === rightValue) continue;
18
+ if (leftValue == null) return sort.dir === 'asc' ? -1 : 1;
19
+ if (rightValue == null) return sort.dir === 'asc' ? 1 : -1;
20
+
21
+ const result = leftValue > rightValue ? 1 : -1;
22
+ return sort.dir === 'asc' ? result : -result;
23
+ }
24
+
25
+ return 0;
26
+ }
27
+
28
+ function groupKeyPart(row: RegisterDataRow, key: string) {
29
+ const value = row[key];
30
+ return value == null || value === '' ? '—' : String(value);
31
+ }
32
+
33
+ function collectGroupIds(rows: RegisterDataRow[], keys: string[], path: string[] = []): string[] {
34
+ if (keys.length === 0) return [];
35
+
36
+ const key = keys[0];
37
+ const buckets = new Map<string, RegisterDataRow[]>();
38
+ for (const row of rows) {
39
+ const label = groupKeyPart(row, key);
40
+ const bucket = buckets.get(label);
41
+ if (bucket) bucket.push(row);
42
+ else buckets.set(label, [row]);
43
+ }
44
+
45
+ const ids: string[] = [];
46
+ for (const [label, bucket] of buckets.entries()) {
47
+ const groupId = [...path, `${key}:${label}`].join('|');
48
+ ids.push(groupId);
49
+ ids.push(...collectGroupIds(bucket, keys.slice(1), [...path, `${key}:${label}`]));
50
+ }
51
+
52
+ return ids;
53
+ }
54
+
55
+ function buildGroups(rows: RegisterDataRow[], keys: string[], expandedGroups: Set<string>, path: string[] = []): DisplayRow[] {
56
+ if (keys.length === 0) {
57
+ return rows.map((row) => ({ ...row, type: 'row', __index: 0 }));
58
+ }
59
+
60
+ const key = keys[0];
61
+ const buckets = new Map<string, RegisterDataRow[]>();
62
+ for (const row of rows) {
63
+ const label = groupKeyPart(row, key);
64
+ const bucket = buckets.get(label);
65
+ if (bucket) bucket.push(row);
66
+ else buckets.set(label, [row]);
67
+ }
68
+
69
+ const output: DisplayRow[] = [];
70
+ for (const [label, bucket] of buckets.entries()) {
71
+ const groupId = [...path, `${key}:${label}`].join('|');
72
+ const totals = bucket.reduce(
73
+ (accumulator, row) => {
74
+ accumulator.debit += Number(row.debit ?? 0);
75
+ accumulator.credit += Number(row.credit ?? 0);
76
+ return accumulator;
77
+ },
78
+ { debit: 0, credit: 0 },
79
+ );
80
+
81
+ output.push({
82
+ type: 'group',
83
+ key: `g:${groupId}`,
84
+ groupId,
85
+ label,
86
+ count: bucket.length,
87
+ totalDebit: totals.debit,
88
+ totalCredit: totals.credit,
89
+ });
90
+
91
+ if (expandedGroups.has(groupId)) {
92
+ output.push(...buildGroups(bucket, keys.slice(1), expandedGroups, [...path, `${key}:${label}`]));
93
+ }
94
+ }
95
+
96
+ return output;
97
+ }
98
+
99
+ export function inferRegisterRowStatus(row: RegisterDataRow): RowStatus {
100
+ const explicit = row.__status;
101
+ if (explicit === 'active' || explicit === 'saved' || explicit === 'posted' || explicit === 'marked') return explicit;
102
+
103
+ if (row.isMarkedForDeletion === true || row.isDeleted === true) return 'marked';
104
+
105
+ const status = row.status;
106
+ if (typeof status === 'number') {
107
+ if (status === 2) return 'posted';
108
+ if (status === 3) return 'marked';
109
+ return 'saved';
110
+ }
111
+
112
+ if (typeof row.isActive === 'boolean') return row.isActive ? 'active' : 'saved';
113
+
114
+ return 'active';
115
+ }
116
+
117
+ export function useRegisterRows(args: UseRegisterRowsArgs) {
118
+ const hasGroups = computed(() => args.groupBy.value.length > 0);
119
+ const expandedGroups = ref<Set<string>>(new Set());
120
+
121
+ const sortedRows = computed(() => {
122
+ const rows = args.rows.value.map((row) => ({ ...row }));
123
+ if (!args.sortBy.value.length) return rows;
124
+ return rows.sort((left, right) => compareRows(left, right, args.sortBy.value));
125
+ });
126
+
127
+ const allGroupIds = computed(() => {
128
+ if (!hasGroups.value) return [];
129
+ return Array.from(new Set(collectGroupIds(sortedRows.value, args.groupBy.value)));
130
+ });
131
+
132
+ const allGroupsExpanded = computed(() => {
133
+ const ids = allGroupIds.value;
134
+ if (!ids.length) return false;
135
+ for (const id of ids) {
136
+ if (!expandedGroups.value.has(id)) return false;
137
+ }
138
+ return true;
139
+ });
140
+
141
+ function isGroupExpanded(id: string) {
142
+ return expandedGroups.value.has(id);
143
+ }
144
+
145
+ function toggleGroup(id: string) {
146
+ const next = new Set(expandedGroups.value);
147
+ if (next.has(id)) next.delete(id);
148
+ else next.add(id);
149
+ expandedGroups.value = next;
150
+ }
151
+
152
+ function toggleAllGroups() {
153
+ if (!hasGroups.value) return;
154
+ const ids = allGroupIds.value;
155
+ if (!ids.length) return;
156
+ expandedGroups.value = allGroupsExpanded.value ? new Set() : new Set(ids);
157
+ }
158
+
159
+ watch(
160
+ () => [args.defaultExpanded.value, allGroupIds.value.join('|')],
161
+ ([isDefaultExpanded]) => {
162
+ if (!isDefaultExpanded) return;
163
+ const ids = allGroupIds.value;
164
+ if (!ids.length) return;
165
+ expandedGroups.value = new Set(ids);
166
+ },
167
+ { immediate: true },
168
+ );
169
+
170
+ const displayRows = computed<DisplayRow[]>(() => {
171
+ if (!args.groupBy.value.length) {
172
+ return sortedRows.value.map((row, index) => ({ ...row, type: 'row', __index: index }));
173
+ }
174
+
175
+ const grouped = buildGroups(sortedRows.value, args.groupBy.value, expandedGroups.value);
176
+ let index = 0;
177
+
178
+ return grouped.map((row) => {
179
+ if (isDisplayDataRow(row)) return { ...row, __index: index++ };
180
+ return row;
181
+ });
182
+ });
183
+
184
+ const totals = computed(() => {
185
+ return args.rows.value.reduce(
186
+ (accumulator, row) => {
187
+ accumulator.debit += Number(row.debit ?? 0);
188
+ accumulator.credit += Number(row.credit ?? 0);
189
+ return accumulator;
190
+ },
191
+ { debit: 0, credit: 0 },
192
+ );
193
+ });
194
+
195
+ const dataRows = computed(() => displayRows.value.filter(isDisplayDataRow));
196
+
197
+ return {
198
+ hasGroups,
199
+ displayRows,
200
+ dataRows,
201
+ totals,
202
+ allGroupIds,
203
+ allGroupsExpanded,
204
+ isGroupExpanded,
205
+ toggleGroup,
206
+ toggleAllGroups,
207
+ inferRowStatus: inferRegisterRowStatus,
208
+ };
209
+ }
@@ -0,0 +1,56 @@
1
+ import { computed, onBeforeUnmount, onMounted, ref, type ComputedRef, type Ref } from 'vue';
2
+ import type { DisplayRow } from './registerTypes';
3
+
4
+ type UseRegisterViewportArgs = {
5
+ viewport: Ref<HTMLElement | null>;
6
+ heightPx: ComputedRef<number>;
7
+ rowHeight: ComputedRef<number>;
8
+ displayRows: ComputedRef<DisplayRow[]>;
9
+ overscan?: number;
10
+ };
11
+
12
+ export function useRegisterViewport(args: UseRegisterViewportArgs) {
13
+ const overscan = args.overscan ?? 8;
14
+ const scrollTop = ref(0);
15
+ const viewportHeight = ref(args.heightPx.value);
16
+
17
+ let resizeObserver: ResizeObserver | null = null;
18
+
19
+ onMounted(() => {
20
+ viewportHeight.value = args.viewport.value?.clientHeight ?? args.heightPx.value;
21
+
22
+ if (typeof ResizeObserver !== 'undefined' && args.viewport.value) {
23
+ resizeObserver = new ResizeObserver(() => {
24
+ viewportHeight.value = args.viewport.value?.clientHeight ?? args.heightPx.value;
25
+ });
26
+ resizeObserver.observe(args.viewport.value);
27
+ }
28
+ });
29
+
30
+ onBeforeUnmount(() => {
31
+ if (resizeObserver) {
32
+ resizeObserver.disconnect();
33
+ resizeObserver = null;
34
+ }
35
+ });
36
+
37
+ function onScroll() {
38
+ scrollTop.value = args.viewport.value?.scrollTop ?? 0;
39
+ }
40
+
41
+ const totalHeight = computed(() => args.displayRows.value.length * args.rowHeight.value);
42
+ const startIndex = computed(() => Math.max(0, Math.floor(scrollTop.value / args.rowHeight.value) - overscan));
43
+ const endIndex = computed(() => {
44
+ const visibleCount = Math.ceil(viewportHeight.value / args.rowHeight.value) + overscan * 2;
45
+ return Math.min(args.displayRows.value.length, startIndex.value + visibleCount);
46
+ });
47
+ const offsetTop = computed(() => startIndex.value * args.rowHeight.value);
48
+ const visibleRows = computed(() => args.displayRows.value.slice(startIndex.value, endIndex.value));
49
+
50
+ return {
51
+ onScroll,
52
+ totalHeight,
53
+ offsetTop,
54
+ visibleRows,
55
+ };
56
+ }