tailjng 0.0.55 → 0.0.56

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.
@@ -0,0 +1,652 @@
1
+ import { Component, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { LucideAngularModule } from 'lucide-angular';
5
+ import { Params } from '@angular/router';
6
+ import { animate, state, style, transition, trigger } from '@angular/animations';
7
+ import { EnableBoolean, FilterButton, FilterSelect, JAlertToastService, JConverterCrudService, JGenericCrudService, JIconsService, LoadingState, LoadingStates, OptionsTable, SortDirection, TableColumn } from 'tailjng';
8
+ import { JCompletePaginatorComponent } from '../../paginator/paginator-complete/complete-paginator.component';
9
+ import { JCompleteFilterComponent } from '../../filter/filter-complete/complete-filter.component';
10
+
11
+ @Component({
12
+ selector: 'JCompleteCard',
13
+ standalone: true,
14
+ imports: [CommonModule, FormsModule, JCompletePaginatorComponent, JCompleteFilterComponent, LucideAngularModule],
15
+ templateUrl: './complete-card.component.html',
16
+ styleUrl: './complete-card.component.scss',
17
+ animations: [
18
+ trigger('slideToggle', [
19
+ state('collapsed', style({
20
+ height: '0',
21
+ opacity: 0,
22
+ overflow: 'hidden',
23
+ })),
24
+ state('expanded', style({
25
+ height: '*',
26
+ opacity: 1,
27
+ })),
28
+ transition('collapsed <=> expanded', [
29
+ animate('0.3s ease-in-out')
30
+ ])
31
+ ])
32
+ ]
33
+ })
34
+ export class JCompleteCardComponent implements OnInit {
35
+
36
+ Math = Math;
37
+
38
+ // Loading states
39
+ @Output() dataLoaded = new EventEmitter<void>();
40
+
41
+ loadingStates: LoadingStates = {
42
+ initialLoad: 'idle',
43
+ search: 'idle',
44
+ itemsPerPage: 'idle',
45
+ pagination: 'idle',
46
+ sort: 'idle',
47
+ aditionalButtons: {},
48
+ checked: 'idle',
49
+ action: 'idle',
50
+ };
51
+
52
+ @Input() endpoint!: string;
53
+ mainEndpoint!: string;
54
+ @Input() columns: TableColumn<any>[] = [];
55
+ @Input() defaultFilters: { [key: string]: any } = {};
56
+ @Input() isPaginator = true;
57
+ @Input() isSearch = true;
58
+
59
+ data: any[] = [];
60
+ @Input() itemTemplate!: any;
61
+
62
+ // Expansion
63
+ expandedRows: Set<any> = new Set();
64
+
65
+ // Paginator
66
+ currentPage = 1;
67
+ @Input() itemsPerPageOptions = [12, 36, 60, 120];
68
+ itemsPerPage = this.itemsPerPageOptions[0];
69
+ totalItems = 0;
70
+
71
+ // Sorting
72
+ sortColumn: string | null = null;
73
+ sortDirection: SortDirection = 'none';
74
+ sortingColumn: string | null = null;
75
+
76
+ // Search
77
+ searchQuery = '';
78
+ @Input() searchPlaceholder = 'Buscar...';
79
+
80
+ // Filters
81
+ filters: any = {};
82
+
83
+ // Data filtered and paginated
84
+ displayData: any[] = [];
85
+
86
+ // Pages display
87
+ pages: number[] = [];
88
+
89
+ // Properties for visualization
90
+ get startIndex(): number {
91
+ return (this.currentPage - 1) * this.itemsPerPage;
92
+ }
93
+
94
+ get totalPages(): number {
95
+ return Math.ceil(this.totalItems / this.itemsPerPage);
96
+ }
97
+
98
+ get params(): Params {
99
+ return this.getQueryParams();
100
+ }
101
+
102
+ @Input() filtersButton: FilterButton[] = [];
103
+ @Input() filtersSelect: FilterSelect[] = [];
104
+
105
+ // Check
106
+ @Input() checked: boolean = false;
107
+ @Input() checkedColumn: string = 'id_status';
108
+ @Input() checkedValues: any[][] = [[true], [false]];
109
+ @Input() checkedTitles: string[] = ["Activos", "Inactivos"];
110
+ isChecked!: boolean;
111
+ titleChecked!: string;
112
+
113
+ constructor(
114
+ public readonly iconsService: JIconsService,
115
+ private readonly genericService: JGenericCrudService,
116
+ private readonly alertToastService: JAlertToastService,
117
+ private readonly converterService: JConverterCrudService,
118
+ ) { }
119
+
120
+ ngOnInit() {
121
+ this.mainEndpoint = this.endpoint.split('/')[0] ?? this.endpoint;
122
+ this.isChecked = this.checkedValues[0][0];
123
+ this.titleChecked = this.checkedTitles[0];
124
+ this.columnDefaults();
125
+ this.loadData();
126
+ this.overrideFilterEvents();
127
+ }
128
+
129
+ overrideFilterEvents() {
130
+ for (const filter of this.filtersSelect) {
131
+ if (filter.type === 'dropdown' || filter.type === 'searchable') {
132
+ const key = filter.optionValue ?? 'value';
133
+ const deepKey = filter.deep ? `${filter.deep}.${key}` : key;
134
+
135
+ const originalOnSelected = filter.onSelected;
136
+
137
+ filter.onSelected = (value: any) => {
138
+ const selectedValue = value?.[key] ?? value;
139
+
140
+ if (selectedValue === null || selectedValue === undefined) {
141
+ delete this.filters[deepKey];
142
+ } else {
143
+ this.filters[deepKey] = selectedValue;
144
+ }
145
+
146
+ if (typeof originalOnSelected === 'function') {
147
+ originalOnSelected(value);
148
+ }
149
+
150
+ this.loadData('search');
151
+ };
152
+ }
153
+ }
154
+ }
155
+
156
+ // =====================================================
157
+ // Get data
158
+ // =====================================================
159
+
160
+ // Load data from the server
161
+ loadData(loadingType: keyof LoadingStates = 'initialLoad', onFinally?: () => void) {
162
+ this.setLoadingState(loadingType, 'loading');
163
+
164
+ const params = this.getQueryParams();
165
+
166
+ // Simulating API wait
167
+ // setTimeout(() => {
168
+ this.genericService.findAll<any>({ endpoint: this.endpoint, params }).subscribe({
169
+ next: (response) => {
170
+ this.data = response.data[this.mainEndpoint] ?? [];
171
+
172
+ if (response.meta?.page) {
173
+ this.totalItems = response.meta.page.totalRecords;
174
+ this.currentPage = response.meta.page.currentPage;
175
+ } else {
176
+ this.totalItems = this.data.length;
177
+ }
178
+
179
+ if (response.meta?.sort) {
180
+ this.sortColumn = response.meta.sort.by;
181
+ this.sortDirection = response.meta.sort.order.toLowerCase() as SortDirection;
182
+ }
183
+
184
+ this.updateDisplayData();
185
+ this.generatePagination();
186
+ this.setLoadingState(loadingType, 'success');
187
+ },
188
+ error: (error) => {
189
+ console.error('Error fetching data:', error);
190
+ this.setLoadingState(loadingType, 'error');
191
+ }
192
+ }).add(() => {
193
+ this.dataLoaded.emit();
194
+ if (loadingType === 'sort') {
195
+ this.sortingColumn = null;
196
+ }
197
+
198
+ if (onFinally) {
199
+ onFinally();
200
+ }
201
+ });
202
+ // }, 2000);
203
+ }
204
+
205
+ // Update the data displayed in the table
206
+ updateDisplayData() {
207
+ this.displayData = this.data;
208
+ }
209
+
210
+ // =====================================================
211
+ // Change state in boolean fields
212
+ // =====================================================
213
+
214
+ // MMethod to change the state of a checkbox
215
+ onCheckboxChange(item: any, column: TableColumn<any>) {
216
+ // Get the ID field name based on dataProperty
217
+ const idField = `id_${this.mainEndpoint}`;
218
+
219
+ // Get the record ID
220
+ const recordId = item[idField];
221
+
222
+ // Get the current boolean value
223
+ const currentValue = this.getValue(item, column);
224
+
225
+ const formData: EnableBoolean<any> = {
226
+ key: idField,
227
+ value: currentValue,
228
+ values: [currentValue, !currentValue]
229
+ }
230
+
231
+ // Update status
232
+ this.genericService.toggle<any>({ endpoint: this.mainEndpoint, id: recordId, data: formData }).subscribe({
233
+ next: (response) => {
234
+ item[column.key] = !currentValue;
235
+
236
+ this.alertToastService.AlertToast({
237
+ type: "success",
238
+ title: "Registro actualizado!",
239
+ description: response.message,
240
+ });
241
+ }
242
+ })
243
+ }
244
+
245
+ // Change active or inactive
246
+ checkActiveInactive(isChecked: boolean): void {
247
+ this.isChecked = !isChecked;
248
+
249
+ const index = this.isChecked ? 0 : 1;
250
+ this.titleChecked = this.checkedTitles[index];
251
+
252
+ // ONLY update the id_status property without touching other filters
253
+ this.filters[this.checkedColumn] = this.checkedValues[index];
254
+
255
+ this.filtersSelect = this.filtersSelect.map(filter => {
256
+ if ('optionValue' in filter && filter.optionValue === this.checkedColumn) {
257
+ return {
258
+ ...filter,
259
+ defaultFilters: {
260
+ ...(filter.hasOwnProperty('defaultFilters') ? (filter as any).defaultFilters : {}),
261
+ [this.checkedColumn]: this.filters[this.checkedColumn]
262
+ },
263
+ selected: null
264
+ };
265
+ }
266
+ return filter;
267
+ });
268
+
269
+ this.currentPage = 1;
270
+ this.loadData('checked');
271
+ }
272
+
273
+ // Remove filters
274
+ onClearFilters(buttonType: string): void {
275
+ this.setAditionalButtonLoading(buttonType);
276
+
277
+ this.loadData('initialLoad', () => {
278
+ this.clearAditionalButtonLoading(buttonType);
279
+ });
280
+ }
281
+
282
+ // =====================================================
283
+ // Columns
284
+ // =====================================================
285
+
286
+ // Default column values
287
+ columnDefaults() {
288
+ this.columns.forEach(column => {
289
+ column.visible ??= true;
290
+ column.sortable ??= true;
291
+ column.isSearchable ??= true;
292
+ });
293
+ }
294
+
295
+ // Get the count of visible columns for colspan in empty state
296
+ getVisibleColumnsCount(): number {
297
+ return this.columns.filter(col => col.visible).length;
298
+ }
299
+
300
+ // =====================================================
301
+ // Data Processing
302
+ // =====================================================
303
+
304
+ // Get the cell values to identify a boolean field
305
+ isBoolean(value: any): boolean {
306
+ return typeof value === 'boolean';
307
+ }
308
+
309
+ // MMethod to get the cell values dynamically
310
+ getValue(item: any, column: TableColumn<any>): any {
311
+ let value: any;
312
+
313
+ // If a valueGetter exists, use it directly
314
+ if (typeof column.valueGetter === 'function') {
315
+ value = column.valueGetter(item);
316
+ } else {
317
+ // If not, look for the value by key (with support for nested keys)
318
+ const keys = column.key.split('.');
319
+ value = item;
320
+
321
+ for (const key of keys) {
322
+ if (value != null) {
323
+ value = value[key];
324
+ } else {
325
+ value = null;
326
+ break;
327
+ }
328
+ }
329
+ }
330
+
331
+ // If value is null or undefined, return 'S/N'
332
+ if (value === null || value === undefined) {
333
+ return 'S/N';
334
+ }
335
+
336
+ // Apply formatting according to column configuration
337
+ const formatted = this.converterService.formatData(value, column);
338
+
339
+ // If formatData returns nothing (unformatted value), return the original value
340
+ return formatted ?? value;
341
+ }
342
+
343
+ // =====================================================
344
+ // Search Parameters
345
+ // =====================================================
346
+
347
+ // Get the query parameters for the data request
348
+ getQueryParams(): Params {
349
+ const params: Params = this.genericService.params({
350
+ page: this.currentPage,
351
+ limit: this.itemsPerPage,
352
+ sort: {
353
+ column: this.sortColumn,
354
+ direction: this.sortDirection,
355
+ },
356
+ filters: this.filters,
357
+ defaultFilters: this.defaultFilters,
358
+ });
359
+
360
+ if (this.searchQuery && this.searchQuery.trim() !== '') {
361
+ const baseSearchKeys = this.columns
362
+ .filter(col => col.isSearchable)
363
+ .map(col => col.key);
364
+
365
+ const extraSearchKeys = this.columns
366
+ .flatMap(col => col.extraSearchFields || []);
367
+
368
+ const allSearchKeys = [...baseSearchKeys, ...extraSearchKeys];
369
+
370
+ params['search'] = this.searchQuery;
371
+ params['searchFields'] = allSearchKeys;
372
+ }
373
+
374
+ return params;
375
+ }
376
+
377
+ // =====================================================
378
+ // Sorting
379
+ // =====================================================
380
+
381
+ // Get the row number
382
+ getRowNumber(index: number): number {
383
+ return this.startIndex + index + 1;
384
+ }
385
+
386
+ // Column sorting
387
+ onSort(column: TableColumn<any>) {
388
+ // Prevent multiple simultaneous sort requests
389
+ if (this.isLoading('sort')) {
390
+ return;
391
+ }
392
+
393
+ if (!column.sortable) return;
394
+
395
+ // Guard the column that is being sorted
396
+ this.sortingColumn = column.key;
397
+
398
+ // Check if we are dealing with the same column as the last sort
399
+ const currentSortKey = this.converterService.getSortKey(this.sortColumn);
400
+ const columnSortKey = this.converterService.getSortKey(column.key);
401
+
402
+ // Sort direction logic
403
+ if (currentSortKey === columnSortKey) {
404
+ // Toggle sort direction
405
+ if (this.sortDirection === 'asc') {
406
+ this.sortDirection = 'desc';
407
+ } else if (this.sortDirection === 'desc') {
408
+ this.sortDirection = 'none';
409
+ } else {
410
+ this.sortDirection = 'asc';
411
+ }
412
+ } else {
413
+ // If a new column is selected for sorting, start with ascending order
414
+ this.sortColumn = column.key;
415
+ this.sortDirection = 'asc';
416
+ }
417
+
418
+ this.loadData('sort');
419
+ }
420
+
421
+ // Get the sort key
422
+ getSortKey(value: any): string {
423
+ return this.converterService.getSortKey(value);
424
+ }
425
+
426
+ // Handle the key press event for sorting
427
+ onSortKeyPress(event: KeyboardEvent, column: TableColumn<any>) {
428
+ // If a sort is already in progress, do not allow another
429
+ if (this.isLoading('sort')) {
430
+ return;
431
+ }
432
+
433
+ if (event.key === 'Enter' || event.key === ' ') {
434
+ event.preventDefault();
435
+ this.onSort(column);
436
+ }
437
+ }
438
+
439
+ // =====================================================
440
+ // Search
441
+ // =====================================================
442
+
443
+ // Search functionality
444
+ onSearch() {
445
+ this.currentPage = 1;
446
+ this.loadData('search');
447
+ }
448
+
449
+ // Selector de items por página
450
+ onItemsPerPageChange() {
451
+ this.currentPage = 1;
452
+ this.loadData('itemsPerPage');
453
+ }
454
+
455
+ // =====================================================
456
+ // Pagination
457
+ // =====================================================
458
+
459
+ // Generate pagination numbers
460
+ generatePagination() {
461
+ const totalPages = this.totalPages;
462
+ const currentPage = this.currentPage;
463
+
464
+ // Show 3 pagination numbers
465
+ const maxPagesToShow = 3;
466
+ let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
467
+ let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
468
+
469
+ // Adjust if we're near the end
470
+ if (endPage - startPage + 1 < maxPagesToShow) {
471
+ startPage = Math.max(1, endPage - maxPagesToShow + 1);
472
+ }
473
+
474
+ this.pages = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
475
+ }
476
+
477
+ // Pagination
478
+ handlePageChange(page: number) {
479
+ this.currentPage = page;
480
+ this.loadData('pagination');
481
+ }
482
+
483
+ // =====================================================
484
+ // Options
485
+ // =====================================================
486
+
487
+ // MMethod to handle button click
488
+ onButtonClick(button: OptionsTable, element: any): void {
489
+ // Executes the parent's action if defined
490
+ if (button.clicked) {
491
+ button.clicked(element);
492
+ }
493
+ }
494
+
495
+ // MMethod to get a tooltip
496
+ getTooltip(tooltip: string | ((data?: any) => string), data: any): string {
497
+ if (typeof tooltip === 'function') {
498
+ return tooltip(data);
499
+ }
500
+ return tooltip ?? '';
501
+ }
502
+
503
+ // MMethod to get an icon
504
+ getIcon(icon: ((data?: any) => any), data: any): any {
505
+ if (typeof icon === 'function') {
506
+ return icon(data);
507
+ }
508
+ return icon;
509
+ }
510
+
511
+ // Evaluate if a button is disabled
512
+ getDisabled(option: OptionsTable, data: any): boolean {
513
+ if (typeof option.disabled === 'function') {
514
+ return option.disabled(data);
515
+ }
516
+ return !!option.disabled;
517
+ }
518
+
519
+ // Evaluate if a button is visible
520
+ getIsVisible(option: OptionsTable, data: any): boolean {
521
+ if (typeof option.isVisible === 'function') {
522
+ return option.isVisible(data);
523
+ }
524
+ // If not defined, default is visible
525
+ return option.isVisible !== false;
526
+ }
527
+
528
+ // MMethod to get a button's CSS class
529
+ mergeNgClasses(optionNgClass: ((data?: any) => any), data: any): any {
530
+ const baseClass = {
531
+ 'min-w-auto p-1! pl-2! pr-2!': true
532
+ };
533
+
534
+ let dynamicClass: any = {};
535
+ if (typeof optionNgClass === 'function') {
536
+ dynamicClass = optionNgClass(data);
537
+ } else {
538
+ dynamicClass = optionNgClass ?? {};
539
+ }
540
+
541
+ return {
542
+ ...baseClass,
543
+ ...(typeof dynamicClass === 'string' ? { [dynamicClass]: true } : dynamicClass)
544
+ };
545
+ }
546
+
547
+ // ====================================================
548
+ // Loading parameters
549
+ // =====================================================
550
+
551
+ // MMethod to check if a specific state is loading
552
+ isLoading(state: keyof LoadingStates): boolean {
553
+ return this.loadingStates[state] === 'loading';
554
+ }
555
+
556
+ // MMethod to check if any state is loading
557
+ isAnyLoading(): boolean {
558
+ return Object.values(this.loadingStates).some(state => state === 'loading');
559
+ }
560
+
561
+ // Method to update a loading state
562
+ setLoadingState(state: keyof LoadingStates, value: LoadingState | { [buttonType: string]: LoadingState }): void {
563
+ if (state === 'aditionalButtons') {
564
+ if (typeof value === 'string') {
565
+ // Prevent assigning a string directly if an object is expected
566
+ console.warn(`No puedes asignar '${value}' directamente a aditionalButtons. Usa setAditionalButtonLoading en su lugar.`);
567
+ } else {
568
+ this.loadingStates.aditionalButtons = value;
569
+ }
570
+ } else {
571
+ this.loadingStates[state] = value as LoadingState;
572
+ }
573
+ }
574
+
575
+ // Activate loading
576
+ setAditionalButtonLoading(buttonType: string, id?: number | string): void {
577
+ const key = id !== undefined ? `${buttonType}_${id}` : buttonType;
578
+ this.loadingStates.aditionalButtons[key] = 'loading';
579
+ }
580
+
581
+ // Clear loading
582
+ clearAditionalButtonLoading(buttonType: string, id?: number | string): void {
583
+ const key = id !== undefined ? `${buttonType}_${id}` : buttonType;
584
+ this.loadingStates.aditionalButtons[key] = 'idle';
585
+ }
586
+
587
+ // Verify loading
588
+ isAditionalButtonLoading(buttonType: string, id?: number | string): boolean {
589
+ const key = id !== undefined ? `${buttonType}_${id}` : buttonType;
590
+ return this.loadingStates.aditionalButtons[key] === 'loading';
591
+ }
592
+
593
+ // ==================================================
594
+ // HTML
595
+ // ==================================================
596
+
597
+ getTemplate(row: any): string {
598
+ const templateColumn = this.columns.find(col => typeof col.expandTemplate === 'function');
599
+ return templateColumn?.expandTemplate?.(row) ?? '';
600
+ }
601
+
602
+ // ==================================================
603
+ // Expand rows
604
+ // ==================================================
605
+
606
+ @Input() expandTemplate?: TemplateRef<any>;
607
+ @Input() onExpandData?: (item: any) => Promise<any>;
608
+
609
+ expandedItems: { [key: string]: boolean } = {};
610
+ expandData: { [key: string]: any } = {};
611
+ expandLoading: { [key: string]: boolean } = {};
612
+
613
+ // MMethod to expand or collapse a row
614
+ toggleExpanded(item: any) {
615
+ const id = item?.id || item?.id_voucher || item?.id_card;
616
+ const isOpen = this.expandedItems[id];
617
+
618
+ if (!isOpen) {
619
+ this.expandLoading[id] = true;
620
+
621
+ this.expandedItems[id] = true;
622
+ if (this.onExpandData) {
623
+ this.onExpandData(item).then(data => {
624
+ this.expandData[id] = data;
625
+ this.expandLoading[id] = false;
626
+ });
627
+ }
628
+ } else {
629
+ this.expandedItems[id] = false;
630
+ }
631
+ }
632
+
633
+ isExpanded(item: any): boolean {
634
+ const id = item?.id || item?.id_voucher || item?.id_card;
635
+ return !!this.expandedItems[id];
636
+ }
637
+
638
+ getExpandData(item: any): any {
639
+ const id = item?.id || item?.id_voucher || item?.id_card;
640
+ return this.expandData[id];
641
+ }
642
+
643
+ isLoadingExpand(item: any): boolean {
644
+ const id = item?.id || item?.id_voucher || item?.id_card;
645
+ return !!this.expandLoading[id];
646
+ }
647
+
648
+ trackById(index: number, item: any): any {
649
+ const id = `${item['id_' + this.mainEndpoint]}-${index}`;
650
+ return id ? id : index;
651
+ }
652
+ }