tailjng 0.1.6 → 0.1.7

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 (176) hide show
  1. package/README.md +12 -4
  2. package/cli/execute/init-app.js +5 -2
  3. package/cli/execute/sync-app.js +14 -2
  4. package/cli/settings/colors-config-utils.js +43 -8
  5. package/cli/settings/icons-config-utils.js +62 -0
  6. package/cli/settings/path-utils.js +32 -2
  7. package/cli/settings/project-utils.js +7 -1
  8. package/cli/templates/app.generator.js +2 -2
  9. package/fesm2022/tailjng.mjs +247 -80
  10. package/fesm2022/tailjng.mjs.map +1 -1
  11. package/lib/services/static/theme.service.d.ts +39 -1
  12. package/lib/utils/theme/theme-variables.util.d.ts +31 -0
  13. package/package.json +1 -1
  14. package/public-api.d.ts +2 -1
  15. package/registry/components.json +41 -18
  16. package/src/colors.safelist.css +2 -2
  17. package/src/lib/components/.config/README.md +11 -0
  18. package/src/lib/components/.config/colors/README.md +38 -0
  19. package/src/lib/components/{colors-config → .config/colors}/colors.config.ts +5 -5
  20. package/src/lib/components/{colors-config → .config/colors}/colors.safelist.css +2 -2
  21. package/src/lib/components/.config/icons/README.md +26 -0
  22. package/src/lib/components/.config/icons/icons.lucide.ts +134 -0
  23. package/src/lib/components/.config/input/README.md +24 -0
  24. package/src/lib/components/.config/input/input.classes.ts +119 -0
  25. package/src/lib/components/alert/alert-dialog/dialog-alert.component.css +244 -2
  26. package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +25 -38
  27. package/src/lib/components/alert/alert-dialog/dialog-alert.component.ts +66 -56
  28. package/src/lib/components/alert/alert-dialog/dialog-alert.types.ts +19 -0
  29. package/src/lib/components/alert/alert-toast/toast-alert.component.css +630 -12
  30. package/src/lib/components/alert/alert-toast/toast-alert.component.html +103 -102
  31. package/src/lib/components/alert/alert-toast/toast-alert.component.ts +485 -128
  32. package/src/lib/components/alert/alert-toast/toast-alert.types.ts +25 -0
  33. package/src/lib/components/badge/badge.component.html +34 -21
  34. package/src/lib/components/badge/badge.component.ts +140 -31
  35. package/src/lib/components/button/button.component.html +16 -10
  36. package/src/lib/components/button/button.component.ts +162 -22
  37. package/src/lib/components/card/card-complete/complete-card.component.html +2 -2
  38. package/src/lib/components/card/card-complete/complete-card.component.ts +26 -16
  39. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +2 -2
  40. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +26 -16
  41. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.css +97 -0
  42. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.html +54 -46
  43. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.ts +135 -64
  44. package/src/lib/components/checkbox/checkbox-input/input-checkbox.types.ts +3 -0
  45. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.css +112 -0
  46. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +28 -25
  47. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.ts +67 -15
  48. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.types.ts +1 -0
  49. package/src/lib/components/coach-mark/coach-mark.component.html +4 -22
  50. package/src/lib/components/coach-mark/coach-mark.component.scss +1 -1
  51. package/src/lib/components/coach-mark/coach-mark.component.ts +51 -18
  52. package/src/lib/components/coach-mark/coach-mark.directive.ts +133 -78
  53. package/src/lib/components/coach-mark/coach-mark.types.ts +12 -0
  54. package/src/lib/components/dialog/dialog.component.css +103 -1
  55. package/src/lib/components/dialog/dialog.component.html +46 -66
  56. package/src/lib/components/dialog/dialog.component.ts +136 -110
  57. package/src/lib/components/dialog/dialog.types.ts +19 -0
  58. package/src/lib/components/filter/filter-complete/complete-filter.component.html +16 -19
  59. package/src/lib/components/filter/filter-complete/complete-filter.component.scss +35 -0
  60. package/src/lib/components/filter/filter-complete/complete-filter.component.ts +58 -34
  61. package/src/lib/components/filter/filter-complete/complete-filter.types.ts +7 -0
  62. package/src/lib/components/filter/filter-complete/complete-filter.util.ts +16 -0
  63. package/src/lib/components/form/form-container/container-form.component.css +4 -0
  64. package/src/lib/components/form/form-container/container-form.component.html +2 -2
  65. package/src/lib/components/form/form-container/container-form.component.ts +72 -16
  66. package/src/lib/components/form/form-container/container-form.types.ts +42 -0
  67. package/src/lib/components/form/form-container/form-col-span.directive.ts +25 -0
  68. package/src/lib/components/form/form-sidebar/sidebar-form.component.css +276 -0
  69. package/src/lib/components/form/form-sidebar/sidebar-form.component.html +117 -125
  70. package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +109 -34
  71. package/src/lib/components/form/form-sidebar/sidebar-form.types.ts +3 -0
  72. package/src/lib/components/{toggle-radio/toggle-radio.component.css → form/form-validation/validation-form.component.css} +0 -1
  73. package/src/lib/components/form/form-validation/validation-form.component.html +10 -6
  74. package/src/lib/components/form/form-validation/validation-form.component.ts +99 -12
  75. package/src/lib/components/form/form-validation/validation-form.types.ts +33 -0
  76. package/src/lib/components/icon/icon.component.html +8 -5
  77. package/src/lib/components/icon/icon.component.ts +111 -9
  78. package/src/lib/components/input/input/input.component.html +19 -16
  79. package/src/lib/components/input/input/input.component.ts +130 -53
  80. package/src/lib/components/input/input/input.types.ts +8 -0
  81. package/src/lib/components/input/input-file/file-input.component.html +65 -56
  82. package/src/lib/components/input/input-file/file-input.component.ts +276 -173
  83. package/src/lib/components/input/input-file/file-input.types.ts +2 -0
  84. package/src/lib/components/input/input-range/range-input.component.css +67 -0
  85. package/src/lib/components/input/input-range/range-input.component.html +50 -58
  86. package/src/lib/components/input/input-range/range-input.component.ts +148 -60
  87. package/src/lib/components/input/input-range/range-input.types.ts +7 -0
  88. package/src/lib/components/input/input-textarea/textarea-input.component.html +16 -7
  89. package/src/lib/components/input/input-textarea/textarea-input.component.ts +140 -50
  90. package/src/lib/components/input/input-textarea/textarea-input.types.ts +2 -0
  91. package/src/lib/components/label/label.component.html +17 -16
  92. package/src/lib/components/label/label.component.ts +70 -16
  93. package/src/lib/components/label/label.types.ts +2 -0
  94. package/src/lib/components/menu/menu-options-table/menu-options-defaults.ts +34 -0
  95. package/src/lib/components/menu/menu-options-table/options-table-menu.component.html +34 -20
  96. package/src/lib/components/menu/menu-options-table/options-table-menu.component.ts +211 -58
  97. package/src/lib/components/menu/menu-options-table/options-table-menu.types.ts +38 -0
  98. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +49 -52
  99. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +112 -24
  100. package/src/lib/components/menu/options-coach-menu/options-coach-menu.types.ts +9 -0
  101. package/src/lib/components/mode-toggle/mode-toggle.component.html +11 -16
  102. package/src/lib/components/mode-toggle/mode-toggle.component.ts +69 -33
  103. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.html +4 -4
  104. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.ts +31 -7
  105. package/src/lib/components/paginator/paginator-complete/complete-paginator.types.ts +12 -0
  106. package/src/lib/components/paginator/paginator-complete/complete-paginator.util.ts +36 -0
  107. package/src/lib/components/progress-bar/progress-bar.component.css +11 -0
  108. package/src/lib/components/progress-bar/progress-bar.component.html +41 -40
  109. package/src/lib/components/progress-bar/progress-bar.component.ts +95 -11
  110. package/src/lib/components/progress-bar/progress-bar.types.ts +2 -0
  111. package/src/lib/components/select/select-dropdown/dropdown-select.component.css +6 -0
  112. package/src/lib/components/select/select-dropdown/dropdown-select.component.html +54 -44
  113. package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +450 -509
  114. package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +43 -0
  115. package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +179 -0
  116. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.css +6 -0
  117. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +131 -42
  118. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +491 -475
  119. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.types.ts +22 -0
  120. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +20 -0
  121. package/src/lib/components/select/select-multi-table/multi-table-select.component.css +10 -0
  122. package/src/lib/components/select/select-multi-table/multi-table-select.component.html +76 -60
  123. package/src/lib/components/select/select-multi-table/multi-table-select.component.ts +250 -313
  124. package/src/lib/components/select/select-multi-table/multi-table-select.types.ts +10 -0
  125. package/src/lib/components/select/select-multi-table/multi-table-select.util.ts +5 -0
  126. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.css +212 -0
  127. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.html +62 -53
  128. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.ts +84 -27
  129. package/src/lib/components/sidebar/sidebar-static/static-sidebar.types.ts +2 -0
  130. package/src/lib/components/table/table-complete/complete-table.component.html +15 -17
  131. package/src/lib/components/table/table-complete/complete-table.component.ts +190 -338
  132. package/src/lib/components/table/table-complete/complete-table.types.ts +28 -0
  133. package/src/lib/components/table/table-complete/complete-table.util.ts +236 -0
  134. package/src/lib/components/table/table-complete/index.ts +2 -0
  135. package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +34 -0
  136. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +73 -128
  137. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +542 -829
  138. package/src/lib/components/table/table-crud-complete/complete-crud-table.types.ts +57 -0
  139. package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +723 -0
  140. package/src/lib/components/table/table-crud-complete/index.ts +3 -0
  141. package/src/lib/components/theme-generator/theme-generator.component.css +21 -0
  142. package/src/lib/components/theme-generator/theme-generator.component.html +146 -116
  143. package/src/lib/components/theme-generator/theme-generator.component.ts +44 -24
  144. package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +8 -0
  145. package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +44 -0
  146. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.css +135 -0
  147. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.html +52 -0
  148. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +198 -0
  149. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.types.ts +1 -0
  150. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.css +108 -0
  151. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.html +37 -0
  152. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +193 -0
  153. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +1 -0
  154. package/src/lib/components/tooltip/tooltip.directive.ts +12 -9
  155. package/src/lib/components/tooltip/tooltip.service.ts +331 -133
  156. package/src/lib/components/tooltip/tooltip.types.ts +9 -0
  157. package/src/lib/components/viewer/viewer-image/image-viewer.component.css +90 -4
  158. package/src/lib/components/viewer/viewer-image/image-viewer.component.html +52 -103
  159. package/src/lib/components/viewer/viewer-image/image-viewer.component.ts +182 -177
  160. package/src/lib/components/viewer/viewer-image/image-viewer.types.ts +3 -0
  161. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.css +177 -0
  162. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.html +74 -24
  163. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.ts +168 -15
  164. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.types.ts +1 -0
  165. package/src/styles.css +2 -2
  166. package/lib/services/static/icons.service.d.ts +0 -65
  167. package/src/lib/components/colors-config/README.md +0 -38
  168. package/src/lib/components/form/form-sidebar/sidebar-form.component.scss +0 -0
  169. package/src/lib/components/form/form-validation/validation-form.component.scss +0 -0
  170. package/src/lib/components/menu/menu-options-table/options-table-menu.component.scss +0 -0
  171. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +0 -12
  172. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.scss +0 -0
  173. package/src/lib/components/toggle-radio/toggle-radio.component.html +0 -51
  174. package/src/lib/components/toggle-radio/toggle-radio.component.ts +0 -222
  175. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.scss +0 -0
  176. package/tailjng-0.1.6.tgz +0 -0
