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
@@ -1,13 +1,26 @@
1
1
  import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import { FormsModule } from '@angular/forms';
4
- import { LucideAngularModule } from 'lucide-angular';
5
4
  import { Params } from '@angular/router';
6
- import { DomSanitizer, SafeUrl, SafeHtml } from '@angular/platform-browser';
7
- import { animate, state, style, transition, trigger } from '@angular/animations';
5
+ import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser';
8
6
  import { forkJoin } from 'rxjs';
9
7
 
10
- import { EnableBoolean, FilterButton, FilterSelect, JAlertToastService, JConverterCrudService, JDialogShared, JFormShared, JGenericCrudService, JIconsService, JTransformService, JUploadFilterService, LoadingState, LoadingStates, OptionsTable, SortDirection, TableColumn } from 'tailjng';
8
+ import {
9
+ EnableBoolean,
10
+ FilterButton,
11
+ FilterSelect,
12
+ JAlertToastService,
13
+ JConverterCrudService,
14
+ JDialogShared,
15
+ JFormShared,
16
+ JGenericCrudService,
17
+ JTransformService,
18
+ JUploadFilterService,
19
+ LoadingState,
20
+ LoadingStates,
21
+ OptionsTable,
22
+ SortDirection,
23
+ TableColumn} from 'tailjng';
11
24
 
12
25
  import { JCompletePaginatorComponent } from '../../paginator/paginator-complete/complete-paginator.component';
13
26
  import { JCompleteFilterComponent } from '../../filter/filter-complete/complete-filter.component';
@@ -19,68 +32,150 @@ import { JDropdownSelectComponent } from '../../select/select-dropdown/dropdown-
19
32
  import { JInputComponent } from '../../input/input/input.component';
20
33
  import { JImageViewerComponent } from '../../viewer/viewer-image/image-viewer.component';
21
34
  import { JOptionsCoachMenuComponent } from '../../menu/options-coach-menu/options-coach-menu.component';
