ng-firebase-table-kxp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,555 @@
1
+ <div *ngIf="data" class="card-body">
2
+ <div class="flex flex-col justify-between gap-6">
3
+ <!-- UNIFIED CONTROL PANEL: FILTERS, SORT & ACTIONS -->
4
+ <div
5
+ class="rounded-xl border border-gray-200 bg-white p-4 shadow-lg"
6
+ *ngIf="
7
+ data.pagination === true &&
8
+ (dropdownItems.length > 0 ||
9
+ sortableDropdownItems.length > 0 ||
10
+ data.actionButton)
11
+ "
12
+ >
13
+ <!-- PANEL HEADER: Title, Custom Action, and Global Actions -->
14
+ <div
15
+ class="mb-4 flex flex-col items-start justify-between gap-4 border-b-2 border-gray-200 pb-4 md:flex-row md:items-center"
16
+ >
17
+ <!-- Left Side: Title & Main Action Button -->
18
+ <div class="flex flex-wrap items-center gap-4">
19
+ <div class="flex items-center gap-2">
20
+ <i class="fa fa-filter text-xl text-blue-500"></i>
21
+ <span class="text-lg font-semibold text-gray-700"
22
+ >Filtros e Ações</span
23
+ >
24
+ </div>
25
+ <button
26
+ *ngIf="data.actionButton && data.actionButton.condition"
27
+ [ngClass]="
28
+ (data.actionButton.colorClass || 'bg-blue-500') +
29
+ ' flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'
30
+ "
31
+ [routerLink]="data.actionButton.routerLink"
32
+ (click)="
33
+ data.actionButton.method ? data.actionButton.method($event) : null
34
+ "
35
+ >
36
+ <i
37
+ *ngIf="data.actionButton.icon"
38
+ [class]="data.actionButton.icon"
39
+ ></i>
40
+ {{ data.actionButton.label }}
41
+ </button>
42
+ </div>
43
+
44
+ <!-- Right Side: Search, Reset, Export -->
45
+ <div
46
+ class="flex flex-wrap gap-3"
47
+ *ngIf="
48
+ this.hasFilterableColumn === true || this.hasSortableColumn === true
49
+ "
50
+ >
51
+ <button
52
+ (click)="search()"
53
+ type="button"
54
+ class="flex items-center gap-2 rounded-lg bg-green-600 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-green-700"
55
+ matTooltip="Aplicar filtros"
56
+ >
57
+ <i class="fa fa-search"></i>
58
+ Pesquisar
59
+ </button>
60
+
61
+ <button
62
+ (click)="resetFilter()"
63
+ class="flex items-center gap-2 rounded-lg bg-red-500 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-red-600"
64
+ matTooltip="Limpar filtros"
65
+ >
66
+ <i class="fas fa-redo-alt"></i>
67
+ Resetar
68
+ </button>
69
+
70
+ <button
71
+ *ngIf="data.download !== false && downloadTable"
72
+ class="flex items-center gap-2 rounded-lg bg-orange-500 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-orange-600"
73
+ matTooltipPosition="above"
74
+ matTooltip="Exportar Tabela"
75
+ [disabled]="
76
+ this.dataSource && this.dataSource.filteredData.length <= 0
77
+ "
78
+ (click)="
79
+ $any(arrange) && downloadTable !== undefined
80
+ ? downloadTable($any(arrange), data.conditions || [])
81
+ : null
82
+ "
83
+ >
84
+ <i class="fa fa-download"></i>
85
+ Exportar
86
+ </button>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- FILTERS CONTENT (WITH REFINEMENTS) -->
91
+ <div class="mb-4 space-y-3" *ngIf="filtersForm.controls.length > 0">
92
+ <div
93
+ [formGroup]="$any(filterGroup)"
94
+ *ngFor="let filterGroup of filtersForm.controls; let i = index"
95
+ class="flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 p-2"
96
+ >
97
+ <!-- FILTER TYPE SELECTOR -->
98
+ <div class="min-w-[200px] flex-1" *ngIf="dropdownItems.length > 0">
99
+ <mat-form-field appearance="outline" class="w-full">
100
+ <mat-label>Tipo de filtro</mat-label>
101
+ <mat-select
102
+ placeholder="Selecione o tipo..."
103
+ formControlName="selectFilter"
104
+ (selectionChange)="onSelectFilterChange()"
105
+ >
106
+ <mat-option *ngFor="let item of dropdownItems" [value]="item">
107
+ <div class="flex items-center gap-2">
108
+ <i
109
+ [class]="item.icon || 'fa fa-filter'"
110
+ class="text-sm text-blue-500"
111
+ ></i>
112
+ <span>{{ item.title }}</span>
113
+ </div>
114
+ </mat-option>
115
+ </mat-select>
116
+ </mat-form-field>
117
+ </div>
118
+
119
+ <!-- TEXT FILTER -->
120
+ <div
121
+ class="min-w-[200px] flex-1"
122
+ *ngIf="
123
+ $any(filterGroup).get('selectFilter')?.value?.arrange === 'filter'
124
+ "
125
+ >
126
+ <mat-form-field appearance="outline" class="w-full">
127
+ <mat-label class="flex items-center gap-2">
128
+ <i class="fa fa-search text-gray-400"></i>
129
+ <span>{{
130
+ $any(filterGroup).get("selectFilter")?.value?.title ||
131
+ "Filtrar"
132
+ }}</span>
133
+ </mat-label>
134
+ <input
135
+ (keyup.enter)="search()"
136
+ formControlName="typeFilter"
137
+ matInput
138
+ placeholder="Digite para filtrar..."
139
+ #input
140
+ />
141
+ </mat-form-field>
142
+ </div>
143
+
144
+ <!-- DROPDOWN FILTER -->
145
+ <div
146
+ class="min-w-[200px] flex-1"
147
+ *ngIf="
148
+ $any(filterGroup).get('selectFilter')?.value &&
149
+ $any(filterGroup)
150
+ .get('selectFilter')
151
+ ?.value.hasOwnProperty('items')
152
+ "
153
+ >
154
+ <mat-form-field appearance="outline" class="w-full">
155
+ <mat-label>{{
156
+ $any(filterGroup).get("selectFilter")?.value?.title ||
157
+ "Selecione"
158
+ }}</mat-label>
159
+ <mat-select
160
+ placeholder="Selecione..."
161
+ formControlName="selectItem"
162
+ multiple
163
+ >
164
+ <mat-option
165
+ *ngFor="
166
+ let item of $any(filterGroup).get('selectFilter')?.value
167
+ .items
168
+ "
169
+ [value]="item"
170
+ >
171
+ {{ item.label }}
172
+ </mat-option>
173
+ </mat-select>
174
+ </mat-form-field>
175
+ </div>
176
+
177
+ <!-- DATE FILTER -->
178
+ <div
179
+ class="min-w-[340px] flex-auto"
180
+ *ngIf="
181
+ $any(filterGroup).get('selectFilter')?.value?.arrange ===
182
+ 'filterByDate'
183
+ "
184
+ >
185
+ <div
186
+ class="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center"
187
+ >
188
+ <mat-form-field appearance="outline" class="flex-1">
189
+ <mat-label class="flex items-center gap-2">
190
+ <i class="fa fa-calendar text-gray-400"></i>
191
+ <span>Data Inicial</span>
192
+ </mat-label>
193
+ <input
194
+ matInput
195
+ (keyup.enter)="search()"
196
+ formControlName="initialDate"
197
+ placeholder="DD/MM/AAAA"
198
+ maxlength="10"
199
+ />
200
+ </mat-form-field>
201
+
202
+ <mat-form-field appearance="outline" class="flex-1">
203
+ <mat-label class="flex items-center gap-2">
204
+ <i class="fa fa-calendar text-gray-400"></i>
205
+ <span>Data Final</span>
206
+ </mat-label>
207
+ <input
208
+ (keyup.enter)="search()"
209
+ matInput
210
+ formControlName="finalDate"
211
+ placeholder="DD/MM/AAAA"
212
+ maxlength="10"
213
+ />
214
+ </mat-form-field>
215
+ </div>
216
+ </div>
217
+
218
+ <!-- REMOVE FILTER BUTTON -->
219
+ <div *ngIf="filtersForm.length > 1" class="ml-auto flex-shrink-0">
220
+ <button
221
+ (click)="removeFilter(i)"
222
+ class="flex h-10 w-10 items-center justify-center rounded-full transition-colors duration-300 hover:bg-red-100"
223
+ matTooltip="Remover filtro"
224
+ >
225
+ <i class="fa fa-trash text-red-500 hover:text-red-600"></i>
226
+ </button>
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ <!-- PANEL FOOTER: Add Filter & Sort -->
232
+ <div
233
+ class="-mb-2 flex flex-col items-center justify-between gap-4 border-t border-gray-200 pt-4 sm:flex-row"
234
+ >
235
+ <!-- Add Filter Button -->
236
+ <div *ngIf="dropdownItems.length > 0">
237
+ <button
238
+ (click)="addFilter()"
239
+ class="transform rounded-full border-2 border-blue-300 bg-blue-50 px-6 py-2 text-sm font-medium text-blue-600 transition-all duration-300 hover:-translate-y-0.5 hover:border-blue-400 hover:bg-blue-100 hover:shadow-md"
240
+ matTooltip="Adicionar novo filtro"
241
+ >
242
+ <i class="fa fa-plus mr-2"></i>
243
+ Adicionar Filtro
244
+ </button>
245
+ </div>
246
+
247
+ <!-- Sort Dropdown -->
248
+ <div
249
+ class="w-full sm:w-auto sm:min-w-[250px]"
250
+ *ngIf="sortableDropdownItems.length > 0"
251
+ >
252
+ <mat-form-field appearance="outline" class="w-full">
253
+ <mat-label>Ordenar por</mat-label>
254
+ <mat-select placeholder="Selecione..." [formControl]="selectSort">
255
+ <mat-option
256
+ *ngFor="let item of sortableDropdownItems"
257
+ [value]="item"
258
+ >
259
+ <div class="flex items-center gap-2">
260
+ <i class="fa fa-sort-alpha-down text-cyan-600"></i>
261
+ <span>{{ item.title }}</span>
262
+ </div>
263
+ </mat-option>
264
+ </mat-select>
265
+ </mat-form-field>
266
+ </div>
267
+ </div>
268
+ </div>
269
+
270
+ <!-- SIMPLE SEARCH (for non-paginated tables) -->
271
+ <div
272
+ class="rounded-xl border border-gray-200 bg-white p-4 shadow-lg"
273
+ *ngIf="data.pagination === false"
274
+ >
275
+ <mat-form-field appearance="outline" class="w-full">
276
+ <mat-label class="flex items-center gap-2">
277
+ <i class="fa fa-search text-blue-500"></i>
278
+ Buscar
279
+ </mat-label>
280
+ <input
281
+ matInput
282
+ (keyup.enter)="search()"
283
+ (keyup)="applyFilter(filterInput.value)"
284
+ placeholder="Digite para filtrar..."
285
+ #filterInput
286
+ />
287
+ <mat-icon matSuffix class="text-gray-500">search</mat-icon>
288
+ </mat-form-field>
289
+ <button
290
+ *ngIf="data.actionButton"
291
+ [ngClass]="
292
+ (data.actionButton.colorClass || 'bg-blue-500') +
293
+ ' float-right flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'
294
+ "
295
+ [routerLink]="data.actionButton.routerLink"
296
+ (click)="
297
+ data.actionButton.method ? data.actionButton.method($event) : null
298
+ "
299
+ >
300
+ <i *ngIf="data.actionButton.icon" [class]="data.actionButton.icon"></i>
301
+ {{ data.actionButton.label }}
302
+ </button>
303
+ </div>
304
+
305
+ <div class="flex flex-col">
306
+ <div
307
+ class="mx-auto flex flex-col"
308
+ *ngIf="data.tabs && data.tabs.tabsData && data.tabs.tabsData.length > 0"
309
+ >
310
+ <!-- Calcular quantos grupos de 6 tabs existem -->
311
+ <ng-container
312
+ *ngFor="
313
+ let groupIndex of getTabGroups(data.tabs.tabsData);
314
+ let i = index
315
+ "
316
+ >
317
+ <div class="mx-auto flex flex-row">
318
+ <ng-container
319
+ *ngFor="
320
+ let tab of getTabGroup(data.tabs.tabsData, groupIndex);
321
+ let j = index
322
+ "
323
+ >
324
+ <button
325
+ class="border-2 border-gray-300 bg-gray-200 px-4 py-2 font-medium transition hover:brightness-95"
326
+ [ngClass]="
327
+ isTabSelected(getRealTabIndex(i, j))
328
+ ? 'border-b-0 brightness-110'
329
+ : ''
330
+ "
331
+ (click)="onTableSelected(i, j)"
332
+ >
333
+ {{ tab.label }}
334
+ <span
335
+ *ngIf="tab.counter !== undefined"
336
+ class="ml-2 text-xs font-bold"
337
+ [ngClass]="tab.counterClass"
338
+ >
339
+ {{ tab.counter }}
340
+ </span>
341
+ </button>
342
+ </ng-container>
343
+ </div>
344
+ </ng-container>
345
+ </div>
346
+ <div class="mat-elevation-z8 w-full overflow-x-auto rounded-xl">
347
+ <table
348
+ mat-table
349
+ [dataSource]="dataSource"
350
+ matSort
351
+ #sort="matSort"
352
+ matSortActive="createdAt"
353
+ matSortDirection="desc"
354
+ >
355
+ <ng-container
356
+ *ngFor="let col of data.displayedColumns"
357
+ matColumnDef="{{ col.property }}"
358
+ >
359
+ <ng-container *matHeaderCellDef>
360
+ <!-- IF THE COLUMN IS NOT SORTABLE, THEN DON'T SHOW THE SORT BUTTONS -->
361
+ <th
362
+ *ngIf="!col.isSortable || data.pagination === true"
363
+ mat-header-cell
364
+ [ngClass]="
365
+ (data?.color?.bg ? ' ' + $any(data.color).bg : '') +
366
+ (data?.color?.text ? ' ' + $any(data.color).text : '')
367
+ "
368
+ >
369
+ {{ col.title }}
370
+ </th>
371
+ <!-- IF THE COLUMN IS SORTABLE, THEN SHOW THE SORT BUTTONS -->
372
+ <th
373
+ *ngIf="col.isSortable && data.pagination === false"
374
+ mat-header-cell
375
+ mat-sort-header
376
+ [ngClass]="
377
+ (data?.color?.bg ? ' ' + $any(data.color).bg : '') +
378
+ (data?.color?.text ? ' ' + $any(data.color).text : '')
379
+ "
380
+ >
381
+ {{ col.title }}
382
+ </th>
383
+ <td
384
+ mat-cell
385
+ *matCellDef="let row"
386
+ (click)="col.method ? col.method(row) : null"
387
+ (mouseenter)="onCellMouseEnter($event, row, col)"
388
+ (mouseleave)="onCellMouseLeave()"
389
+ (mousemove)="onCellMouseMove($event)"
390
+ >
391
+ <!-- CHECK IF THE COLUMN MUST BE DISPLAYED -->
392
+ <span *ngIf="!col.image && !col.iconClass && !col.method">
393
+ <ng-container>
394
+ <span
395
+ *ngIf="
396
+ col.charLimit &&
397
+ row[col.property] &&
398
+ row[col.property].length > col.charLimit;
399
+ else withinLimit
400
+ "
401
+ >
402
+ <a
403
+ *ngIf="col.hasLink === true"
404
+ [href]="row[col.property]"
405
+ target="_blank"
406
+ >
407
+ {{ getDisplayValue(col, row) }}
408
+ </a>
409
+ <a
410
+ *ngIf="col.hasLink && isString(col.hasLink)"
411
+ [href]="col.hasLink"
412
+ target="_blank"
413
+ >
414
+ {{ getDisplayValue(col, row) }}
415
+ </a>
416
+ <span
417
+ *ngIf="col.hasLink !== true && !isString(col.hasLink)"
418
+ >
419
+ {{ getDisplayValue(col, row) }}
420
+ </span>
421
+ </span>
422
+ </ng-container>
423
+ <ng-template #withinLimit>
424
+ <a
425
+ *ngIf="col.hasLink === true"
426
+ [href]="row[col.property]"
427
+ target="_blank"
428
+ >
429
+ {{ getDisplayValue(col, row, true) }}
430
+ </a>
431
+ <a
432
+ *ngIf="col.hasLink && isString(col.hasLink)"
433
+ [href]="col.hasLink"
434
+ target="_blank"
435
+ >
436
+ {{ getDisplayValue(col, row, true) }}
437
+ </a>
438
+ <span
439
+ *ngIf="col.hasLink !== true && !isString(col.hasLink)"
440
+ >
441
+ {{ getDisplayValue(col, row, true) }}
442
+ </span>
443
+ </ng-template>
444
+ </span>
445
+ <!------------------- IMAGE ------------------>
446
+ <img
447
+ *ngIf="
448
+ col.image && col.image.path && !col.iconClass && !col.method
449
+ "
450
+ [src]="col.image.path + '/' + row[col.property]"
451
+ [ngClass]="col.image.class"
452
+ alt="Imagem"
453
+ />
454
+ <img
455
+ *ngIf="
456
+ col.image && col.image.url && !col.iconClass && !col.method
457
+ "
458
+ [src]="row[col.property]"
459
+ [ngClass]="col.image.class"
460
+ alt="Imagem"
461
+ />
462
+ <ng-container *ngIf="col.iconClass">
463
+ <button
464
+ *ngFor="let iconClass of col.iconClass"
465
+ (click)="
466
+ iconClass.buttonMethod
467
+ ? iconClass.buttonMethod(row, $event)
468
+ : $event.stopPropagation()
469
+ "
470
+ >
471
+ <span
472
+ [ngClass]="iconClass.class"
473
+ *ngIf="
474
+ iconClass.condition === undefined ||
475
+ (iconClass.condition !== undefined &&
476
+ $any(iconClass.condition)(row))
477
+ "
478
+ >{{ iconClass.text }}</span
479
+ >
480
+ </button>
481
+ </ng-container>
482
+ </td>
483
+ </ng-container>
484
+ </ng-container>
485
+
486
+ <tr mat-header-row *matHeaderRowDef="columnProperties"></tr>
487
+ <tr
488
+ [ngClass]="{
489
+ 'example-element-row': data.isNotClickable === true,
490
+ 'example-element-row cursor-pointer': !data.isNotClickable
491
+ }"
492
+ mat-row
493
+ *matRowDef="let row; columns: columnProperties"
494
+ (click)="goToDetails(row)"
495
+ ></tr>
496
+
497
+ <!-- ROW SHOWN WHEN THERE IS NO MATCHING DATA. -->
498
+ <tr class="mat-row" *matNoDataRow>
499
+ <td *ngIf="!isLoading" class="mat-cell p-4" colspan="4">
500
+ Nenhum resultado encontrado para a busca
501
+ </td>
502
+ </tr>
503
+ </table>
504
+
505
+ <div class="flex justify-center" *ngIf="isLoading">
506
+ <mat-spinner></mat-spinner>
507
+ </div>
508
+
509
+ <div class="paginator-container">
510
+ <mat-paginator
511
+ #paginator
512
+ [pageSizeOptions]="[25, 50, 100]"
513
+ [pageSize]="pageSize"
514
+ [length]="totalItems"
515
+ showFirstLastButtons
516
+ aria-label="Select page of periodic elements"
517
+ (page)="onPageChange($event)"
518
+ [ngClass]="{
519
+ 'hide-length':
520
+ ['filter', 'filterByDate', 'equals'].includes(
521
+ this.currentArrange
522
+ ) || this.data.filterFn,
523
+ 'hide-next-button': !hasNextPage && data.pagination === true,
524
+ 'hide-last-button':
525
+ (!hasNextPage && data.pagination === true) || this.data.filterFn
526
+ }"
527
+ >
528
+ </mat-paginator>
529
+ <div
530
+ *ngIf="
531
+ !isLoading &&
532
+ dataSource?.data &&
533
+ dataSource.data.length > 0 &&
534
+ data?.filterFn
535
+ "
536
+ class="page-number-display"
537
+ >
538
+ {{ currentPageNumber }}
539
+ </div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ </div>
544
+
545
+ <!-- TOOLTIP PERSONALIZADO -->
546
+ <div
547
+ *ngIf="showTooltip"
548
+ class="fixed z-50 max-w-md break-words rounded-lg bg-gray-800 px-3 py-2 text-sm text-white shadow-lg"
549
+ [style.left.px]="tooltipPosition.x"
550
+ [style.top.px]="tooltipPosition.y"
551
+ [style.pointer-events]="'none'"
552
+ >
553
+ {{ tooltipContent }}
554
+ </div>
555
+ </div>
@@ -0,0 +1,22 @@
1
+ ::ng-deep .hide-length .mat-mdc-paginator-range-label {
2
+ display: none;
3
+ }
4
+
5
+ ::ng-deep .hide-next-button .mat-mdc-tooltip-trigger.mat-mdc-paginator-navigation-next.mdc-icon-button.mat-mdc-icon-button.mat-unthemed.mat-mdc-button-base{
6
+ visibility: hidden;
7
+ }
8
+
9
+ ::ng-deep .hide-next-button .mat-mdc-tooltip-trigger.mat-mdc-paginator-navigation-last.mdc-icon-button.mat-mdc-icon-button.mat-unthemed.mat-mdc-button-base.ng-star-inserted {
10
+ visibility: hidden;
11
+ }
12
+
13
+ ::ng-deep .mat-mdc-text-field-wrapper.mdc-text-field.ng-tns-c162-1.mdc-text-field--filled{
14
+ width: 25dvw;
15
+ }
16
+
17
+ ::ng-deep .custom-filter .mat-mdc-text-field-wrapper {
18
+ width: 20dvw;
19
+ max-width: 300px;
20
+ }
21
+
22
+
@@ -0,0 +1,24 @@
1
+ import {ComponentFixture, TestBed} from '@angular/core/testing';
2
+
3
+ import {TableComponent} from './table.component';
4
+
5
+ describe('TableComponent', () => {
6
+ let component: TableComponent;
7
+ let fixture: ComponentFixture<TableComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [TableComponent]
12
+ }).compileComponents();
13
+
14
+ fixture = TestBed.createComponent(TableComponent);
15
+ component = fixture.componentInstance;
16
+ fixture.detectChanges();
17
+ });
18
+
19
+ it('should create', () => {
20
+ expect(component).toBeTruthy();
21
+ });
22
+ });
23
+
24
+