@@ -0,0 +1,723 @@
1
+ import type { Params } from '@angular/router';
2
+ import type {
3
+ FilterSelect,
4
+ JConverterCrudService,
5
+ JGenericCrudService,
6
+ OptionsTable,
7
+ SortDirection,
8
+ TableColumn,
9
+ } from 'tailjng';
10
+ import { generatePaginationPages } from '../../paginator/paginator-complete/complete-paginator.util';
11
+ import type {
12
+ BuildCrudTableQueryParamsOptions,
13
+ CrudTableCheckboxToggleTarget,
14
+ CrudTableEquivalenceConfigs,
15
+ CrudTableEquivalenceData,
16
+ CrudTableEquivalenceMaps,
17
+ CrudTableGetCellValueFn,
18
+ CrudTableGroupTableConfig,
19
+ CrudTableRowColorSemantic,
20
+ } from './complete-crud-table.types';
21
+
22
+ // ── Row colors ───────────────────────────────────────────────────────────────
23
+
24
+ export const CRUD_TABLE_ROW_COLOR_CLASS_MAP: Record<CrudTableRowColorSemantic, string> = {
25
+ primary: 'bg-primary/20 hover:bg-dark-primary/30! dark:hover:bg-dark-primary/20',
26
+ success: 'bg-green-500/20 hover:bg-green-500/30! dark:hover:bg-green-500/20',
27
+ error: ' bg-red-500/20 hover:bg-red-500/30! dark:hover:bg-red-500/20',
28
+ warning: 'bg-yellow-500/20 hover:bg-yellow-500/30! dark:hover:bg-yellow-500/20',
29
+ info: ' bg-blue-500/20 hover:bg-blue-500/30! dark:hover:bg-blue-500/20',
30
+ };
31
+
32
+ // ── Columns ──────────────────────────────────────────────────────────────────
33
+
34
+ /** Applies default `visible`, `sortable`, and `isSearchable` on column definitions. */
35
+ export function applyColumnDefaults(columns: TableColumn<any>[]): void {
36
+ columns.forEach((column) => {
37
+ if (column.visible === undefined) {
38
+ column.visible = true;
39
+ }
40
+
41
+ if (column.sortable === undefined) {
42
+ column.sortable = true;
43
+ }
44
+
45
+ if (column.isSearchable === undefined) {
46
+ column.isSearchable = true;
47
+ }
48
+ });
49
+ }
50
+
51
+ /** Counts columns with `visible !== false`. */
52
+ export function countVisibleColumns(columns: TableColumn<any>[]): number {
53
+ return columns.filter((column) => column.visible).length;
54
+ }
55
+
56
+ /**
57
+ * Styles for `<col>` in `table-layout: fixed` — copies `minWidth` to `width` when needed.
58
+ */
59
+ export function getColumnColStyles(column: TableColumn<any>): Record<string, string> | undefined {
60
+ if (!column.styles) {
61
+ return undefined;
62
+ }
63
+
64
+ const styles = { ...column.styles };
65
+
66
+ if (styles['minWidth'] && !styles['width']) {
67
+ styles['width'] = styles['minWidth'];
68
+ }
69
+
70
+ return styles;
71
+ }
72
+
73
+ export function getGroupExpandColspan(
74
+ columns: TableColumn<any>[],
75
+ isNumbering: boolean,
76
+ isOptions: boolean,
77
+ ): number {
78
+ let colspan = countVisibleColumns(columns);
79
+
80
+ if (isNumbering) {
81
+ colspan += 1;
82
+ }
83
+
84
+ if (isOptions) {
85
+ colspan += 1;
86
+ }
87
+
88
+ return colspan;
89
+ }
90
+
91
+ export function getGroupCollapsedHeaderColspan(
92
+ columns: TableColumn<any>[],
93
+ isOptions: boolean,
94
+ ): number {
95
+ return 1 + countVisibleColumns(columns) + (isOptions ? 1 : 0);
96
+ }
97
+
98
+ // ── Pagination ───────────────────────────────────────────────────────────────
99
+
100
+ export function calculateTotalPages(totalItems: number, itemsPerPage: number): number {
101
+ return Math.ceil(totalItems / itemsPerPage);
102
+ }
103
+
104
+ export function calculateStartIndex(currentPage: number, itemsPerPage: number): number {
105
+ return (currentPage - 1) * itemsPerPage;
106
+ }
107
+
108
+ /** Re-exported sliding window used by the embedded paginator. */
109
+ export { generatePaginationPages };
110
+
111
+ // ── Query params ─────────────────────────────────────────────────────────────
112
+
113
+ export function collectSearchFieldKeys(columns: TableColumn<any>[]): string[] {
114
+ const baseSearchKeys = columns.filter((column) => column.isSearchable).map((column) => column.key);
115
+ const extraSearchKeys = columns.flatMap((column) =>
116
+ (column.extraSearchFields || []).map((field) => String(field)),
117
+ );
118
+ return [...baseSearchKeys, ...extraSearchKeys];
119
+ }
120
+
121
+ /** Builds list query params via {@link JGenericCrudService.params}. */
122
+ export function buildCrudTableQueryParams(
123
+ genericService: JGenericCrudService,
124
+ options: BuildCrudTableQueryParamsOptions,
125
+ ): Params {
126
+ const params: Params = genericService.params({
127
+ page: options.currentPage,
128
+ limit: options.itemsPerPage,
129
+ sort: {
130
+ column: options.sortColumn,
131
+ direction: options.sortDirection,
132
+ },
133
+ filters: options.filters,
134
+ defaultFilters: options.defaultFilters,
135
+ });
136
+
137
+ if (options.searchQuery?.trim()) {
138
+ params['search'] = options.searchQuery;
139
+ params['searchFields'] = collectSearchFieldKeys(options.columns);
140
+ }
141
+
142
+ if (options.isGroupTable && typeof options.groupTable['getGroupParams'] === 'function') {
143
+ Object.assign(params, options.groupTable['getGroupParams']());
144
+ }
145
+
146
+ return params;
147
+ }
148
+
149
+ // ── Sorting ──────────────────────────────────────────────────────────────────
150
+
151
+ /** Computes next sort column/direction after a header click. */
152
+ export function resolveNextSortState(
153
+ column: TableColumn<any>,
154
+ currentSortColumn: string | null,
155
+ currentSortDirection: SortDirection,
156
+ getSortKey: (value: unknown) => string,
157
+ ): { sortColumn: string | null; sortDirection: SortDirection } {
158
+ const currentSortKey = getSortKey(currentSortColumn);
159
+ const columnSortKey = getSortKey(column.key);
160
+
161
+ if (currentSortKey === columnSortKey) {
162
+ let sortDirection = currentSortDirection;
163
+
164
+ if (sortDirection === 'asc') {
165
+ sortDirection = 'desc';
166
+ } else if (sortDirection === 'desc') {
167
+ sortDirection = 'none';
168
+ } else {
169
+ sortDirection = 'asc';
170
+ }
171
+
172
+ return { sortColumn: column.key, sortDirection };
173
+ }
174
+
175
+ return { sortColumn: column.key, sortDirection: 'asc' };
176
+ }
177
+
178
+ // ── Expand rows ──────────────────────────────────────────────────────────────
179
+
180
+ export function getExpandedRowCacheKey(item: Record<string, unknown>, isGroupTable: boolean): string {
181
+ if (isGroupTable) {
182
+ return `${item['title']}-${item['description']}`;
183
+ }
184
+
185
+ const id = item['id'];
186
+ return (id || JSON.stringify(item)) as string;
187
+ }
188
+
189
+ /** Rebuilds `expandedRows` after a data refresh using cached keys. */
190
+ export function restoreExpandedRowsAfterLoad(
191
+ data: Record<string, unknown>[],
192
+ expandedKeys: Set<string>,
193
+ isGroupTable: boolean,
194
+ ): Set<Record<string, unknown>> {
195
+ const restored = new Set<Record<string, unknown>>();
196
+
197
+ if (expandedKeys.size === 0) {
198
+ return restored;
199
+ }
200
+
201
+ data.forEach((item) => {
202
+ if (expandedKeys.has(getExpandedRowCacheKey(item, isGroupTable))) {
203
+ restored.add(item);
204
+ }
205
+ });
206
+
207
+ return restored;
208
+ }
209
+
210
+ export function cacheExpandedRowKeys(
211
+ expandedRows: Set<Record<string, unknown>>,
212
+ isGroupTable: boolean,
213
+ ): Set<string> {
214
+ const keys = new Set<string>();
215
+ expandedRows.forEach((item) => {
216
+ keys.add(getExpandedRowCacheKey(item, isGroupTable));
217
+ });
218
+ return keys;
219
+ }
220
+
221
+ // ── Row styling ──────────────────────────────────────────────────────────────
222
+
223
+ export function buildRowNgClass(
224
+ row: Record<string, unknown>,
225
+ columns: TableColumn<any>[],
226
+ hasExpandableRows: boolean,
227
+ ): Record<string, boolean> {
228
+ const base: Record<string, boolean> = {
229
+ 'cursor-pointer': hasExpandableRows,
230
+ };
231
+
232
+ const columnWithColor = columns.find((column) => !!column.rowColorCondition);
233
+ if (!columnWithColor) {
234
+ return base;
235
+ }
236
+
237
+ const keys = columnWithColor.key.split('.');
238
+ let value: unknown = row;
239
+
240
+ for (const key of keys) {
241
+ value = (value as Record<string, unknown> | undefined)?.[key];
242
+ }
243
+
244
+ const normalizedValue =
245
+ value === null || value === undefined ? 'null' : String(value).toUpperCase();
246
+ const semanticClass = columnWithColor.rowColorCondition?.[normalizedValue] as
247
+ | CrudTableRowColorSemantic
248
+ | undefined;
249
+
250
+ if (semanticClass && CRUD_TABLE_ROW_COLOR_CLASS_MAP[semanticClass]) {
251
+ base[CRUD_TABLE_ROW_COLOR_CLASS_MAP[semanticClass]] = true;
252
+ }
253
+
254
+ return base;
255
+ }
256
+
257
+ // ── Cell / value helpers ─────────────────────────────────────────────────────
258
+
259
+ export function isBooleanValue(value: unknown): boolean {
260
+ return typeof value === 'boolean';
261
+ }
262
+
263
+ export function isDateValue(value: unknown): boolean {
264
+ return value instanceof Date || (typeof value === 'string' && !Number.isNaN(Date.parse(value)));
265
+ }
266
+
267
+ /** Resolves nested cell value before converter formatting. */
268
+ export function resolveRawCellValue(item: Record<string, unknown>, column: TableColumn<any>): unknown {
269
+ if (typeof column.valueGetter === 'function') {
270
+ return column.valueGetter(item);
271
+ }
272
+
273
+ const keys = column.key.split('.');
274
+ let value: unknown = item;
275
+
276
+ for (const key of keys) {
277
+ if (value != null) {
278
+ value = (value as Record<string, unknown>)[key];
279
+ } else {
280
+ value = null;
281
+ break;
282
+ }
283
+ }
284
+
285
+ return value;
286
+ }
287
+
288
+ /** Formats a cell for display (`S/N` when empty). */
289
+ export function formatCellDisplayValue(
290
+ item: Record<string, unknown>,
291
+ column: TableColumn<any>,
292
+ converterService: JConverterCrudService,
293
+ ): unknown {
294
+ const value = resolveRawCellValue(item, column);
295
+
296
+ if (value === null || value === undefined) {
297
+ return 'S/N';
298
+ }
299
+
300
+ return converterService.formatData(value, column) ?? value;
301
+ }
302
+
303
+ /** Parses a cell value for inline editing. */
304
+ export function parseCellEditValue(
305
+ item: Record<string, unknown>,
306
+ column: TableColumn<any>,
307
+ converterService: JConverterCrudService,
308
+ ): unknown {
309
+ const value = resolveRawCellValue(item, column);
310
+ return converterService.parseData(value, column);
311
+ }
312
+
313
+ export function shouldStopCellClickPropagation(column: TableColumn<any>): boolean {
314
+ return column.stopRowClickOnCellClick !== false;
315
+ }
316
+
317
+ // ── Checkbox toggle ──────────────────────────────────────────────────────────
318
+
319
+ export function resolveCheckboxToggleTarget(
320
+ item: Record<string, unknown>,
321
+ column: TableColumn<any>,
322
+ mainEndpoint: string,
323
+ ): CrudTableCheckboxToggleTarget {
324
+ let key = column.key;
325
+ let recordId: any;
326
+ let endpoint = mainEndpoint;
327
+
328
+ if (key.includes('.')) {
329
+ const parts = key.split('.');
330
+ const parent = parts[0];
331
+ key = parts[parts.length - 1];
332
+ recordId = item[`id_${parent}`];
333
+ endpoint = parent;
334
+ } else {
335
+ recordId = item[`id_${mainEndpoint}`];
336
+ }
337
+
338
+ return { key, recordId, endpoint };
339
+ }
340
+
341
+ export function applyNestedBooleanToggle(
342
+ item: Record<string, unknown>,
343
+ columnKey: string,
344
+ currentValue: boolean,
345
+ ): void {
346
+ if (columnKey.includes('.')) {
347
+ const parts = columnKey.split('.');
348
+ const parent = parts[0];
349
+ const lastKey = parts[parts.length - 1];
350
+ item[parent] = { ...(item[parent] as Record<string, unknown>), [lastKey]: !currentValue };
351
+ return;
352
+ }
353
+
354
+ item[columnKey] = !currentValue;
355
+ }
356
+
357
+ // ── Table options ────────────────────────────────────────────────────────────
358
+
359
+ export function resolveOptionTooltip(
360
+ tooltip: string | ((data?: unknown) => string) | undefined,
361
+ data: unknown,
362
+ ): string {
363
+ if (typeof tooltip === 'function') {
364
+ return tooltip(data);
365
+ }
366
+
367
+ return tooltip ?? '';
368
+ }
369
+
370
+ export function resolveOptionIcon(
371
+ icon: unknown | ((data?: unknown) => unknown),
372
+ data: unknown,
373
+ ): unknown {
374
+ if (typeof icon === 'function') {
375
+ return icon(data);
376
+ }
377
+
378
+ return icon;
379
+ }
380
+
381
+ export function resolveOptionDisabled(option: OptionsTable, data: unknown): boolean {
382
+ if (typeof option.disabled === 'function') {
383
+ return option.disabled(data);
384
+ }
385
+
386
+ return !!option.disabled;
387
+ }
388
+
389
+ export function resolveOptionVisible(option: OptionsTable, data: unknown): boolean {
390
+ if (typeof option.isVisible === 'function') {
391
+ return option.isVisible(data);
392
+ }
393
+
394
+ return option.isVisible !== false;
395
+ }
396
+
397
+ export function mergeOptionNgClasses(
398
+ optionNgClass: ((data?: unknown) => unknown) | undefined,
399
+ data: unknown,
400
+ ): Record<string, boolean> {
401
+ const baseClass: Record<string, boolean> = {
402
+ 'min-w-auto p-[1px]! pl-[5px]! pr-[5px]!': true,
403
+ };
404
+
405
+ let dynamicClass: unknown = {};
406
+
407
+ if (typeof optionNgClass === 'function') {
408
+ dynamicClass = optionNgClass(data);
409
+ } else {
410
+ dynamicClass = optionNgClass ?? {};
411
+ }
412
+
413
+ if (typeof dynamicClass === 'string') {
414
+ return { ...baseClass, [dynamicClass]: true };
415
+ }
416
+
417
+ return { ...baseClass, ...(dynamicClass as Record<string, boolean>) };
418
+ }
419
+
420
+ // ── Inline editing ───────────────────────────────────────────────────────────
421
+
422
+ export function deepCopy<T>(obj: T): T {
423
+ if (obj === null || typeof obj !== 'object') {
424
+ return obj;
425
+ }
426
+
427
+ if (obj instanceof Date) {
428
+ return new Date(obj.getTime()) as T;
429
+ }
430
+
431
+ if (Array.isArray(obj)) {
432
+ return obj.map((item) => deepCopy(item)) as T;
433
+ }
434
+
435
+ const copy: Record<string, unknown> = {};
436
+
437
+ for (const key in obj as Record<string, unknown>) {
438
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
439
+ copy[key] = deepCopy((obj as Record<string, unknown>)[key]);
440
+ }
441
+ }
442
+
443
+ return copy as T;
444
+ }
445
+
446
+ export function setNestedValue(
447
+ item: Record<string, unknown>,
448
+ columnKey: string,
449
+ value: unknown,
450
+ ): void {
451
+ const keys = columnKey.split('.');
452
+ let current: Record<string, unknown> = item;
453
+
454
+ for (let index = 0; index < keys.length; index++) {
455
+ const key = keys[index];
456
+
457
+ if (index === keys.length - 1) {
458
+ current[key] = value;
459
+ } else {
460
+ if (!current[key]) {
461
+ current[key] = {};
462
+ }
463
+ current = current[key] as Record<string, unknown>;
464
+ }
465
+ }
466
+ }
467
+
468
+ export function hasEquivalenceConfig(
469
+ equivalenceConfigs: CrudTableEquivalenceConfigs | undefined,
470
+ columnKey: string,
471
+ ): boolean {
472
+ for (const [, config] of Object.entries(equivalenceConfigs || {})) {
473
+ if (config?.keyColumnSearch === columnKey) {
474
+ return true;
475
+ }
476
+ }
477
+
478
+ return false;
479
+ }
480
+
481
+ export function getEquivalenceData(
482
+ equivalenceConfigs: CrudTableEquivalenceConfigs | undefined,
483
+ equivalences: CrudTableEquivalenceMaps | undefined,
484
+ columnKey: string,
485
+ ): CrudTableEquivalenceData | null {
486
+ for (const [, config] of Object.entries(equivalenceConfigs || {})) {
487
+ if (!config || config.keyColumnSearch !== columnKey) {
488
+ continue;
489
+ }
490
+
491
+ const keyReturn = config.keyReturn;
492
+ const idMap = equivalences?.[keyReturn];
493
+
494
+ if (!idMap) {
495
+ return null;
496
+ }
497
+
498
+ const options = Object.entries(idMap).map(([id, label]) => ({
499
+ key: +id,
500
+ label: String(label),
501
+ }));
502
+
503
+ return {
504
+ keyReturn,
505
+ options,
506
+ optional: config.optional || false,
507
+ };
508
+ }
509
+
510
+ return null;
511
+ }
512
+
513
+ // ── Filters ──────────────────────────────────────────────────────────────────
514
+
515
+ /** Wraps filter `onSelected` handlers so table filters reload automatically. */
516
+ export function bindFilterSelectHandlers(
517
+ filtersSelect: FilterSelect[],
518
+ filters: Record<string, unknown>,
519
+ onFilterApplied: () => void,
520
+ ): void {
521
+ for (const filter of filtersSelect) {
522
+ if (filter.type !== 'dropdown' && filter.type !== 'searchable') {
523
+ continue;
524
+ }
525
+
526
+ const key = filter.optionValue ?? 'value';
527
+ const deepKey = filter.deep ? `${filter.deep}.${key}` : key;
528
+ const originalOnSelected = filter.onSelected;
529
+
530
+ filter.onSelected = (value: unknown) => {
531
+ const record = value as Record<string, unknown> | null | undefined;
532
+ const selectedValue = record?.[key] ?? value;
533
+
534
+ if (selectedValue === null || selectedValue === undefined) {
535
+ delete filters[deepKey];
536
+ } else {
537
+ filters[deepKey] = selectedValue;
538
+ }
539
+
540
+ if (typeof originalOnSelected === 'function') {
541
+ originalOnSelected(value);
542
+ }
543
+
544
+ onFilterApplied();
545
+ };
546
+ }
547
+ }
548
+
549
+ /** Syncs active/inactive toggle into matching filter select defaults. */
550
+ export function syncCheckedFilterSelects(
551
+ filtersSelect: FilterSelect[],
552
+ checkedColumn: string,
553
+ checkedFilterValue: unknown,
554
+ ): FilterSelect[] {
555
+ return filtersSelect.map((filter) => {
556
+ if ('optionValue' in filter && filter.optionValue === checkedColumn) {
557
+ return {
558
+ ...filter,
559
+ defaultFilters: {
560
+ ...(Object.prototype.hasOwnProperty.call(filter, 'defaultFilters')
561
+ ? (filter as FilterSelect & { defaultFilters?: Record<string, unknown> }).defaultFilters
562
+ : {}),
563
+ [checkedColumn]: checkedFilterValue,
564
+ },
565
+ selected: null,
566
+ };
567
+ }
568
+
569
+ return filter;
570
+ });
571
+ }
572
+
573
+ // ── Group / mobile layout ────────────────────────────────────────────────────
574
+
575
+ export function trackGroupRowKey(group: Record<string, unknown>, index: number): string | number {
576
+ const studentCourseId = group['id_student_course'];
577
+ if (studentCourseId !== undefined && studentCourseId !== null) {
578
+ return studentCourseId as string | number;
579
+ }
580
+
581
+ const id = group['id'];
582
+ if (id !== undefined && id !== null) {
583
+ return id as string | number;
584
+ }
585
+
586
+ return index;
587
+ }
588
+
589
+ export function getMobileMainTitle(
590
+ item: Record<string, unknown>,
591
+ columns: TableColumn<any>[],
592
+ getValue: CrudTableGetCellValueFn,
593
+ ): string {
594
+ const firstTextColumn = columns.find(
595
+ (column) =>
596
+ column.visible &&
597
+ !column.isDecorator &&
598
+ !column.isDecoratorArray &&
599
+ !column.isLink &&
600
+ !column.isLinkImage &&
601
+ !isBooleanValue(getValue(item, column)),
602
+ );
603
+
604
+ return firstTextColumn ? String(getValue(item, firstTextColumn)) : 'Registro';
605
+ }
606
+
607
+ export function getMobileMainDescription(
608
+ item: Record<string, unknown>,
609
+ columns: TableColumn<any>[],
610
+ getValue: CrudTableGetCellValueFn,
611
+ ): string | null {
612
+ const descriptionColumn = columns.find(
613
+ (column) => column.visible && (column as TableColumn<any> & { isDescription?: boolean }).isDescription,
614
+ );
615
+
616
+ if (!descriptionColumn) {
617
+ return null;
618
+ }
619
+
620
+ const value = getValue(item, descriptionColumn);
621
+
622
+ if (value === null || value === undefined || value === '' || value === 'S/N') {
623
+ return null;
624
+ }
625
+
626
+ return String(value);
627
+ }
628
+
629
+ export function getMobileHeaderColumns(columns: TableColumn<any>[]): TableColumn<any>[] {
630
+ return columns
631
+ .filter((column) => column.visible)
632
+ .filter((column) => column.isDecorator || column.colorDecorator)
633
+ .slice(0, 2);
634
+ }
635
+
636
+ export function getGroupMobileStudentTitle(
637
+ student: Record<string, unknown>,
638
+ columns: TableColumn<any>[],
639
+ getValue: CrudTableGetCellValueFn,
640
+ ): string {
641
+ const firstNameCol = columns.find(
642
+ (column) => column.visible && column.key === 'student.firstname_student',
643
+ );
644
+ const lastNameCol = columns.find(
645
+ (column) => column.visible && column.key === 'student.lastname_student',
646
+ );
647
+
648
+ const parts: string[] = [];
649
+
650
+ if (firstNameCol) {
651
+ const firstName = getValue(student, firstNameCol);
652
+ if (firstName) {
653
+ parts.push(String(firstName));
654
+ }
655
+ }
656
+
657
+ if (lastNameCol) {
658
+ const lastName = getValue(student, lastNameCol);
659
+ if (lastName) {
660
+ parts.push(String(lastName));
661
+ }
662
+ }
663
+
664
+ if (parts.length > 0) {
665
+ return parts.join(' ');
666
+ }
667
+
668
+ const fallbackColumn = columns.find(
669
+ (column) =>
670
+ column.visible &&
671
+ !column.isDecorator &&
672
+ !column.isDecoratorArray &&
673
+ !column.isLink &&
674
+ !column.isLinkImage &&
675
+ !isBooleanValue(getValue(student, column)),
676
+ );
677
+
678
+ return fallbackColumn ? String(getValue(student, fallbackColumn)) : 'Estudiante';
679
+ }
680
+
681
+ export function getGroupMobileStudentHeaderColumns(
682
+ student: Record<string, unknown>,
683
+ columns: TableColumn<any>[],
684
+ getValue: CrudTableGetCellValueFn,
685
+ ): TableColumn<any>[] {
686
+ return columns
687
+ .filter(
688
+ (column) =>
689
+ column.visible &&
690
+ (column.isDecorator || column.colorDecorator) &&
691
+ getValue(student, column),
692
+ )
693
+ .slice(0, 3);
694
+ }
695
+
696
+ export function getGroupMobileStudentIndex(
697
+ groupItem: Record<string, unknown>,
698
+ index: number,
699
+ ): number {
700
+ const meta = groupItem['meta'] as { page?: { currentPage?: number; limit?: number } } | undefined;
701
+ const page = meta?.page?.currentPage ?? 1;
702
+ const limit = meta?.page?.limit ?? 0;
703
+
704
+ return (page - 1) * limit + index + 1;
705
+ }
706
+
707
+ /** Applies group meta title from row title when present in API payload. */
708
+ export function normalizeGroupMetaTitles(data: Record<string, unknown>[]): void {
709
+ data.forEach((item) => {
710
+ const meta = item['meta'] as { page?: { title?: string } } | undefined;
711
+
712
+ if (meta?.page && item['title']) {
713
+ meta.page.title = String(item['title']);
714
+ }
715
+ });
716
+ }
717
+
718
+ /** Resolves main endpoint segment from full CRUD path. */
719
+ export function resolveMainEndpoint(endpoint: string): string {
720
+ return endpoint.split('/')[0] ?? endpoint;
721
+ }
722
+
723
+ export type CrudTableGroupTableConfigExport = CrudTableGroupTableConfig;