22
-
35
+ import { CRUD_TABLE_ANIMATIONS } from './complete-crud-table.animations';
36
+ import type {
37
+ CrudTableEquivalenceConfigs,
38
+ CrudTableEquivalenceMaps,
39
+ CrudTableGroupTableConfig} from './complete-crud-table.types';
40
+ import { Icons } from '../../.config/icons/icons.lucide';
41
+ import { JIconComponent } from '../../icon/icon.component';
42
+ import {
43
+ applyColumnDefaults,
44
+ applyNestedBooleanToggle,
45
+ bindFilterSelectHandlers,
46
+ buildCrudTableQueryParams,
47
+ buildRowNgClass,
48
+ cacheExpandedRowKeys,
49
+ calculateStartIndex,
50
+ calculateTotalPages,
51
+ countVisibleColumns,
52
+ deepCopy,
53
+ formatCellDisplayValue,
54
+ generatePaginationPages,
55
+ getColumnColStyles,
56
+ getEquivalenceData as resolveEquivalenceData,
57
+ getGroupCollapsedHeaderColspan,
58
+ getGroupExpandColspan,
59
+ getGroupMobileStudentHeaderColumns,
60
+ getGroupMobileStudentIndex,
61
+ getGroupMobileStudentTitle,
62
+ getMobileHeaderColumns,
63
+ getMobileMainDescription,
64
+ getMobileMainTitle,
65
+ hasEquivalenceConfig,
66
+ isBooleanValue,
67
+ isDateValue,
68
+ mergeOptionNgClasses,
69
+ normalizeGroupMetaTitles,
70
+ parseCellEditValue,
71
+ resolveCheckboxToggleTarget,
72
+ resolveMainEndpoint,
73
+ resolveNextSortState,
74
+ resolveOptionDisabled,
75
+ resolveOptionIcon,
76
+ resolveOptionTooltip,
77
+ resolveOptionVisible,
78
+ restoreExpandedRowsAfterLoad,
79
+ setNestedValue,
80
+ shouldStopCellClickPropagation,
81
+ syncCheckedFilterSelects,
82
+ trackGroupRowKey} from './complete-crud-table.util';
83
+
84
+ export type {
85
+ BuildCrudTableQueryParamsOptions,
86
+ CrudTableCheckboxToggleTarget,
87
+ CrudTableEquivalenceConfig,
88
+ CrudTableEquivalenceConfigs,
89
+ CrudTableEquivalenceData,
90
+ CrudTableEquivalenceMaps,
91
+ CrudTableEquivalenceOption,
92
+ CrudTableGetCellValueFn,
93
+ CrudTableGroupTableConfig,
94
+ CrudTableRowColorSemantic} from './complete-crud-table.types';
95
+ export {
96
+ applyColumnDefaults,
97
+ applyNestedBooleanToggle,
98
+ bindFilterSelectHandlers,
99
+ buildCrudTableQueryParams,
100
+ buildRowNgClass,
101
+ cacheExpandedRowKeys,
102
+ calculateStartIndex,
103
+ calculateTotalPages,
104
+ collectSearchFieldKeys,
105
+ countVisibleColumns,
106
+ CRUD_TABLE_ROW_COLOR_CLASS_MAP,
107
+ deepCopy,
108
+ formatCellDisplayValue,
109
+ generatePaginationPages,
110
+ getColumnColStyles,
111
+ getEquivalenceData,
112
+ getExpandedRowCacheKey,
113
+ getGroupCollapsedHeaderColspan,
114
+ getGroupExpandColspan,
115
+ getGroupMobileStudentHeaderColumns,
116
+ getGroupMobileStudentIndex,
117
+ getGroupMobileStudentTitle,
118
+ getMobileHeaderColumns,
119
+ getMobileMainDescription,
120
+ getMobileMainTitle,
121
+ hasEquivalenceConfig,
122
+ isBooleanValue,
123
+ isDateValue,
124
+ mergeOptionNgClasses,
125
+ normalizeGroupMetaTitles,
126
+ parseCellEditValue,
127
+ resolveCheckboxToggleTarget,
128
+ resolveMainEndpoint,
129
+ resolveNextSortState,
130
+ resolveOptionDisabled,
131
+ resolveOptionIcon,
132
+ resolveOptionTooltip,
133
+ resolveOptionVisible,
134
+ restoreExpandedRowsAfterLoad,
135
+ setNestedValue,
136
+ shouldStopCellClickPropagation,
137
+ syncCheckedFilterSelects,
138
+ trackGroupRowKey} from './complete-crud-table.util';
139
+ export { CRUD_TABLE_ANIMATIONS } from './complete-crud-table.animations';
140
+
141
+ /**
142
+ * Full CRUD data table: filters, pagination, sorting, inline edit, expand rows, group mode.
143
+ *
144
+ * Install: `npx tailjng add table-crud-complete`
145
+ *
146
+ * Heavy logic lives in `complete-crud-table.util.ts`; expand grid in `expand-grid/`.
147
+ */
23
148
  @Component({
24
149
  selector: 'JCompleteCrudTable',
25
150
  standalone: true,
26
- imports: [CommonModule, FormsModule, JCompletePaginatorComponent, JCompleteFilterComponent, LucideAngularModule, JButtonComponent, JInputCheckboxComponent, JDialogComponent, JImageViewerComponent, JDropdownSelectComponent, JInputComponent, JOptionsCoachMenuComponent],
151
+ imports: [
152
+ CommonModule,
153
+ FormsModule,
154
+ JCompletePaginatorComponent,
155
+ JCompleteFilterComponent,
156
+ JIconComponent,
157
+ JButtonComponent,
158
+ JInputCheckboxComponent,
159
+ JDialogComponent,
160
+ JImageViewerComponent,
161
+ JDropdownSelectComponent,
162
+ JInputComponent,
163
+ JOptionsCoachMenuComponent,
164
+ ],
27
165
  templateUrl: './complete-crud-table.component.html',
28
166
  styleUrl: './complete-crud-table.component.scss',
29
- animations: [
30
- trigger('slideToggle', [
31
- state('collapsed', style({
32
- height: '0',
33
- opacity: 0,
34
- })),
35
- state('expanded', style({
36
- height: '*',
37
- opacity: 1,
38
- })),
39
- transition('collapsed <=> expanded', [
40
- animate('0.3s ease-in-out')
41
- ])
42
- ]),
43
- trigger('groupSlideToggle', [
44
- state('collapsed', style({
45
- height: '0',
46
- opacity: 0,
47
- overflow: 'hidden',
48
- })),
49
- state('expanded', style({
50
- height: '*',
51
- opacity: 1,
52
- overflow: 'visible',
53
- })),
54
- transition('collapsed => expanded', [
55
- animate('380ms cubic-bezier(0.4, 0, 0.2, 1)'),
56
- ]),
57
- transition('expanded => collapsed', [
58
- animate('300ms cubic-bezier(0.4, 0, 0.6, 1)'),
59
- ]),
60
- ]),
61
- trigger('groupRowReveal', [
62
- transition(':enter', [
63
- style({ opacity: 0, transform: 'translateY(-4px)' }),
64
- animate(
65
- '280ms cubic-bezier(0.4, 0, 0.2, 1)',
66
- style({ opacity: 1, transform: 'translateY(0)' }),
67
- ),
68
- ]),
69
- transition(':leave', [
70
- animate(
71
- '220ms cubic-bezier(0.4, 0, 0.6, 1)',
72
- style({ opacity: 0, transform: 'translateY(-4px)' }),
73
- ),
74
- ]),
75
- ]),
76
- ]
77
- })
167
+ animations: CRUD_TABLE_ANIMATIONS})
78
168
  export class JCompleteCrudTableComponent implements OnInit {
169
+ readonly Icons = Icons;
79
170
 
80
171
  Math = Math;
81
172
 
82
- // Charge status
173
+ // =====================================================
174
+ // Loading
175
+ // =====================================================
176
+
83
177
  @Output() dataLoaded = new EventEmitter<void>();
178
+
84
179
  loadingStates: LoadingStates = {
85
180
  initialLoad: 'idle',
86
181
  search: 'idle',
@@ -89,131 +184,173 @@ export class JCompleteCrudTableComponent implements OnInit {
89
184
  sort: 'idle',
90
185
  aditionalButtons: {},
91
186
  checked: 'idle',
92
- action: 'idle',
93
- };
187
+ action: 'idle'};
94
188
 
95
- @Input() urlBase: string = '';
189
+ // =====================================================
190
+ // Data / endpoint
191
+ // =====================================================
192
+
193
+ @Input() urlBase = '';
96
194
  @Input() endpoint!: string;
97
195
  mainEndpoint!: string;
196
+
197
+ @Input() staticData: any[] = [];
98
198
  @Input() columns: TableColumn<any>[] = [];
99
- @Input() isNumbering: boolean = true;
100
- @Input() defaultFilters: { [key: string]: any } = {};
199
+
200
+ data: any[] = [];
201
+
202
+ @Output() loadedData = new EventEmitter<any[]>();
203
+ @Output() loadedParams = new EventEmitter<any>();
204
+
205
+ displayData: any[] = [];
206
+ metadata: any = {};
207
+
208
+ @Input() defaultFilters: Record<string, unknown> = {};
209
+
210
+ // =====================================================
211
+ // Table chrome
212
+ // =====================================================
213
+
214
+ @Input() isNumbering = true;
101
215
  @Input() isPaginator = true;
102
216
  @Input() isFilter = true;
103
217
  @Input() isbackground = false;
104
218
  @Input() isSearch = true;
105
219
  @Input() isEyeColumn = true;
106
- @Input() hideRowCondition?: (row: any) => boolean
107
-
108
- data: any[] = [];
220
+ @Input() hideRowCondition?: (row: any) => boolean;
109
221
 
110
- @Output() loadedData: EventEmitter<any[]> = new EventEmitter<any[]>();
111
- @Output() loadedParams: EventEmitter<any> = new EventEmitter<any>();
222
+ // =====================================================
223
+ // Options column
224
+ // =====================================================
112
225
 
113
- // Type options
114
226
  @Input() optionsType: 'button' | 'dropdown' = 'button';
115
- @Input() isOptions: boolean = true;
227
+ @Input() isOptions = true;
228
+
229
+ // =====================================================
230
+ // Expand rows
231
+ // =====================================================
116
232
 
117
- // Expansion
118
233
  expandTemplate?: (row: any) => string;
119
- expandedRows: Set<any> = new Set();
234
+ expandedRows = new Set<any>();
120
235
 
236
+ // =====================================================
121
237
  // Paginator
238
+ // =====================================================
239
+
122
240
  currentPage = 1;
123
- @Input() isItemsPerPage: boolean = true;
241
+
242
+ @Input() isItemsPerPage = true;
124
243
  @Input() itemsPerPageOptions: number[] = [10, 25, 50, 100];
125
244
  @Input() itemsPerPage = this.itemsPerPageOptions[0];
245
+
126
246
  totalItems = 0;
247
+ pages: number[] = [];
248
+
249
+ get startIndex(): number {
250
+ return calculateStartIndex(this.currentPage, this.itemsPerPage);
251
+ }
127
252
 
253
+ get totalPages(): number {
254
+ return calculateTotalPages(this.totalItems, this.itemsPerPage);
255
+ }
256
+
257
+ get params(): Params {
258
+ return this.getQueryParams();
259
+ }
260
+
261
+ // =====================================================
128
262
  // Sorting
263
+ // =====================================================
264
+
129
265
  sortColumn: string | null = null;
130
266
  sortDirection: SortDirection = 'none';
131
267
  sortingColumn: string | null = null;
132
268
 
133
- // Search
134
- searchQuery = '';
135
- @Input() searchPlaceholder = 'Buscar...';
136
-
137
- // Filters
138
- filters: any = {};
139
-
140
269
  @Input() initialSortColumn: string | null = null;
141
270
  @Input() initialSortDirection: SortDirection = 'asc';
142
271
 
143
- // Data and metadata
144
- displayData: any[] = [];
145
- metadata: any = {};
272
+ // =====================================================
273
+ // Search / filters
274
+ // =====================================================
146
275
 
147
- // Pagination display
148
- pages: number[] = [];
276
+ searchQuery = '';
277
+ @Input() searchPlaceholder = 'Buscar...';
149
278
 
150
- // Check
151
- @Input() checked: boolean = false;
152
- @Input() checkedColumn: string = 'id_status';
153
- @Input() checkedValues: any[][] = [[true], [false]];
154
- @Input() checkedTitles: string[] = ["Activos", "Inactivos"];
155
- isChecked!: boolean;
156
- titleChecked!: string;
279
+ filters: Record<string, unknown> = {};
157
280
 
158
- // Calculated properties for display
159
- get startIndex(): number {
160
- return (this.currentPage - 1) * this.itemsPerPage;
161
- }
281
+ @Input() filtersButton: FilterButton[] = [];
282
+ @Input() filtersSelect: FilterSelect[] = [];
162
283
 
163
- get totalPages(): number {
164
- return Math.ceil(this.totalItems / this.itemsPerPage);
165
- }
284
+ // =====================================================
285
+ // Active / inactive toggle
286
+ // =====================================================
166
287
 
167
- get params(): Params {
168
- return this.getQueryParams();
169
- }
288
+ @Input() checked = false;
289
+ @Input() checkedColumn = 'id_status';
290
+ @Input() checkedValues: any[][] = [[true], [false]];
291
+ @Input() checkedTitles = ['Activos', 'Inactivos'];
170
292
 
171
- // Button configuration
172
- @Input() filtersButton: FilterButton[] = [];
293
+ isChecked!: boolean;
294
+ titleChecked!: string;
173
295
 
174
- // Table filters
175
- @Input() filtersSelect: FilterSelect[] = [];
296
+ // =====================================================
297
+ // Row options
298
+ // =====================================================
176
299
 
177
- // Button options
178
300
  @Input() optionsTable: OptionsTable[] = [];
301
+ selectOptions: Record<string, { key: unknown; label: string }[]> = {};
179
302
 
180
- // Group table configuration
181
- @Input() isGroupTable: boolean = false;
182
- @Input() groupTable: { [key: string]: any } = {};
183
- @Output() loadDataGroup = new EventEmitter<{ data: any[], metadata: any }>();
303
+ // =====================================================
304
+ // Group table
305
+ // =====================================================
306
+
307
+ @Input() isGroupTable = false;
308
+ @Input() groupTable: CrudTableGroupTableConfig = {};
309
+ @Output() loadDataGroup = new EventEmitter<{ data: any[]; metadata: any }>();
184
310
  @Input() groupsTemplate!: TemplateRef<any>;
185
311
 
186
- // Variables for image dialog
312
+ // =====================================================
313
+ // Image dialog
314
+ // =====================================================
315
+
187
316
  keyDialog!: string;
188
317
  mediaDialog!: SafeUrl | string;
189
318
  titleDialog!: string;
190
319
 
191
- // Edit record in table
320
+ // =====================================================
321
+ // Inline row editing
322
+ // =====================================================
323
+
192
324
  editingRowItem: any = null;
193
325
  originalRowItem: any = null;
194
- @Input() equivalenceConfigs: { [key: string]: { keyColumnSearch: string; keyReturn: string; keyAlternate?: string; optional?: boolean } } = {};
195
- @Input() equivalences: any = {};
326
+
327
+ @Input() equivalenceConfigs: CrudTableEquivalenceConfigs = {};
328
+ @Input() equivalences: CrudTableEquivalenceMaps = {};
329
+
330
+ // =====================================================
331
+ // Mobile nested expand
332
+ // =====================================================
333
+
334
+ mobileInnerExpandedRows = new Set<any>();
196
335
 
197
336
  constructor(
198
337
  public readonly form: JFormShared,
199
338
  public readonly dialog: JDialogShared,
200
339
  public readonly transformService: JTransformService,
201
- public readonly iconsService: JIconsService,
202
340
  private readonly sanitizer: DomSanitizer,
203
341
  private readonly genericService: JGenericCrudService,
204
342
  private readonly alertToastService: JAlertToastService,
205
343
  private readonly converterService: JConverterCrudService,
206
344
  private readonly uploadFilterService: JUploadFilterService,
207
- private cd: ChangeDetectorRef
208
- ) { }
345
+ private readonly cd: ChangeDetectorRef,
346
+ ) {}
209
347
 
210
- ngOnInit() {
211
- this.mainEndpoint = this.endpoint.split('/')[0] ?? this.endpoint;
348
+ ngOnInit(): void {
349
+ this.mainEndpoint = resolveMainEndpoint(this.endpoint);
212
350
  this.isChecked = this.checkedValues[0][0];
213
351
  this.titleChecked = this.checkedTitles[0];
214
352
  this.columnDefaults();
215
353
 
216
- // Apply initial sort if passed from parent
217
354
  if (this.initialSortColumn) {
218
355
  this.sortColumn = this.initialSortColumn;
219
356
  this.sortDirection = this.initialSortDirection || 'asc';
@@ -221,296 +358,239 @@ export class JCompleteCrudTableComponent implements OnInit {
221
358
 
222
359
  this.loadData();
223
360
  this.overrideFilterEvents();
224
-
225
- // Normalize buttons
226
361
  this.optionsTable = this.normalizeOptionsTable(this.optionsTable);
227
362
  }
228
363
 
229
- // Override filter events to update table filters
230
- overrideFilterEvents() {
231
- for (const filter of this.filtersSelect) {
232
- if (filter.type === 'dropdown' || filter.type === 'searchable') {
233
- const key = filter.optionValue ?? 'value';
234
- const deepKey = filter.deep ? `${filter.deep}.${key}` : key;
235
-
236
- const originalOnSelected = filter.onSelected;
237
-
238
- filter.onSelected = (value: any) => {
239
- const selectedValue = value?.[key] ?? value;
240
-
241
- if (selectedValue === null || selectedValue === undefined) {
242
- delete this.filters[deepKey];
243
- } else {
244
- this.filters[deepKey] = selectedValue;
245
- }
246
-
247
- if (typeof originalOnSelected === 'function') {
248
- originalOnSelected(value);
249
- }
250
-
251
- this.currentPage = 1;
252
- this.loadData('search');
253
- };
254
- }
255
- }
364
+ /** Wraps filter dropdown handlers to sync `this.filters` and reload. */
365
+ overrideFilterEvents(): void {
366
+ bindFilterSelectHandlers(this.filtersSelect, this.filters, () => {
367
+ this.currentPage = 1;
368
+ this.loadData('search');
369
+ });
256
370
  }
257
371
 
258
-
259
- // After file upload
372
+ /** Reload after file upload from filter actions. */
260
373
  onAfterUpload(): void {
261
374
  this.loadData('initialLoad');
262
375
  }
263
376
 
264
377
  // =====================================================
265
- // Get data
378
+ // Data loading
266
379
  // =====================================================
267
380
 
268
- // Load data from server
269
- loadData(loadingType: keyof LoadingStates = 'initialLoad', onFinally?: () => void) {
381
+ /**
382
+ * Loads rows from API or `staticData`, preserving expanded row state when possible.
383
+ * @param loadingType Which loading indicator to show.
384
+ * @param onFinally Optional callback after the request completes.
385
+ */
386
+ loadData(loadingType: keyof LoadingStates = 'initialLoad', onFinally?: () => void): void {
270
387
  this.setLoadingState(loadingType, 'loading');
271
388
 
272
389
  const params = this.getQueryParams();
390
+ const expandedKeys = cacheExpandedRowKeys(this.expandedRows, this.isGroupTable);
273
391
 
274
- // Store references to expanded rows before updating
275
- const expandedItemsCache = new Map();
276
- this.expandedRows.forEach(item => {
277
- // Use a unique identifier for each row (title + description for groups)
278
- const key = this.isGroupTable ?
279
- `${item.title}-${item.description}` :
280
- (item.id || JSON.stringify(item));
281
- expandedItemsCache.set(key, true);
282
- });
283
-
284
- // Simulating API wait
285
- // setTimeout(() => {
286
-
287
- // Emit search parameters
288
392
  this.loadedParams.emit(params);
289
393
 
290
- this.genericService.findAll<any>({ urlBase: this.urlBase, endpoint: this.endpoint, params }).subscribe({
291
- next: (response) => {
394
+ if (this.staticData.length > 0) {
395
+ this.data = this.staticData.map((item) => ({ ...item }));
396
+ this.totalItems = this.data.length;
397
+ this.loadedData.emit(this.data);
398
+ this.updateDisplayData();
399
+ this.generatePagination();
400
+ this.setLoadingState(loadingType, 'success');
401
+ this.dataLoaded.emit();
292
402
 
293
- this.data = response.data[this.mainEndpoint] ?? [];
294
-
295
- // Restore expanded state
296
- if (expandedItemsCache.size > 0) {
297
- this.expandedRows.clear();
298
- this.data.forEach(item => {
299
- const key = this.isGroupTable ?
300
- `${item.title}-${item.description}` :
301
- (item.id || JSON.stringify(item));
302
- if (expandedItemsCache.has(key)) {
303
- this.expandedRows.add(item);
304
- }
305
- });
306
- }
403
+ if (loadingType === 'sort') {
404
+ this.sortingColumn = null;
405
+ }
406
+
407
+ onFinally?.();
408
+ this.cd.detectChanges();
409
+ return;
410
+ }
307
411
 
308
- this.data.forEach((item: any) => {
309
- if (item.meta?.page && item.title) {
310
- item.meta.page.title = item.title;
412
+ this.genericService
413
+ .findAll<any>({ urlBase: this.urlBase, endpoint: this.endpoint, params })
414
+ .subscribe({
415
+ next: (response) => {
416
+ this.data = response.data[this.mainEndpoint] ?? [];
417
+
418
+ if (expandedKeys.size > 0) {
419
+ this.expandedRows = restoreExpandedRowsAfterLoad(
420
+ this.data,
421
+ expandedKeys,
422
+ this.isGroupTable,
423
+ );
311
424
  }
312
- });
313
425
 
314
- this.loadedData.emit(this.data);
426
+ normalizeGroupMetaTitles(this.data);
427
+ this.loadedData.emit(this.data);
315
428
 
316
- this.loadDataGroup.emit({
317
- data: this.data,
318
- metadata: response.meta,
319
- });
429
+ this.loadDataGroup.emit({
430
+ data: this.data,
431
+ metadata: response.meta});
320
432
 
321
- if (response.meta?.page) {
322
- this.totalItems = response.meta.page.totalRecords;
323
- this.currentPage = response.meta.page.currentPage;
324
- } else {
325
- this.totalItems = this.data.length;
326
- }
433
+ if (response.meta?.page) {
434
+ this.totalItems = response.meta.page.totalRecords;
435
+ this.currentPage = response.meta.page.currentPage;
436
+ } else {
437
+ this.totalItems = this.data.length;
438
+ }
327
439
 
328
- if (response.meta?.sort) {
329
- this.sortColumn = response.meta.sort.by;
330
- this.sortDirection = response.meta.sort.order.toLowerCase() as SortDirection;
331
- }
440
+ if (response.meta?.sort) {
441
+ this.sortColumn = response.meta.sort.by;
442
+ this.sortDirection = response.meta.sort.order.toLowerCase() as SortDirection;
443
+ }
332
444
 
333
- this.updateDisplayData();
334
- this.generatePagination();
335
- this.setLoadingState(loadingType, 'success');
336
- this.cd.detectChanges();
337
- },
338
- error: (error) => {
339
- console.error('Error fetching data:', error);
340
- this.setLoadingState(loadingType, 'error');
341
- }
342
- }).add(() => {
343
- this.dataLoaded.emit();
344
- if (loadingType === 'sort') {
345
- this.sortingColumn = null;
346
- }
445
+ this.updateDisplayData();
446
+ this.generatePagination();
447
+ this.setLoadingState(loadingType, 'success');
448
+ this.cd.detectChanges();
449
+ },
450
+ error: (error) => {
451
+ console.error('Error fetching data:', error);
452
+ this.setLoadingState(loadingType, 'error');
453
+ }})
454
+ .add(() => {
455
+ this.dataLoaded.emit();
456
+
457
+ if (loadingType === 'sort') {
458
+ this.sortingColumn = null;
459
+ }
347
460
 
348
- if (onFinally) {
349
- onFinally();
350
- }
351
- });
352
- // }, 2000);
461
+ onFinally?.();
462
+ });
353
463
  }
354
464
 
355
- // Update the data displayed in the table
356
- updateDisplayData() {
465
+ /** Copies `data` into `displayData` (hook point for future client-side transforms). */
466
+ updateDisplayData(): void {
357
467
  this.displayData = this.data;
358
468
  }
359
469
 
360
470
  // =====================================================
361
- // Table options
471
+ // Table options normalization
362
472
  // =====================================================
363
473
 
364
- selectOptions: { [key: string]: { key: any, label: string }[] } = {};
365
-
366
- // Normalize table options
474
+ /** Enriches `editRow` options with equivalence maps and dynamic icons. */
367
475
  normalizeOptionsTable(buttons: OptionsTable[]): OptionsTable[] {
368
476
  return buttons.map((button) => {
477
+ if (button.type !== 'editRow') {
478
+ return button;
479
+ }
369
480
 
370
- // If it's a filter button
371
- if (button.type === 'editRow') {
372
-
373
- this.equivalenceConfigs = button?.searchListUpload ?? {};
374
-
375
- // Process selected IDs
376
- // In the normalizeOptionsTable method, replace this part:
377
- forkJoin(this.uploadFilterService.getIdsSelectedData(button)).subscribe({
378
- next: (results) => {
379
- // Initialize the equivalences object if it doesn't exist
380
- if (!this.equivalences) this.equivalences = {};
481
+ this.equivalenceConfigs = button?.searchListUpload ?? {};
381
482
 
382
- results.forEach((result, index) => {
383
- const { data, mappingInfo } = result;
384
- const keyReturn = mappingInfo.keyReturn;
385
- const keyAlternate = mappingInfo.keyAlternate;
386
- const keyColumnSearch = mappingInfo.keyColumnSearch;
483
+ forkJoin(this.uploadFilterService.getIdsSelectedData(button)).subscribe({
484
+ next: (results) => {
485
+ if (!this.equivalences) {
486
+ this.equivalences = {};
487
+ }
387
488
 
388
- if (!keyReturn || !keyColumnSearch) return;
489
+ results.forEach((result) => {
490
+ const { data, mappingInfo } = result;
491
+ const keyReturn = mappingInfo.keyReturn;
492
+ const keyAlternate = mappingInfo.keyAlternate;
493
+ const keyColumnSearch = mappingInfo.keyColumnSearch;
389
494
 
390
- // Initialize the equivalences structure for this keyReturn
391
- if (!this.equivalences[keyReturn]) this.equivalences[keyReturn] = {};
495
+ if (!keyReturn || !keyColumnSearch) {
496
+ return;
497
+ }
392
498
 
499
+ if (!this.equivalences[keyReturn]) {
500
+ this.equivalences[keyReturn] = {};
501
+ }
393
502
 
394
- data.forEach((item: any) => {
395
- const idKey = keyAlternate ?? keyReturn;
396
- const searchKey = keyColumnSearch.includes(".") ? keyColumnSearch.split(".").pop()! : keyColumnSearch;
503
+ data.forEach((item: any) => {
504
+ const idKey = keyAlternate ?? keyReturn;
505
+ const searchKey = keyColumnSearch.includes('.')
506
+ ? keyColumnSearch.split('.').pop()!
507
+ : keyColumnSearch;
397
508
 
398
- this.equivalences[keyReturn][item[idKey]] = item[searchKey];
399
- });
509
+ this.equivalences[keyReturn][item[idKey]] = item[searchKey];
400
510
  });
401
-
511
+ });
512
+ }});
513
+
514
+ return {
515
+ ...button,
516
+ icon: (data: any) =>
517
+ this.editingRowItem === data
518
+ ? Icons.Pencil
519
+ : Icons.PencilLine,
520
+ tooltip: (data: any) =>
521
+ this.optionsType === 'button'
522
+ ? this.editingRowItem === data
523
+ ? 'Guardar edición'
524
+ : 'Editar en tabla'
525
+ : '',
526
+ text: (data: any) =>
527
+ this.optionsType === 'dropdown'
528
+ ? this.editingRowItem === data
529
+ ? 'Guardar edición'
530
+ : 'Editar en tabla'
531
+ : '',
532
+ clicked: (data) => {
533
+ if (
534
+ this.editingRowItem &&
535
+ this.editingRowItem[`id_${this.mainEndpoint}`] === data[`id_${this.mainEndpoint}`]
536
+ ) {
537
+ this.onSaveRow(data);
538
+ } else {
539
+ this.onEditRow(data);
402
540
  }
403
- });
404
-
405
- return {
406
- ...button,
407
- icon: (data: any) => this.editingRowItem === data ? this.iconsService.icons.editRow : this.iconsService.icons.editRowLine,
408
- tooltip: (data: any) => this.optionsType === 'button' ? (this.editingRowItem === data ? 'Guardar edición' : 'Editar en tabla') : '',
409
- text: (data: any) => this.optionsType === 'dropdown' ? (this.editingRowItem === data ? 'Guardar edición' : 'Editar en tabla') : '',
410
- clicked: (data) => {
411
- if (this.editingRowItem && this.editingRowItem[`id_${this.mainEndpoint}`] === data[`id_${this.mainEndpoint}`]) {
412
- this.onSaveRow(data);
413
- } else {
414
- this.onEditRow(data);
415
- }
416
- },
417
- ngClass: (data: any) => this.editingRowItem === data ? 'success_secondary' : 'warning_secondary',
418
- };
419
- }
420
-
421
- return button;
541
+ },
542
+ ngClass: (data: any) =>
543
+ this.editingRowItem === data ? 'success_secondary' : 'warning_secondary'};
422
544
  });
423
545
  }
424
546
 
425
547
  // =====================================================
426
- // Change state in boolean fields
548
+ // Boolean checkbox column
427
549
  // =====================================================
428
- onCheckboxChange(item: any, column: TableColumn<any>) {
429
-
430
- let key = column.key;
431
- let recordId: any;
432
- let endpoint = this.mainEndpoint;
433
-
434
- // Verificar si es key compuesta
435
- if (key.includes('.')) {
436
- const parts = key.split('.');
437
- const parent = parts[0];
438
- const lastKey = parts[parts.length - 1];
439
-
440
- key = lastKey;
441
-
442
- // El id corresponde a la tabla padre
443
- recordId = item[`id_${parent}`];
444
-
445
- // El endpoint también debe ser la tabla padre
446
- endpoint = parent;
447
- } else {
448
- // key simple
449
- recordId = item[`id_${this.mainEndpoint}`];
450
- }
451
550
 
452
- // Obtener el valor actual
551
+ /** Toggles a boolean field via API (supports nested composite keys). */
552
+ onCheckboxChange(item: any, column: TableColumn<any>): void {
553
+ const { key, recordId, endpoint } = resolveCheckboxToggleTarget(
554
+ item,
555
+ column,
556
+ this.mainEndpoint,
557
+ );
453
558
  const currentValue = this.getValue(item, column);
454
559
 
455
560
  const formData: EnableBoolean<any> = {
456
- key: key,
561
+ key,
457
562
  value: currentValue,
458
- values: [currentValue, !currentValue]
459
- };
563
+ values: [currentValue, !currentValue]};
460
564
 
461
- // Llamar al servicio
462
565
  this.genericService.toggle<any>({ endpoint, id: recordId, data: formData }).subscribe({
463
566
  next: (response) => {
464
- // Actualizar localmente el valor
465
- if (column.key.includes('.')) {
466
- const parts = column.key.split('.');
467
- const parent = parts[0];
468
- const lastKey = parts[parts.length - 1];
469
-
470
- // Actualizar objeto anidado
471
- item[parent] = { ...item[parent], [lastKey]: !currentValue };
472
- } else {
473
- item[key] = !currentValue;
474
- }
567
+ applyNestedBooleanToggle(item, column.key, currentValue as boolean);
475
568
 
476
569
  this.alertToastService.AlertToast({
477
- type: "success",
478
- title: "Registro actualizado!",
479
- description: response.message,
480
- });
481
- }
482
- });
570
+ type: 'success',
571
+ title: 'Registro actualizado!',
572
+ description: response.message});
573
+ }});
483
574
  }
484
575
 
485
- // Change active or inactive
576
+ /** Switches active/inactive filter without clearing other filters. */
486
577
  checkActiveInactive(isChecked: boolean): void {
487
578
  this.isChecked = !isChecked;
488
579
 
489
580
  const index = this.isChecked ? 0 : 1;
490
581
  this.titleChecked = this.checkedTitles[index];
491
-
492
- // ONLY update the id_status property without touching the other filters
493
582
  this.filters[this.checkedColumn] = this.checkedValues[index];
494
-
495
- this.filtersSelect = this.filtersSelect.map(filter => {
496
- if ('optionValue' in filter && filter.optionValue === this.checkedColumn) {
497
- return {
498
- ...filter,
499
- defaultFilters: {
500
- ...(filter.hasOwnProperty('defaultFilters') ? (filter as any).defaultFilters : {}),
501
- [this.checkedColumn]: this.filters[this.checkedColumn]
502
- },
503
- selected: null
504
- };
505
- }
506
- return filter;
507
- });
583
+ this.filtersSelect = syncCheckedFilterSelects(
584
+ this.filtersSelect,
585
+ this.checkedColumn,
586
+ this.filters[this.checkedColumn],
587
+ );
508
588
 
509
589
  this.currentPage = 1;
510
590
  this.loadData('checked');
511
591
  }
512
592
 
513
- // Remove filters
593
+ /** Clears filters and reloads (used by filter bar clear action). */
514
594
  onClearFilters(buttonType: string): void {
515
595
  this.setAditionalButtonLoading(buttonType);
516
596
 
@@ -523,60 +603,24 @@ export class JCompleteCrudTableComponent implements OnInit {
523
603
  // Columns
524
604
  // =====================================================
525
605
 
526
- // Default column values
527
- columnDefaults() {
528
- // Default values for columns
529
- this.columns.forEach(column => {
530
- if (column.visible === undefined) {
531
- column.visible = true;
532
- }
533
-
534
- if (column.sortable === undefined) {
535
- column.sortable = true;
536
- }
537
-
538
- if (column.isSearchable === undefined) {
539
- column.isSearchable = true;
540
- }
541
- });
606
+ columnDefaults(): void {
607
+ applyColumnDefaults(this.columns);
542
608
  }
543
609
 
544
- // Get the count of visible columns for colspan in empty state
545
610
  getVisibleColumnsCount(): number {
546
- return this.columns.filter(col => col.visible).length;
611
+ return countVisibleColumns(this.columns);
547
612
  }
548
613
 
549
- /** En tablas `table-layout: fixed`, el ancho debe ir en `<col>`; minWidth solo en `<td>` no aplica. */
550
614
  getColumnColStyles(column: TableColumn<any>): Record<string, string> | undefined {
551
- if (!column.styles) {
552
- return undefined;
553
- }
554
-
555
- const styles = { ...column.styles };
556
-
557
- if (styles['minWidth'] && !styles['width']) {
558
- styles['width'] = styles['minWidth'];
559
- }
560
-
561
- return styles;
615
+ return getColumnColStyles(column);
562
616
  }
563
617
 
564
618
  getGroupExpandColspan(): number {
565
- let colspan = this.getVisibleColumnsCount();
566
-
567
- if (this.isNumbering) {
568
- colspan += 1;
569
- }
570
-
571
- if (this.isOptions) {
572
- colspan += 1;
573
- }
574
-
575
- return colspan;
619
+ return getGroupExpandColspan(this.columns, this.isNumbering, this.isOptions);
576
620
  }
577
621
 
578
622
  getGroupCollapsedHeaderColspan(): number {
579
- return 1 + this.getVisibleColumnsCount() + (this.isOptions ? 1 : 0);
623
+ return getGroupCollapsedHeaderColspan(this.columns, this.isOptions);
580
624
  }
581
625
 
582
626
  getGroupExpansionState(item: any): 'expanded' | 'collapsed' {
@@ -584,20 +628,19 @@ export class JCompleteCrudTableComponent implements OnInit {
584
628
  }
585
629
 
586
630
  trackGroupRow(group: any, index: number): string | number {
587
- return group?.id_student_course ?? group?.id ?? index;
631
+ return trackGroupRowKey(group, index);
588
632
  }
589
633
 
590
- // Handle cell click events
591
- handleCellClick(event: MouseEvent, row: any, column: TableColumn<any>) {
592
- // If no onCellClick defined, do nothing
593
- if (!column.onCellClick) return;
634
+ handleCellClick(event: MouseEvent, row: any, column: TableColumn<any>): void {
635
+ if (!column.onCellClick) {
636
+ return;
637
+ }
594
638
 
595
- // If there is a clickable condition and it is not met, do nothing
596
- if (column.isCellClickable && !column.isCellClickable(row)) return;
639
+ if (column.isCellClickable && !column.isCellClickable(row)) {
640
+ return;
641
+ }
597
642
 
598
- // By default, we do NOT want the click to also toggleRow
599
- const shouldStop = column.stopRowClickOnCellClick !== false; // default true
600
- if (shouldStop) {
643
+ if (shouldStopCellClickPropagation(column)) {
601
644
  event.stopPropagation();
602
645
  }
603
646
 
@@ -606,136 +649,71 @@ export class JCompleteCrudTableComponent implements OnInit {
606
649
  }
607
650
 
608
651
  // =====================================================
609
- // Data processing
652
+ // Cell values
610
653
  // =====================================================
611
654
 
612
- // Get the cell value to identify a boolean field
613
655
  isBoolean(value: any): boolean {
614
- return typeof value === 'boolean';
656
+ return isBooleanValue(value);
615
657
  }
616
658
 
617
- // Get the cell value to identify a date field
618
659
  isDate(value: any): boolean {
619
- return value instanceof Date || (typeof value === 'string' && !isNaN(Date.parse(value)));
660
+ return isDateValue(value);
620
661
  }
621
662
 
622
- // MMethod to get the cell value dynamically
623
663
  getValue(item: any, column: TableColumn<any>, row = false): any {
624
- let value: any;
625
-
626
- // If a valueGetter exists, use it directly
627
- if (typeof column.valueGetter === 'function') {
628
- value = column.valueGetter(item);
629
- } else {
630
- // If not, look for the value by key (with support for nested keys)
631
- const keys = column.key.split('.');
632
- value = item;
633
-
634
- for (const key of keys) {
635
- if (value != null) {
636
- value = value[key];
637
- } else {
638
- value = null;
639
- break;
640
- }
641
- }
642
- }
643
-
644
664
  if (row) {
645
- // Transform the data
646
- return this.converterService.parseData(value, column);
647
- } else {
648
-
649
- // If value is null or undefined, return 'S/N'
650
- if (value === null || value === undefined) {
651
- return 'S/N';
652
- }
653
-
654
- // If formatData returns nothing (unformatted value), return the original value
655
- return this.converterService.formatData(value, column) ?? value;
665
+ return parseCellEditValue(item, column, this.converterService);
656
666
  }
667
+
668
+ return formatCellDisplayValue(item, column, this.converterService);
657
669
  }
658
670
 
659
671
  // =====================================================
660
- // Search parameters
672
+ // Query params
661
673
  // =====================================================
662
674
 
663
- // Get the query parameters for the data request
664
675
  getQueryParams(): Params {
665
- const params: Params = this.genericService.params({
666
- page: this.currentPage,
667
- limit: this.itemsPerPage,
668
- sort: {
669
- column: this.sortColumn,
670
- direction: this.sortDirection,
671
- },
676
+ return buildCrudTableQueryParams(this.genericService, {
677
+ currentPage: this.currentPage,
678
+ itemsPerPage: this.itemsPerPage,
679
+ sortColumn: this.sortColumn,
680
+ sortDirection: this.sortDirection,
672
681
  filters: this.filters,
673
682
  defaultFilters: this.defaultFilters,
674
- });
675
-
676
- if (this.searchQuery?.trim()) {
677
- const baseSearchKeys = this.columns.filter(col => col.isSearchable).map(col => col.key);
678
- const extraSearchKeys = this.columns.flatMap(col => col.extraSearchFields || []);
679
- const allSearchKeys = [...baseSearchKeys, ...extraSearchKeys];
680
- params['search'] = this.searchQuery;
681
- params['searchFields'] = allSearchKeys;
682
- }
683
-
684
- // Add group pagination if defined
685
- if (this.isGroupTable && typeof this.groupTable['getGroupParams'] === 'function') {
686
- Object.assign(params, this.groupTable['getGroupParams']());
687
- }
688
-
689
- return params;
683
+ searchQuery: this.searchQuery,
684
+ columns: this.columns,
685
+ isGroupTable: this.isGroupTable,
686
+ groupTable: this.groupTable});
690
687
  }
691
688
 
692
689
  // =====================================================
693
690
  // Sorting
694
691
  // =====================================================
695
692
 
696
- // Column sorting
697
- onSort(column: TableColumn<any>) {
698
- // Prevent multiple simultaneous sort requests
699
- if (this.isLoading('sort')) {
693
+ onSort(column: TableColumn<any>): void {
694
+ if (this.isLoading('sort') || !column.sortable) {
700
695
  return;
701
696
  }
702
697
 
703
- if (!column.sortable) return;
704
-
705
- // Save the column being sorted
706
698
  this.sortingColumn = column.key;
707
699
 
708
- // Check if we are dealing with the same column as the last sort
709
- const currentSortKey = this.converterService.getSortKey(this.sortColumn);
710
- const columnSortKey = this.converterService.getSortKey(column.key);
711
-
712
- // Sort direction logic
713
- if (currentSortKey === columnSortKey) {
714
- // Toggle sort direction
715
- if (this.sortDirection === 'asc') {
716
- this.sortDirection = 'desc';
717
- } else if (this.sortDirection === 'desc') {
718
- this.sortDirection = 'none';
719
- } else {
720
- this.sortDirection = 'asc';
721
- }
722
- } else {
723
- // If a new column is selected for sorting, start with ascending order
724
- this.sortColumn = column.key;
725
- this.sortDirection = 'asc';
726
- }
700
+ const next = resolveNextSortState(
701
+ column,
702
+ this.sortColumn,
703
+ this.sortDirection,
704
+ (value) => this.converterService.getSortKey(value),
705
+ );
727
706
 
707
+ this.sortColumn = next.sortColumn;
708
+ this.sortDirection = next.sortDirection;
728
709
  this.loadData('sort');
729
710
  }
730
711
 
731
- // Get the sort key
732
712
  getSortKey(value: any): string {
733
713
  return this.converterService.getSortKey(value);
734
714
  }
735
715
 
736
- // Handle key press event for sorting
737
- onSortKeyPress(event: KeyboardEvent, column: TableColumn<any>) {
738
- // If a sort is already in progress, do not allow another
716
+ onSortKeyPress(event: KeyboardEvent, column: TableColumn<any>): void {
739
717
  if (this.isLoading('sort')) {
740
718
  return;
741
719
  }
@@ -747,146 +725,97 @@ export class JCompleteCrudTableComponent implements OnInit {
747
725
  }
748
726
 
749
727
  // =====================================================
750
- // Search
728
+ // Search / pagination
751
729
  // =====================================================
752
730
 
753
- // Search functionality
754
- onSearch() {
731
+ onSearch(): void {
755
732
  this.currentPage = 1;
756
733
  this.loadData('search');
757
734
  }
758
735
 
759
- // Items per page selector
760
- onItemsPerPageChange() {
736
+ onItemsPerPageChange(): void {
761
737
  this.currentPage = 1;
762
738
  this.loadData('itemsPerPage');
763
739
  }
764
740
 
765
- // =====================================================
766
- // Paginator
767
- // =====================================================
768
-
769
- // Generate pagination numbers
770
- generatePagination() {
741
+ generatePagination(): void {
771
742
  const totalPages = this.totalPages;
772
743
  const currentPage = this.currentPage;
773
-
774
- // Show 3 pagination numbers
775
744
  const maxPagesToShow = 3;
776
745
  let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
777
746
  let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
778
747
 
779
- // Adjust if we're near the end
780
748
  if (endPage - startPage + 1 < maxPagesToShow) {
781
749
  startPage = Math.max(1, endPage - maxPagesToShow + 1);
782
750
  }
783
751
 
784
- this.pages = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
752
+ this.pages = Array.from({ length: endPage - startPage + 1 }, (_, index) => startPage + index);
785
753
  }
786
754
 
787
- // Paginator
788
- handlePageChange(page: number) {
755
+ handlePageChange(page: number): void {
789
756
  this.currentPage = page;
790
757
  this.loadData('pagination');
791
758
  }
792
759
 
793
760
  // =====================================================
794
- // Image Visualization
761
+ // Image dialog
795
762
  // =====================================================
796
763
 
797
- // View receipt
798
- showDialogImg(id: string, name: string, url: string) {
764
+ showDialogImg(id: string, name: string, url: string): void {
799
765
  this.keyDialog = id;
800
766
  this.titleDialog = name;
801
-
802
- const finalUrl = url.trim();
803
-
804
- this.mediaDialog = this.sanitizer.bypassSecurityTrustUrl(finalUrl);
767
+ this.mediaDialog = this.sanitizer.bypassSecurityTrustUrl(url.trim());
805
768
  }
806
769
 
807
770
  // =====================================================
808
- // Options
771
+ // Row options
809
772
  // =====================================================
810
773
 
811
- // MMethod to handle button click
812
774
  onButtonClick(button: OptionsTable, element: any): void {
813
- // Executes the parent's action if defined
814
- if (button.clicked) {
815
- button.clicked(element);
816
- }
775
+ button.clicked?.(element);
817
776
  }
818
777
 
819
- // MMethod to get a tooltip
820
778
  getTooltip(tooltip: string | ((data?: any) => string), data: any): string {
821
- if (typeof tooltip === 'function') {
822
- return tooltip(data);
823
- }
824
- return tooltip ?? '';
779
+ return resolveOptionTooltip(tooltip, data);
825
780
  }
826
781
 
827
- // MMethod to get an icon
828
- getIcon(icon: ((data?: any) => any), data: any): any {
829
- if (typeof icon === 'function') {
830
- return icon(data);
831
- }
832
- return icon;
782
+ getIcon(icon: ((data?: any) => any) | undefined, data: any): any {
783
+ return resolveOptionIcon(icon, data);
833
784
  }
834
785
 
835
- // Evaluate if a button is disabled
836
786
  getDisabled(option: OptionsTable, data: any): boolean {
837
- if (typeof option.disabled === 'function') {
838
- return option.disabled(data);
839
- }
840
- return !!option.disabled;
787
+ return resolveOptionDisabled(option, data);
841
788
  }
842
789
 
843
- // Evaluate if a button is visible
844
790
  getIsVisible(option: OptionsTable, data: any): boolean {
845
- if (typeof option.isVisible === 'function') {
846
- return option.isVisible(data);
847
- }
848
- // If not defined, default is visible
849
- return option.isVisible !== false;
791
+ return resolveOptionVisible(option, data);
850
792
  }
851
793
 
852
- // Method to get a button's CSS class
853
- mergeNgClasses(optionNgClass: ((data?: any) => any), data: any): any {
854
- const baseClass = {
855
- 'min-w-auto p-[1px]! pl-[5px]! pr-[5px]!': true
856
- };
857
-
858
- let dynamicClass: any = {};
859
- if (typeof optionNgClass === 'function') {
860
- dynamicClass = optionNgClass(data);
861
- } else {
862
- dynamicClass = optionNgClass ?? {};
863
- }
864
-
865
- return {
866
- ...baseClass,
867
- ...(typeof dynamicClass === 'string' ? { [dynamicClass]: true } : dynamicClass)
868
- };
794
+ mergeNgClasses(optionNgClass: ((data?: any) => any) | undefined, data: any): any {
795
+ return mergeOptionNgClasses(optionNgClass, data);
869
796
  }
870
797
 
871
798
  // =====================================================
872
- // Parameters for loading
799
+ // Loading state
873
800
  // =====================================================
874
801
 
875
- // MMethod to check if a specific state is loading
876
802
  isLoading(state: keyof LoadingStates): boolean {
877
803
  return this.loadingStates[state] === 'loading';
878
804
  }
879
805
 
880
- // MMethod to check if any state is loading
881
806
  isAnyLoading(): boolean {
882
- return Object.values(this.loadingStates).some(state => state === 'loading');
807
+ return Object.values(this.loadingStates).some((state) => state === 'loading');
883
808
  }
884
809
 
885
- // MMethod to update a loading state
886
- setLoadingState(state: keyof LoadingStates, value: LoadingState | { [buttonType: string]: LoadingState }): void {
810
+ setLoadingState(
811
+ state: keyof LoadingStates,
812
+ value: LoadingState | { [buttonType: string]: LoadingState },
813
+ ): void {
887
814
  if (state === 'aditionalButtons') {
888
815
  if (typeof value === 'string') {
889
- console.warn(`No puedes asignar '${value}' directamente a aditionalButtons. Usa setAditionalButtonLoading en su lugar.`);
816
+ console.warn(
817
+ `No puedes asignar '${value}' directamente a aditionalButtons. Usa setAditionalButtonLoading en su lugar.`,
818
+ );
890
819
  } else {
891
820
  this.loadingStates.aditionalButtons = value;
892
821
  }
@@ -895,34 +824,29 @@ export class JCompleteCrudTableComponent implements OnInit {
895
824
  }
896
825
  }
897
826
 
898
- // Activate loading
899
827
  setAditionalButtonLoading(buttonType: string, id?: number | string): void {
900
828
  const key = id !== undefined ? `${buttonType}_${id}` : buttonType;
901
829
  this.loadingStates.aditionalButtons[key] = 'loading';
902
830
  }
903
831
 
904
- // Clear loading
905
832
  clearAditionalButtonLoading(buttonType: string, id?: number | string): void {
906
833
  const key = id !== undefined ? `${buttonType}_${id}` : buttonType;
907
834
  this.loadingStates.aditionalButtons[key] = 'idle';
908
835
  }
909
836
 
910
- // Verify loading
911
837
  isAditionalButtonLoading(buttonType: string, id?: number | string): boolean {
912
838
  const key = id !== undefined ? `${buttonType}_${id}` : buttonType;
913
839
  return this.loadingStates.aditionalButtons[key] === 'loading';
914
840
  }
915
841
 
916
- // ==================================================
842
+ // =====================================================
917
843
  // Expand rows
918
- // ==================================================
844
+ // =====================================================
919
845
 
920
- // Method to check if the table has expandable rows
921
846
  hasExpandable(): boolean {
922
847
  return hasExpandableColumn(this.columns);
923
848
  }
924
849
 
925
- // Method to check if a row is expanded
926
850
  toggleRow(row: any): void {
927
851
  if (this.expandedRows.has(row)) {
928
852
  this.expandedRows.delete(row);
@@ -931,240 +855,105 @@ export class JCompleteCrudTableComponent implements OnInit {
931
855
  }
932
856
  }
933
857
 
934
- // Method to get expanded content from a row
935
858
  getExpandedContent(row: any): SafeHtml {
936
859
  const html = resolveExpandContent(this.columns, row);
937
-
938
860
  return this.sanitizer.bypassSecurityTrustHtml(html);
939
861
  }
940
862
 
941
- // Method to get the expansion state of a row
942
863
  getExpansionState(row: any): 'expanded' | 'collapsed' {
943
864
  return this.expandedRows.has(row) ? 'expanded' : 'collapsed';
944
865
  }
945
866
 
946
867
  // =====================================================
947
- // Table editing functionality
868
+ // Inline row editing
948
869
  // =====================================================
949
870
 
950
- // Method to edit an item
951
- // Replaces the onEditRow method
952
871
  onEditRow(item: any): void {
953
872
  if (this.editingRowItem === item) {
954
873
  this.editingRowItem = null;
955
874
  this.originalRowItem = null;
956
- } else {
957
- // If we are editing a different row, cancel the editing of the previous one
958
- if (this.editingRowItem !== null) {
959
- // Restore the original value of the previous row
960
- this.restoreOriginalData();
961
- }
962
- this.editingRowItem = item;
963
-
964
- this.originalRowItem = this.deepCopy(item);
965
- }
966
- }
967
-
968
- // Replaces the restoreOriginalData method
969
- restoreOriginalData() {
970
- if (this.editingRowItem && this.originalRowItem) {
971
- const restoredData = this.deepCopy(this.originalRowItem);
972
- Object.assign(this.editingRowItem, restoredData);
973
- }
974
- }
975
-
976
- // Adds this new method for deep copying
977
- private deepCopy(obj: any): any {
978
- if (obj === null || typeof obj !== 'object') {
979
- return obj;
875
+ return;
980
876
  }
981
877
 
982
- if (obj instanceof Date) {
983
- return new Date(obj.getTime());
878
+ if (this.editingRowItem !== null) {
879
+ this.restoreOriginalData();
984
880
  }
985
881
 
986
- if (Array.isArray(obj)) {
987
- return obj.map(item => this.deepCopy(item));
988
- }
882
+ this.editingRowItem = item;
883
+ this.originalRowItem = deepCopy(item);
884
+ }
989
885
 
990
- const copy: any = {};
991
- for (const key in obj) {
992
- if (obj.hasOwnProperty(key)) {
993
- copy[key] = this.deepCopy(obj[key]);
994
- }
886
+ restoreOriginalData(): void {
887
+ if (this.editingRowItem && this.originalRowItem) {
888
+ Object.assign(this.editingRowItem, deepCopy(this.originalRowItem));
995
889
  }
996
-
997
- return copy;
998
890
  }
999
891
 
1000
- // Method to save the editing of the item
1001
892
  onSaveRow(item: any): void {
1002
-
1003
- // Get the name of the entity's id
1004
893
  const idField = `id_${this.mainEndpoint}`;
1005
894
 
1006
- // If the item being edited exists, save it
1007
895
  if (this.editingRowItem && this.editingRowItem[idField] === item[idField]) {
1008
-
1009
- this.genericService.update<any>({ endpoint: this.mainEndpoint, id: this.editingRowItem[idField], data: this.editingRowItem }).subscribe({
1010
- next: (response) => {
1011
-
1012
- this.alertToastService.AlertToast({
1013
- type: "success",
1014
- title: "Registro actualizado!",
1015
- description: response.message,
1016
- })
1017
-
1018
- // Remove the editing item
1019
- this.editingRowItem = null;
1020
-
1021
- }
1022
- })
1023
-
1024
- } else {
1025
- this.alertToastService.AlertToast({
1026
- type: "error",
1027
- title: "Error al guardar",
1028
- description: "No se pudo guardar el registro. Por favor, inténtelo de nuevo.",
1029
- });
896
+ this.genericService
897
+ .update<any>({
898
+ endpoint: this.mainEndpoint,
899
+ id: this.editingRowItem[idField],
900
+ data: this.editingRowItem})
901
+ .subscribe({
902
+ next: (response) => {
903
+ this.alertToastService.AlertToast({
904
+ type: 'success',
905
+ title: 'Registro actualizado!',
906
+ description: response.message});
907
+
908
+ this.editingRowItem = null;
909
+ }});
910
+ return;
1030
911
  }
912
+
913
+ this.alertToastService.AlertToast({
914
+ type: 'error',
915
+ title: 'Error al guardar',
916
+ description: 'No se pudo guardar el registro. Por favor, inténtelo de nuevo.'});
1031
917
  }
1032
918
 
1033
- // Checks if a column has equivalence configuration
1034
919
  hasEquivalence(columnKey: string): boolean {
1035
- for (const [, conf] of Object.entries(this.equivalenceConfigs || {})) {
1036
- if (conf && conf.keyColumnSearch === columnKey) {
1037
- return true;
1038
- }
1039
- }
1040
- return false;
920
+ return hasEquivalenceConfig(this.equivalenceConfigs, columnKey);
1041
921
  }
1042
922
 
1043
- // Gets the equivalence data for a specific column
1044
- getEquivalenceData(columnKey: string): {
1045
- keyReturn: string;
1046
- options: { key: any; label: string }[];
1047
- optional: boolean
1048
- } | null {
1049
-
1050
- for (const [name, conf] of Object.entries(this.equivalenceConfigs || {})) {
1051
- if (conf && conf.keyColumnSearch === columnKey) {
1052
- const keyReturn = conf.keyReturn;
1053
- const idMap = (this.equivalences as any)[keyReturn];
1054
-
1055
- if (!idMap) return null;
1056
-
1057
- const options = Object.entries(idMap).map(([id, label]) => ({
1058
- key: +id,
1059
- label: String(label),
1060
- }));
1061
-
1062
- return {
1063
- keyReturn,
1064
- options,
1065
- optional: conf.optional || false,
1066
- };
1067
- }
1068
- }
1069
- return null;
923
+ getEquivalenceData(columnKey: string): ReturnType<typeof resolveEquivalenceData> {
924
+ return resolveEquivalenceData(this.equivalenceConfigs, this.equivalences, columnKey);
1070
925
  }
1071
926
 
1072
- // Sets a nested value in an object based on the column key
1073
927
  setNestedValue(item: any, columnKey: string, value: any): void {
1074
- const keys = columnKey.split('.');
1075
- let current = item;
1076
-
1077
- for (let i = 0; i < keys.length; i++) {
1078
- const key = keys[i];
1079
- if (i === keys.length - 1) {
1080
- current[key] = value;
1081
- } else {
1082
- if (!current[key]) {
1083
- current[key] = {};
1084
- }
1085
- current = current[key];
1086
- }
1087
- }
928
+ setNestedValue(item, columnKey, value);
1088
929
  }
1089
930
 
1090
- // =====================================================
1091
- // Update table editing
1092
- // =====================================================
1093
-
1094
- // Handles the equivalence change in an item
1095
931
  onEquivalenceChange(item: any, columnKey: string, newId: any): void {
1096
932
  const eqData = this.getEquivalenceData(columnKey);
1097
- if (!eqData) return;
933
+ if (!eqData) {
934
+ return;
935
+ }
1098
936
 
1099
- // Change the value in the edited object (directly in the same object)
1100
937
  const newLabel = eqData.options.find((opt) => opt.key == newId)?.label ?? null;
1101
- this.setNestedValue(this.editingRowItem, columnKey, newLabel);
938
+ setNestedValue(this.editingRowItem, columnKey, newLabel);
1102
939
  this.editingRowItem[eqData.keyReturn] = newId;
1103
940
  }
1104
941
 
1105
- // Handles the change of value in an editable field
1106
942
  onFieldChange(item: any, columnKey: string, value: any): void {
1107
- // Update the value directly on the editingRowItem reference
1108
- this.setNestedValue(this.editingRowItem, columnKey, value);
943
+ setNestedValue(this.editingRowItem, columnKey, value);
1109
944
  }
1110
945
 
946
+ // =====================================================
947
+ // Row styling
948
+ // =====================================================
1111
949
 
1112
-
1113
-
1114
-
1115
- // Apply colors to row based on conditions
1116
- private readonly rowColorMap: { [key: string]: string } = {
1117
- primary: 'bg-primary/20 hover:bg-dark-primary/30! dark:hover:bg-dark-primary/20',
1118
- success: 'bg-green-500/20 hover:bg-green-500/30! dark:hover:bg-green-500/20',
1119
- error: ' bg-red-500/20 hover:bg-red-500/30! dark:hover:bg-red-500/20',
1120
- warning: 'bg-yellow-500/20 hover:bg-yellow-500/30! dark:hover:bg-yellow-500/20',
1121
- info: ' bg-blue-500/20 hover:bg-blue-500/30! dark:hover:bg-blue-500/20',
1122
- };
1123
-
1124
- getRowNgClass(row: any): any {
1125
-
1126
- const base: any = {
1127
- 'cursor-pointer': this.hasExpandable(),
1128
- };
1129
-
1130
- const columnWithColor = this.columns.find(c => !!c.rowColorCondition);
1131
- if (!columnWithColor) return base;
1132
-
1133
- const keys = columnWithColor.key.split('.');
1134
- let value = row;
1135
- for (const key of keys) {
1136
- value = value?.[key];
1137
- }
1138
-
1139
- const normalizedValue =
1140
- value === null || value === undefined
1141
- ? 'null'
1142
- : String(value).toUpperCase();
1143
- const semanticClass = columnWithColor.rowColorCondition?.[normalizedValue];
1144
-
1145
- if (semanticClass && this.rowColorMap[semanticClass]) {
1146
- base[this.rowColorMap[semanticClass]] = true;
1147
- }
1148
-
1149
- return base;
950
+ getRowNgClass(row: any): Record<string, boolean> {
951
+ return buildRowNgClass(row, this.columns, this.hasExpandable());
1150
952
  }
1151
953
 
1152
-
1153
-
1154
-
1155
-
1156
-
1157
-
1158
-
1159
-
1160
-
1161
-
1162
-
1163
-
1164
-
1165
-
1166
-
1167
- mobileInnerExpandedRows: Set<any> = new Set();
954
+ // =====================================================
955
+ // Mobile layout
956
+ // =====================================================
1168
957
 
1169
958
  toggleMobileInnerRow(row: any): void {
1170
959
  if (this.mobileInnerExpandedRows.has(row)) {
@@ -1175,106 +964,30 @@ export class JCompleteCrudTableComponent implements OnInit {
1175
964
  }
1176
965
 
1177
966
  getMobileMainTitle(item: any): string {
1178
- const firstTextColumn = this.columns.find(column =>
1179
- column.visible &&
1180
- !column.isDecorator &&
1181
- !column.isDecoratorArray &&
1182
- !column.isLink &&
1183
- !column.isLinkImage &&
1184
- !this.isBoolean(this.getValue(item, column))
1185
- );
1186
-
1187
- return firstTextColumn ? this.getValue(item, firstTextColumn) : 'Registro';
967
+ return getMobileMainTitle(item, this.columns, (row, column) => this.getValue(row, column));
1188
968
  }
1189
969
 
1190
970
  getMobileMainDescription(item: any): string | null {
1191
- const descriptionColumn = this.columns.find(column =>
1192
- column.visible &&
1193
- (column as any).isDescription
1194
- );
1195
-
1196
- if (!descriptionColumn) return null;
1197
-
1198
- const value = this.getValue(item, descriptionColumn);
1199
-
1200
- if (
1201
- value === null ||
1202
- value === undefined ||
1203
- value === '' ||
1204
- value === 'S/N'
1205
- ) {
1206
- return null;
1207
- }
1208
-
1209
- return value;
971
+ return getMobileMainDescription(item, this.columns, (row, column) => this.getValue(row, column));
1210
972
  }
1211
973
 
1212
974
  getMobileHeaderColumns(): TableColumn<any>[] {
1213
- return this.columns
1214
- .filter(column => column.visible)
1215
- .filter(column => column.isDecorator || column.colorDecorator)
1216
- .slice(0, 2);
975
+ return getMobileHeaderColumns(this.columns);
1217
976
  }
1218
977
 
1219
978
  getGroupMobileStudentTitle(student: any): string {
1220
- const firstNameCol = this.columns.find(
1221
- (column) => column.visible && column.key === 'student.firstname_student',
1222
- );
1223
- const lastNameCol = this.columns.find(
1224
- (column) => column.visible && column.key === 'student.lastname_student',
1225
- );
1226
-
1227
- const parts: string[] = [];
1228
-
1229
- if (firstNameCol) {
1230
- const firstName = this.getValue(student, firstNameCol);
1231
- if (firstName) {
1232
- parts.push(firstName);
1233
- }
1234
- }
1235
-
1236
- if (lastNameCol) {
1237
- const lastName = this.getValue(student, lastNameCol);
1238
- if (lastName) {
1239
- parts.push(lastName);
1240
- }
1241
- }
1242
-
1243
- if (parts.length > 0) {
1244
- return parts.join(' ');
1245
- }
1246
-
1247
- const fallbackColumn = this.columns.find(
1248
- (column) =>
1249
- column.visible &&
1250
- !column.isDecorator &&
1251
- !column.isDecoratorArray &&
1252
- !column.isLink &&
1253
- !column.isLinkImage &&
1254
- !this.isBoolean(this.getValue(student, column)),
979
+ return getGroupMobileStudentTitle(student, this.columns, (row, column) =>
980
+ this.getValue(row, column),
1255
981
  );
1256
-
1257
- return fallbackColumn
1258
- ? this.getValue(student, fallbackColumn)
1259
- : 'Estudiante';
1260
982
  }
1261
983
 
1262
984
  getGroupMobileStudentHeaderColumns(student: any): TableColumn<any>[] {
1263
- return this.columns
1264
- .filter(
1265
- (column) =>
1266
- column.visible &&
1267
- (column.isDecorator || column.colorDecorator) &&
1268
- this.getValue(student, column),
1269
- )
1270
- .slice(0, 3);
985
+ return getGroupMobileStudentHeaderColumns(student, this.columns, (row, column) =>
986
+ this.getValue(row, column),
987
+ );
1271
988
  }
1272
989
 
1273
990
  getGroupMobileStudentIndex(groupItem: any, index: number): number {
1274
- const page = groupItem.meta?.page?.currentPage ?? 1;
1275
- const limit = groupItem.meta?.page?.limit ?? 0;
1276
-
1277
- return (page - 1) * limit + index + 1;
991
+ return getGroupMobileStudentIndex(groupItem, index);
1278
992
  }
1279
-
1280
993
  }