ng-firebase-table-kxp 1.0.3 → 1.0.4

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.
@@ -33,1763 +33,1755 @@ import * as i1 from '@angular/fire/compat/firestore';
33
33
  import * as i3 from 'ngx-toastr';
34
34
  import * as i12 from '@angular/material/core';
35
35
 
36
- class FirebaseTableKxpLibComponent {
37
- }
38
- FirebaseTableKxpLibComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
39
- FirebaseTableKxpLibComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: FirebaseTableKxpLibComponent, selector: "lib-firebase-table-kxp-lib", ngImport: i0, template: `
40
- <p>
41
- firebase-table-kxp-lib works!
42
- </p>
43
- `, isInline: true });
44
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibComponent, decorators: [{
45
- type: Component,
46
- args: [{ selector: 'lib-firebase-table-kxp-lib', template: `
47
- <p>
48
- firebase-table-kxp-lib works!
49
- </p>
50
- ` }]
36
+ class NgFirebaseTableKxpComponent {
37
+ }
38
+ NgFirebaseTableKxpComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
39
+ NgFirebaseTableKxpComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: NgFirebaseTableKxpComponent, selector: "lib-ng-firebase-table-kxp", ngImport: i0, template: ` <p>ng-firebase-table-kxp works!</p> `, isInline: true });
40
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpComponent, decorators: [{
41
+ type: Component,
42
+ args: [{ selector: 'lib-ng-firebase-table-kxp', template: ` <p>ng-firebase-table-kxp works!</p> ` }]
51
43
  }] });
52
44
 
53
- class TableService {
54
- constructor(ngFire, dialog, toastr) {
55
- this.ngFire = ngFire;
56
- this.dialog = dialog;
57
- this.toastr = toastr;
58
- this.operators = {
59
- '==': (a, b) => a === b,
60
- '!=': (a, b) => a !== b,
61
- '>': (a, b) => a > b,
62
- '<': (a, b) => a < b,
63
- '>=': (a, b) => a >= b,
64
- '<=': (a, b) => a <= b,
65
- in: (a, b) => Array.isArray(b) && b.includes(a),
66
- 'not-in': (a, b) => Array.isArray(b) && !b.includes(a),
67
- 'array-contains': (a, b) => Array.isArray(a) && a.includes(b),
68
- 'array-contains-any': (a, b) => Array.isArray(a) &&
69
- Array.isArray(b) &&
70
- b.some((item) => a.includes(item)),
71
- includes: (a, b) => a.includes(b), // Para strings ou arrays
72
- };
73
- }
74
- async getItems(collection) {
75
- try {
76
- const querySnapshot = await collection.get();
77
- return querySnapshot.docs.map((doc) => {
78
- return { ...doc.data(), id: doc.id };
79
- });
80
- }
81
- catch (error) {
82
- console.warn('Collection não encontrada:', error);
83
- return [];
84
- }
85
- }
86
- async executeQuery(params) {
87
- if (params.filterFn) {
88
- // Lógica com filtro no cliente (filterFn)
89
- const BATCH_FETCH_SIZE = params.batchSize;
90
- const GOAL_SIZE = params.batchSize + 1;
91
- if (params.navigation === 'forward' || params.navigation === 'reload') {
92
- if (params.navigation === 'reload' && params.doc) {
93
- params.doc.lastDoc = null;
94
- }
95
- let lastDocCursor = params.doc ? params.doc.lastDoc : null;
96
- let pageResults = [];
97
- let allFetchedDocs = [];
98
- let hasMoreDocsInDb = true;
99
- while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
100
- let query = this.ngFire.collection(params.collection).ref;
101
- query = this.applyFilters(query, params.arrange, params.conditions);
102
- if (lastDocCursor) {
103
- query = query.startAfter(lastDocCursor);
104
- }
105
- query = query.limit(BATCH_FETCH_SIZE);
106
- const snapshot = await query.get();
107
- if (snapshot.empty) {
108
- hasMoreDocsInDb = false;
109
- break;
110
- }
111
- lastDocCursor = snapshot.docs[snapshot.docs.length - 1];
112
- allFetchedDocs.push(...snapshot.docs);
113
- const batchUsers = snapshot.docs
114
- .map((doc) => ({
115
- id: doc.id,
116
- ...doc.data(),
117
- }))
118
- .filter(params.filterFn);
119
- pageResults.push(...batchUsers);
120
- if (snapshot.size < BATCH_FETCH_SIZE) {
121
- hasMoreDocsInDb = false;
122
- }
123
- }
124
- const hasNextPage = pageResults.length > params.batchSize;
125
- const finalItems = pageResults.slice(0, params.batchSize);
126
- const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
127
- const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
128
- return {
129
- items: finalItems,
130
- filterLength: null,
131
- firstDoc: firstDocOfPage,
132
- lastDoc: lastDocOfPage,
133
- hasNextPage: hasNextPage,
134
- hasPreviousPage: !!(params.doc && params.doc.lastDoc) &&
135
- params.navigation !== 'reload',
136
- currentClientPageIndex: undefined,
137
- };
138
- }
139
- // Lógica para trás (backward)
140
- else if (params.navigation === 'backward') {
141
- if (!params.doc || !params.doc.firstDoc) {
142
- return {
143
- items: [],
144
- filterLength: null,
145
- firstDoc: null,
146
- lastDoc: null,
147
- hasNextPage: true,
148
- hasPreviousPage: false,
149
- currentClientPageIndex: undefined,
150
- };
151
- }
152
- let pageResults = [];
153
- let allFetchedDocs = [];
154
- let hasMoreDocsInDb = true;
155
- let boundaryDoc = params.doc.firstDoc;
156
- while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
157
- let query = this.ngFire.collection(params.collection).ref;
158
- query = this.applyFilters(query, params.arrange, params.conditions);
159
- query = query.endBefore(boundaryDoc);
160
- query = query.limitToLast(BATCH_FETCH_SIZE);
161
- const snapshot = await query.get();
162
- if (snapshot.empty) {
163
- hasMoreDocsInDb = false;
164
- break;
165
- }
166
- boundaryDoc = snapshot.docs[0];
167
- allFetchedDocs = [...snapshot.docs, ...allFetchedDocs];
168
- const batchUsers = snapshot.docs
169
- .map((doc) => ({
170
- id: doc.id,
171
- ...doc.data(),
172
- }))
173
- .filter(params.filterFn);
174
- pageResults = [...batchUsers, ...pageResults];
175
- if (snapshot.size < BATCH_FETCH_SIZE) {
176
- hasMoreDocsInDb = false;
177
- }
178
- }
179
- const finalItems = pageResults.slice(0, params.batchSize);
180
- const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
181
- const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
182
- return {
183
- items: finalItems,
184
- filterLength: null,
185
- firstDoc: firstDocOfPage,
186
- lastDoc: lastDocOfPage,
187
- hasNextPage: true,
188
- currentClientPageIndex: undefined,
189
- };
190
- }
191
- }
192
- else {
193
- let items = [];
194
- let docs = [];
195
- let hasNextPage = false;
196
- let filterLength = null;
197
- let query = this.ngFire.collection(params.collection).ref;
198
- if (params.conditions) {
199
- params.conditions.forEach((c) => {
200
- if (c.operator === '!=') {
201
- query = query.orderBy(c.firestoreProperty);
202
- }
203
- });
204
- }
205
- query = this.applyFilters(query, params.arrange, params.conditions);
206
- if (params.navigation === 'reload') {
207
- query = query.limit(params.batchSize + 1);
208
- if (params.doc && params.doc.firstDoc) {
209
- query = query.startAt(params.doc.firstDoc);
210
- }
211
- }
212
- else if (params.navigation === 'forward') {
213
- query = query.limit(params.batchSize + 1);
214
- if (params.doc && params.doc.lastDoc) {
215
- query = query.startAfter(params.doc.lastDoc);
216
- }
217
- }
218
- else {
219
- // backward
220
- query = query.limitToLast(params.batchSize + 1);
221
- if (params.doc && params.doc.firstDoc) {
222
- query = query.endBefore(params.doc.firstDoc);
223
- }
224
- }
225
- const itemCol = await query.get();
226
- itemCol.docs.forEach((doc) => docs.push(doc));
227
- const itemPromises = docs.map(async (item) => {
228
- const itemData = item.data();
229
- items.push({ id: item.id, ...itemData });
230
- });
231
- let lastDoc = docs[docs.length - 1] || null;
232
- let firstDoc = docs[0];
233
- if ((items.length > params.batchSize && params.navigation === 'forward') ||
234
- (params.navigation === 'reload' && items.length > params.batchSize)) {
235
- lastDoc = docs[docs.length - 2] || null;
236
- items.pop();
237
- hasNextPage = true;
238
- }
239
- if (items.length > params.batchSize && params.navigation === 'backward') {
240
- firstDoc = docs[1];
241
- items.shift();
242
- hasNextPage = true;
243
- }
244
- await Promise.all(itemPromises);
245
- return {
246
- items,
247
- filterLength,
248
- lastDoc,
249
- firstDoc,
250
- hasNextPage,
251
- currentClientPageIndex: undefined,
252
- };
253
- }
254
- // Fallback para garantir que sempre retornamos algo
255
- return {
256
- items: [],
257
- filterLength: null,
258
- firstDoc: null,
259
- lastDoc: null,
260
- hasNextPage: false,
261
- currentClientPageIndex: undefined,
262
- };
263
- }
264
- applyFilters(query, arrange, conditions) {
265
- if (conditions) {
266
- conditions.map((cond) => {
267
- query = query.where(cond.firestoreProperty, cond.operator, cond.dashProperty);
268
- });
269
- }
270
- let hasFilterSpecificOrderBy = false;
271
- let appliedOrderByField = null;
272
- const equalsFilters = arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
273
- const otherFilters = arrange.filters.filter((f) => f.arrange !== 'equals');
274
- const equalsGroupedByProperty = equalsFilters.reduce((acc, current) => {
275
- const prop = current.filter.property;
276
- if (!acc[prop]) {
277
- acc[prop] = [];
278
- }
279
- acc[prop].push(current.filter.filtering);
280
- return acc;
281
- }, {});
282
- for (const prop in equalsGroupedByProperty) {
283
- const values = equalsGroupedByProperty[prop];
284
- if (values.length > 0) {
285
- query = query.where(prop, 'in', values);
286
- }
287
- }
288
- otherFilters.forEach((filterItem) => {
289
- // Aplicar filtragem por busca
290
- if (filterItem.filter?.filtering &&
291
- filterItem.filter?.property !== '' &&
292
- filterItem.arrange === 'filter') {
293
- query = query
294
- .where(filterItem.filter.property, '>=', filterItem.filter.filtering.trim().toUpperCase())
295
- .where(filterItem.filter.property, '<=', filterItem.filter.filtering.trim().toUpperCase() + '\uf8ff');
296
- if (!hasFilterSpecificOrderBy) {
297
- query = query.orderBy(filterItem.filter.property);
298
- hasFilterSpecificOrderBy = true;
299
- appliedOrderByField = filterItem.filter.property;
300
- }
301
- }
302
- // Aplicar filtro do tipo "filterByDate"
303
- if (filterItem.dateFilter && filterItem.arrange === 'filterByDate') {
304
- query = query
305
- .where(arrange.sortBy.field, '>=', filterItem.dateFilter.initial)
306
- .where(arrange.sortBy.field, '<=', filterItem.dateFilter.final);
307
- if (!hasFilterSpecificOrderBy) {
308
- query = query.orderBy(arrange.sortBy.field);
309
- hasFilterSpecificOrderBy = true;
310
- appliedOrderByField = arrange.sortBy.field;
311
- }
312
- }
313
- });
314
- // Aplicar sortBy
315
- if (arrange.sortBy && arrange.sortBy.field && arrange.sortBy.order) {
316
- if (appliedOrderByField !== arrange.sortBy.field) {
317
- query = query.orderBy(arrange.sortBy.field, arrange.sortBy.order);
318
- }
319
- }
320
- return query;
321
- }
322
- /**
323
- * Detecta se a query vai precisar de index composto e deve usar fallback client-side
324
- */
325
- shouldUseClientSideFallback(params) {
326
- const hasConditions = params.conditions && params.conditions.length > 0;
327
- const hasArrangeFilters = params.arrange?.filters && params.arrange.filters.length > 0;
328
- const hasSortBy = params.arrange?.sortBy?.field;
329
- if (params.filterFn) {
330
- return false;
331
- }
332
- if (hasConditions && hasArrangeFilters && hasSortBy) {
333
- return true;
334
- }
335
- if (hasConditions && hasArrangeFilters) {
336
- return true;
337
- }
338
- if (hasArrangeFilters && params.arrange.filters.length > 1 && hasSortBy) {
339
- return true;
340
- }
341
- return false;
342
- }
343
- async getPaginated(params) {
344
- // Detectar preventivamente se deve usar fallback
345
- if (this.shouldUseClientSideFallback(params)) {
346
- await this.trackMissingIndexPreventive(params.collection, params.arrange, params.conditions);
347
- const result = await this.executeClientSideQuery(params);
348
- console.log('📊 [TABLE] Resultados paginados via fallback client-side:', {
349
- totalItems: result.filterLength,
350
- returnedItems: result.items.length,
351
- hasNextPage: result.hasNextPage,
352
- currentPage: (result.currentClientPageIndex || 0) + 1,
353
- });
354
- return result;
355
- }
356
- try {
357
- const result = await this.executeQuery(params);
358
- console.log('📊 [TABLE] Resultados paginados via Firestore:', {
359
- totalItems: result.filterLength || 'N/A',
360
- returnedItems: result.items.length,
361
- hasNextPage: result.hasNextPage,
362
- });
363
- return result;
364
- }
365
- catch (error) {
366
- if (error && error.code === 'failed-precondition') {
367
- await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
368
- const result = await this.executeClientSideQuery(params);
369
- console.log('📊 [TABLE] Resultados paginados via fallback (erro de index):', {
370
- totalItems: result.filterLength,
371
- returnedItems: result.items.length,
372
- hasNextPage: result.hasNextPage,
373
- currentPage: (result.currentClientPageIndex || 0) + 1,
374
- });
375
- return result;
376
- }
377
- else if (error && error.code === 'invalid-argument') {
378
- await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
379
- const result = await this.executeClientSideQuery(params);
380
- console.log('📊 [TABLE] Resultados paginados via fallback (argumento inválido):', {
381
- totalItems: result.filterLength,
382
- returnedItems: result.items.length,
383
- hasNextPage: result.hasNextPage,
384
- currentPage: (result.currentClientPageIndex || 0) + 1,
385
- });
386
- return result;
387
- }
388
- else {
389
- throw error;
390
- }
391
- }
392
- }
393
- async executeClientSideQuery(params) {
394
- // Otimizar usando pelo menos uma cláusula .where() quando possível
395
- let query = this.ngFire.collection(params.collection).ref;
396
- let appliedCondition = null;
397
- let hasAppliedWhereClause = false;
398
- // Primeiro, tenta aplicar condições simples
399
- if (params.conditions && params.conditions.length > 0) {
400
- const simpleCondition = params.conditions.find((cond) => ['==', '>', '<', '>=', '<=', 'in', 'array-contains'].includes(cond.operator));
401
- if (simpleCondition) {
402
- query = query.where(simpleCondition.firestoreProperty, simpleCondition.operator, simpleCondition.dashProperty);
403
- appliedCondition = simpleCondition;
404
- hasAppliedWhereClause = true;
405
- }
406
- }
407
- // Se não há condições disponíveis, tenta aplicar filtros do arrange
408
- let appliedFirestoreFilter = null;
409
- if (!hasAppliedWhereClause && params.arrange?.filters) {
410
- const equalsFilter = params.arrange.filters.find((f) => f.arrange === 'equals' && f.filter?.filtering);
411
- if (equalsFilter && equalsFilter.filter) {
412
- query = query.where(equalsFilter.filter.property, '==', equalsFilter.filter.filtering);
413
- hasAppliedWhereClause = true;
414
- appliedFirestoreFilter = equalsFilter;
415
- }
416
- else {
417
- const otherFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
418
- f.filter?.filtering &&
419
- f.filter?.property) ||
420
- (f.arrange === 'filterByDate' &&
421
- f.dateFilter?.initial &&
422
- f.dateFilter?.final));
423
- if (otherFilter) {
424
- if (otherFilter.arrange === 'filter' && otherFilter.filter) {
425
- const filterValue = otherFilter.filter.filtering
426
- .trim()
427
- .toUpperCase();
428
- query = query
429
- .where(otherFilter.filter.property, '>=', filterValue)
430
- .where(otherFilter.filter.property, '<=', filterValue + '\uf8ff');
431
- hasAppliedWhereClause = true;
432
- appliedFirestoreFilter = otherFilter;
433
- }
434
- else if (otherFilter.arrange === 'filterByDate' &&
435
- otherFilter.dateFilter &&
436
- params.arrange.sortBy?.field) {
437
- query = query
438
- .where(params.arrange.sortBy.field, '>=', otherFilter.dateFilter.initial)
439
- .where(params.arrange.sortBy.field, '<=', otherFilter.dateFilter.final);
440
- hasAppliedWhereClause = true;
441
- appliedFirestoreFilter = otherFilter;
442
- }
443
- }
444
- }
445
- }
446
- const allDocsSnapshot = await query.get();
447
- let items = allDocsSnapshot.docs.map((doc) => ({
448
- id: doc.id,
449
- ...doc.data(),
450
- }));
451
- // Aplicar condições restantes
452
- if (params.conditions) {
453
- const remainingConditions = params.conditions.filter((cond) => cond !== appliedCondition);
454
- if (remainingConditions.length > 0) {
455
- const operators = this.operators;
456
- items = items.filter((item) => {
457
- return remainingConditions.every((cond) => {
458
- const operatorFn = operators[cond.operator];
459
- return operatorFn
460
- ? operatorFn(item[cond.firestoreProperty], cond.dashProperty)
461
- : false;
462
- });
463
- });
464
- }
465
- }
466
- const { filters, sortBy } = params.arrange;
467
- // Track which filter was already applied in Firestore to avoid double filtering
468
- if (hasAppliedWhereClause && !appliedCondition && params.arrange?.filters) {
469
- const equalsFilter = params.arrange.filters.find((f) => f.arrange === 'equals' && f.filter?.filtering);
470
- if (equalsFilter) {
471
- appliedFirestoreFilter = equalsFilter;
472
- }
473
- else {
474
- appliedFirestoreFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
475
- f.filter?.filtering &&
476
- f.filter?.property) ||
477
- (f.arrange === 'filterByDate' &&
478
- f.dateFilter?.initial &&
479
- f.dateFilter?.final));
480
- }
481
- }
482
- const equalsFilters = filters.filter((f) => f.arrange === 'equals');
483
- const otherFilters = filters.filter((f) => f.arrange !== 'equals');
484
- const remainingEqualsFilters = equalsFilters.filter((f) => f !== appliedFirestoreFilter);
485
- if (remainingEqualsFilters.length > 0) {
486
- items = items.filter((item) => {
487
- return remainingEqualsFilters.every((f) => item[f.filter.property] === f.filter.filtering);
488
- });
489
- }
490
- otherFilters.forEach((filterItem) => {
491
- if (appliedFirestoreFilter === filterItem) {
492
- return;
493
- }
494
- if (filterItem.arrange === 'filter' &&
495
- filterItem.filter?.filtering &&
496
- filterItem.filter?.property) {
497
- const filterValue = String(filterItem.filter.filtering)
498
- .trim()
499
- .toLowerCase();
500
- items = items.filter((item) => {
501
- const itemValue = String(item[filterItem.filter.property]).toLowerCase();
502
- return itemValue.includes(filterValue);
503
- });
504
- }
505
- if (filterItem.arrange === 'filterByDate' &&
506
- filterItem.dateFilter?.initial &&
507
- filterItem.dateFilter?.final &&
508
- sortBy.field) {
509
- items = items.filter((item) => {
510
- try {
511
- const fieldValue = item[sortBy.field];
512
- if (!fieldValue) {
513
- return false;
514
- }
515
- let itemDate;
516
- if (typeof fieldValue.toDate === 'function') {
517
- itemDate = fieldValue.toDate();
518
- }
519
- else if (fieldValue instanceof Date) {
520
- itemDate = fieldValue;
521
- }
522
- else if (typeof fieldValue === 'string') {
523
- itemDate = new Date(fieldValue);
524
- if (isNaN(itemDate.getTime())) {
525
- return false;
526
- }
527
- }
528
- else if (typeof fieldValue === 'number') {
529
- itemDate = new Date(fieldValue);
530
- }
531
- else {
532
- return false;
533
- }
534
- return (itemDate >= filterItem.dateFilter.initial &&
535
- itemDate <= filterItem.dateFilter.final);
536
- }
537
- catch (error) {
538
- console.warn('Erro ao processar filtro de data para o item:', item.id, error);
539
- return false;
540
- }
541
- });
542
- }
543
- });
544
- // Aplicar filterFn se existir
545
- if (params.filterFn) {
546
- items = items.filter(params.filterFn);
547
- }
548
- if (sortBy && sortBy.field && sortBy.order) {
549
- items.sort((a, b) => {
550
- const valA = a[sortBy.field];
551
- const valB = b[sortBy.field];
552
- if (valA < valB) {
553
- return sortBy.order === 'asc' ? -1 : 1;
554
- }
555
- if (valA > valB) {
556
- return sortBy.order === 'asc' ? 1 : -1;
557
- }
558
- return 0;
559
- });
560
- }
561
- // Implementação adequada da paginação
562
- let currentClientPageIndex = 0;
563
- // Determinar a página atual baseada na navegação
564
- if (params.navigation === 'reload') {
565
- currentClientPageIndex = 0;
566
- }
567
- else if (params.navigation === 'forward') {
568
- currentClientPageIndex = (params.clientPageIndex || 0) + 1;
569
- }
570
- else if (params.navigation === 'backward') {
571
- currentClientPageIndex = Math.max(0, (params.clientPageIndex || 0) - 1);
572
- }
573
- const pageSize = params.batchSize;
574
- const startIndex = currentClientPageIndex * pageSize;
575
- const endIndex = startIndex + pageSize;
576
- const paginatedItems = items.slice(startIndex, endIndex);
577
- const totalPages = Math.ceil(items.length / pageSize);
578
- const hasNextPage = currentClientPageIndex < totalPages - 1;
579
- const hasPreviousPage = currentClientPageIndex > 0;
580
- return {
581
- items: paginatedItems,
582
- filterLength: items.length,
583
- lastDoc: null,
584
- firstDoc: null,
585
- hasNextPage: hasNextPage,
586
- hasPreviousPage: hasPreviousPage,
587
- currentClientPageIndex: currentClientPageIndex,
588
- totalPages: totalPages,
589
- };
590
- }
591
- async getItemsData(collection, arrange, conditions = undefined) {
592
- try {
593
- let query = this.ngFire.collection(collection).ref;
594
- query = this.applyFilters(query, arrange, conditions);
595
- const snapshot = await query.get();
596
- return await Promise.all(snapshot.docs.map(async (doc) => {
597
- const data = doc.data();
598
- const id = doc.id;
599
- return {
600
- id,
601
- ...data,
602
- };
603
- }));
604
- }
605
- catch (e) {
606
- throw e;
607
- }
608
- }
609
- async deleteIndex(id, col) {
610
- try {
611
- const batch = this.ngFire.firestore.batch();
612
- const docRef = this.ngFire.collection(col).doc(id);
613
- const docSnapshot = (await firstValueFrom(docRef.get()));
614
- const doc = docSnapshot.data();
615
- batch.delete(docRef.ref);
616
- if (doc && typeof doc.index === 'number') {
617
- await this.reindex(doc.index, col, batch);
618
- }
619
- await batch.commit();
620
- this.toastr.success('Item excluído com sucesso!');
621
- return true;
622
- }
623
- catch (e) {
624
- const error = e;
625
- console.error('Erro ao deletar item:', error);
626
- this.toastr.error('Erro ao deletar item.');
627
- return false;
628
- }
629
- }
630
- async reindex(index, col, batch) {
631
- try {
632
- const snapshot = (await firstValueFrom(this.ngFire.collection(col).get()));
633
- const docs = snapshot.docs;
634
- for (let doc of docs) {
635
- const data = doc.data();
636
- if (data && typeof data.index === 'number' && data.index > index) {
637
- data.index--;
638
- const docRef = this.ngFire.collection(col).doc(doc.id).ref;
639
- batch.update(docRef, data);
640
- }
641
- }
642
- }
643
- catch (error) {
644
- console.error('Erro ao reindexar:', error);
645
- }
646
- return;
647
- }
648
- dateFormatValidator() {
649
- return (control) => {
650
- if (!control.value) {
651
- return null;
652
- }
653
- const dateStr = control.value.trim();
654
- const datePattern = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
655
- if (!datePattern.test(dateStr)) {
656
- return { invalidFormat: true };
657
- }
658
- const parts = dateStr.split('/');
659
- const day = parts[0].padStart(2, '0');
660
- const month = parts[1].padStart(2, '0');
661
- const year = parts[2];
662
- const normalizedDate = `${day}/${month}/${year}`;
663
- const date = moment(normalizedDate, 'DD/MM/YYYY', true);
664
- if (!date.isValid()) {
665
- return { invalidDate: true };
666
- }
667
- return null;
668
- };
669
- }
670
- async updateIndex(index, id, col) {
671
- await this.ngFire.collection(col).doc(id).update({ index });
672
- }
673
- /**
674
- * Extrai o link de criação de índice da mensagem de erro do Firestore
675
- */
676
- extractIndexLink(error) {
677
- if (!error || !error.message)
678
- return null;
679
- const linkMatch = error.message.match(/(https:\/\/console\.firebase\.google\.com\/[^\s]+)/);
680
- return linkMatch ? linkMatch[1] : null;
681
- }
682
- /**
683
- * Rastreia índices ausentes ao usar fallback preventivo
684
- */
685
- async trackMissingIndexPreventive(collection, arrange, conditions = undefined) {
686
- try {
687
- const querySignature = this.generateQuerySignature(collection, arrange, conditions);
688
- const docId = `${collection}_${querySignature}`;
689
- const indexLink = this.generateIndexLink(collection, arrange, conditions);
690
- const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
691
- const trackingData = {
692
- collection,
693
- indexLink,
694
- indexInstructions,
695
- arrange: {
696
- sortBy: arrange.sortBy,
697
- filters: arrange.filters?.map((f) => ({
698
- arrange: f.arrange,
699
- property: f.filter?.property || null,
700
- dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
701
- })) || [],
702
- },
703
- conditions: conditions?.map((c) => ({
704
- property: c.firestoreProperty,
705
- operator: c.operator,
706
- })) || [],
707
- errorMessage: `Fallback preventivo usado para a collection ${collection}. A query exigiria índice composto.`,
708
- updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
709
- };
710
- console.log('📄 [INDEX LINK] Dados que serão salvos no documento:', {
711
- docId,
712
- collection: trackingData.collection,
713
- indexLink: trackingData.indexLink,
714
- arrange: trackingData.arrange,
715
- conditions: trackingData.conditions,
716
- errorMessage: trackingData.errorMessage,
717
- });
718
- const docRef = this.ngFire.collection('missingIndexes').doc(docId);
719
- const doc = await docRef.get().toPromise();
720
- if (doc && doc.exists) {
721
- await docRef.update({
722
- count: firebase.firestore.FieldValue.increment(1),
723
- updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
724
- lastError: trackingData.errorMessage,
725
- });
726
- }
727
- else {
728
- await docRef.set({
729
- ...trackingData,
730
- count: 1,
731
- createdAt: firebase.firestore.FieldValue.serverTimestamp(),
732
- });
733
- }
734
- }
735
- catch (trackingError) {
736
- console.warn('Falha ao rastrear fallback preventivo:', trackingError);
737
- }
738
- }
739
- /**
740
- * Gera uma assinatura única para uma query
741
- */
742
- generateQuerySignature(collection, arrange, conditions = undefined) {
743
- const signature = {
744
- collection,
745
- sortBy: arrange.sortBy,
746
- filters: arrange.filters?.map((f) => ({
747
- arrange: f.arrange,
748
- property: f.filter?.property || null,
749
- })) || [],
750
- conditions: conditions?.map((c) => ({
751
- property: c.firestoreProperty,
752
- operator: c.operator,
753
- })) || [],
754
- };
755
- return btoa(JSON.stringify(signature))
756
- .replace(/[^a-zA-Z0-9]/g, '')
757
- .substring(0, 20);
758
- }
759
- /**
760
- * Gera instruções claras para criar o índice manualmente
761
- */
762
- generateIndexInstructions(collection, arrange, conditions = undefined) {
763
- const instructions = {
764
- summary: '',
765
- collection: collection,
766
- fields: [],
767
- queryExample: '',
768
- stepByStep: [],
769
- notes: [],
770
- };
771
- const fields = [];
772
- if (conditions && conditions.length > 0) {
773
- conditions.forEach((condition) => {
774
- if (condition.firestoreProperty) {
775
- fields.push({
776
- field: condition.firestoreProperty,
777
- order: 'Ascending',
778
- type: 'WHERE clause',
779
- operator: condition.operator,
780
- description: `Filtrar por ${condition.firestoreProperty} usando operador ${condition.operator}`,
781
- });
782
- }
783
- });
784
- }
785
- if (arrange.filters && arrange.filters.length > 0) {
786
- arrange.filters.forEach((filter) => {
787
- if (filter.filter?.property) {
788
- fields.push({
789
- field: filter.filter.property,
790
- order: 'Ascending',
791
- type: 'WHERE clause (filter)',
792
- operator: filter.arrange === 'filter' ? 'CONTAINS' : 'RANGE',
793
- description: `Filtrar por ${filter.filter.property} usando filtro ${filter.arrange}`,
794
- });
795
- }
796
- });
797
- }
798
- if (arrange.sortBy?.field) {
799
- fields.push({
800
- field: arrange.sortBy.field,
801
- order: arrange.sortBy.order === 'desc' ? 'Descending' : 'Ascending',
802
- type: 'ORDER BY clause',
803
- operator: 'N/A',
804
- description: `Ordenar resultados por ${arrange.sortBy.field} em ordem ${arrange.sortBy.order}`,
805
- });
806
- }
807
- instructions.fields = fields;
808
- const fieldNames = fields.map((f) => f.field).join(' + ');
809
- instructions.summary = `Criar índice composto para ${collection}: ${fieldNames}`;
810
- let queryExample = `db.collection('${collection}')`;
811
- fields.forEach((field, index) => {
812
- if (field.type.includes('WHERE')) {
813
- if (field.operator === '==') {
814
- queryExample += `\n .where('${field.field}', '==', 'value')`;
815
- }
816
- else if (field.operator === 'CONTAINS') {
817
- queryExample += `\n .where('${field.field}', '>=', 'searchText')`;
818
- }
819
- else {
820
- queryExample += `\n .where('${field.field}', '${field.operator}', 'value')`;
821
- }
822
- }
823
- });
824
- const orderByField = fields.find((f) => f.type.includes('ORDER BY'));
825
- if (orderByField) {
826
- queryExample += `\n .orderBy('${orderByField.field}', '${orderByField.order.toLowerCase()}')`;
827
- }
828
- instructions.queryExample = queryExample;
829
- instructions.stepByStep = [
830
- '1. Ir para Firebase Console → Firestore → Indexes',
831
- '2. Clicar em "Create Index"',
832
- `3. Definir Collection ID: ${collection}`,
833
- '4. Configurar campos nesta ORDEM EXATA:',
834
- ...fields.map((field, index) => ` ${index + 1}. Campo: ${field.field}, Order: ${field.order}, Array: No`),
835
- '5. Definir Query scopes: Collection',
836
- '6. Clicar em "Create" e aguardar conclusão',
837
- ];
838
- instructions.notes = [
839
- '⚠️ A ordem dos campos é CRÍTICA - deve corresponder exatamente à ordem da query',
840
- '⚠️ As cláusulas WHERE devem vir ANTES do campo ORDER BY',
841
- '⚠️ Este índice só funcionará para queries com esta combinação EXATA de campos',
842
- '⚠️ A criação do índice pode levar vários minutos',
843
- ];
844
- return instructions;
845
- }
846
- /**
847
- * Gera um link de índice baseado na estrutura da query
848
- */
849
- generateIndexLink(collection, arrange, conditions = undefined) {
850
- try {
851
- const indexFields = [];
852
- if (conditions && conditions.length > 0) {
853
- conditions.forEach((condition) => {
854
- if (condition.firestoreProperty) {
855
- indexFields.push(condition.firestoreProperty);
856
- }
857
- });
858
- }
859
- if (arrange.filters && arrange.filters.length > 0) {
860
- arrange.filters.forEach((filter) => {
861
- if (filter.filter?.property) {
862
- indexFields.push(filter.filter.property);
863
- }
864
- });
865
- }
866
- if (arrange.sortBy?.field) {
867
- indexFields.push(arrange.sortBy.field);
868
- }
869
- if (indexFields.length > 1) {
870
- const baseUrl = 'https://console.firebase.google.com/project/toppayy-dev/firestore/indexes';
871
- const queryParams = new URLSearchParams({
872
- create_composite: `collection=${collection}&fields=${indexFields.join(',')}`,
873
- });
874
- const finalLink = `${baseUrl}?${queryParams.toString()}`;
875
- return finalLink;
876
- }
877
- return null;
878
- }
879
- catch (error) {
880
- console.warn('Falha ao gerar link de índice:', error);
881
- return null;
882
- }
883
- }
884
- async trackMissingIndex(error, collection, arrange, conditions = undefined) {
885
- try {
886
- const indexLink = this.extractIndexLink(error);
887
- if (!indexLink)
888
- return;
889
- const linkHash = btoa(indexLink)
890
- .replace(/[^a-zA-Z0-9]/g, '')
891
- .substring(0, 20);
892
- const docId = `${collection}_${linkHash}`;
893
- const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
894
- const trackingData = {
895
- collection,
896
- indexLink,
897
- indexInstructions,
898
- arrange: {
899
- sortBy: arrange.sortBy,
900
- filters: arrange.filters?.map((f) => ({
901
- arrange: f.arrange,
902
- property: f.filter?.property || null,
903
- dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
904
- })) || [],
905
- },
906
- conditions: conditions?.map((c) => ({
907
- property: c.firestoreProperty,
908
- operator: c.operator,
909
- })) || [],
910
- errorMessage: error.message,
911
- updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
912
- };
913
- const docRef = this.ngFire.collection('missingIndexes').doc(docId);
914
- const doc = await docRef.get().toPromise();
915
- if (doc && doc.exists) {
916
- await docRef.update({
917
- count: firebase.firestore.FieldValue.increment(1),
918
- updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
919
- lastError: error.message,
920
- });
921
- }
922
- else {
923
- await docRef.set({
924
- ...trackingData,
925
- count: 1,
926
- createdAt: firebase.firestore.FieldValue.serverTimestamp(),
927
- });
928
- }
929
- }
930
- catch (trackingError) {
931
- console.warn('Falha ao rastrear índice ausente:', trackingError);
932
- }
933
- }
934
- }
935
- TableService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, deps: [{ token: i1.AngularFirestore, optional: true }, { token: i2.MatDialog, optional: true }, { token: i3.ToastrService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
936
- TableService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, providedIn: 'root' });
937
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, decorators: [{
938
- type: Injectable,
939
- args: [{
940
- providedIn: 'root',
941
- }]
942
- }], ctorParameters: function () { return [{ type: i1.AngularFirestore, decorators: [{
943
- type: Optional
944
- }] }, { type: i2.MatDialog, decorators: [{
945
- type: Optional
946
- }] }, { type: i3.ToastrService, decorators: [{
947
- type: Optional
45
+ class TableService {
46
+ constructor(ngFire, dialog, toastr) {
47
+ this.ngFire = ngFire;
48
+ this.dialog = dialog;
49
+ this.toastr = toastr;
50
+ this.operators = {
51
+ '==': (a, b) => a === b,
52
+ '!=': (a, b) => a !== b,
53
+ '>': (a, b) => a > b,
54
+ '<': (a, b) => a < b,
55
+ '>=': (a, b) => a >= b,
56
+ '<=': (a, b) => a <= b,
57
+ in: (a, b) => Array.isArray(b) && b.includes(a),
58
+ 'not-in': (a, b) => Array.isArray(b) && !b.includes(a),
59
+ 'array-contains': (a, b) => Array.isArray(a) && a.includes(b),
60
+ 'array-contains-any': (a, b) => Array.isArray(a) &&
61
+ Array.isArray(b) &&
62
+ b.some((item) => a.includes(item)),
63
+ includes: (a, b) => a.includes(b), // Para strings ou arrays
64
+ };
65
+ }
66
+ async getItems(collection) {
67
+ try {
68
+ const querySnapshot = await collection.get();
69
+ return querySnapshot.docs.map((doc) => {
70
+ return { ...doc.data(), id: doc.id };
71
+ });
72
+ }
73
+ catch (error) {
74
+ console.warn('Collection não encontrada:', error);
75
+ return [];
76
+ }
77
+ }
78
+ async executeQuery(params) {
79
+ if (params.filterFn) {
80
+ // Lógica com filtro no cliente (filterFn)
81
+ const BATCH_FETCH_SIZE = params.batchSize;
82
+ const GOAL_SIZE = params.batchSize + 1;
83
+ if (params.navigation === 'forward' || params.navigation === 'reload') {
84
+ if (params.navigation === 'reload' && params.doc) {
85
+ params.doc.lastDoc = null;
86
+ }
87
+ let lastDocCursor = params.doc ? params.doc.lastDoc : null;
88
+ let pageResults = [];
89
+ let allFetchedDocs = [];
90
+ let hasMoreDocsInDb = true;
91
+ while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
92
+ let query = this.ngFire.collection(params.collection).ref;
93
+ query = this.applyFilters(query, params.arrange, params.conditions);
94
+ if (lastDocCursor) {
95
+ query = query.startAfter(lastDocCursor);
96
+ }
97
+ query = query.limit(BATCH_FETCH_SIZE);
98
+ const snapshot = await query.get();
99
+ if (snapshot.empty) {
100
+ hasMoreDocsInDb = false;
101
+ break;
102
+ }
103
+ lastDocCursor = snapshot.docs[snapshot.docs.length - 1];
104
+ allFetchedDocs.push(...snapshot.docs);
105
+ const batchUsers = snapshot.docs
106
+ .map((doc) => ({
107
+ id: doc.id,
108
+ ...doc.data(),
109
+ }))
110
+ .filter(params.filterFn);
111
+ pageResults.push(...batchUsers);
112
+ if (snapshot.size < BATCH_FETCH_SIZE) {
113
+ hasMoreDocsInDb = false;
114
+ }
115
+ }
116
+ const hasNextPage = pageResults.length > params.batchSize;
117
+ const finalItems = pageResults.slice(0, params.batchSize);
118
+ const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
119
+ const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
120
+ return {
121
+ items: finalItems,
122
+ filterLength: null,
123
+ firstDoc: firstDocOfPage,
124
+ lastDoc: lastDocOfPage,
125
+ hasNextPage: hasNextPage,
126
+ hasPreviousPage: !!(params.doc && params.doc.lastDoc) &&
127
+ params.navigation !== 'reload',
128
+ currentClientPageIndex: undefined,
129
+ };
130
+ }
131
+ // Lógica para trás (backward)
132
+ else if (params.navigation === 'backward') {
133
+ if (!params.doc || !params.doc.firstDoc) {
134
+ return {
135
+ items: [],
136
+ filterLength: null,
137
+ firstDoc: null,
138
+ lastDoc: null,
139
+ hasNextPage: true,
140
+ hasPreviousPage: false,
141
+ currentClientPageIndex: undefined,
142
+ };
143
+ }
144
+ let pageResults = [];
145
+ let allFetchedDocs = [];
146
+ let hasMoreDocsInDb = true;
147
+ let boundaryDoc = params.doc.firstDoc;
148
+ while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
149
+ let query = this.ngFire.collection(params.collection).ref;
150
+ query = this.applyFilters(query, params.arrange, params.conditions);
151
+ query = query.endBefore(boundaryDoc);
152
+ query = query.limitToLast(BATCH_FETCH_SIZE);
153
+ const snapshot = await query.get();
154
+ if (snapshot.empty) {
155
+ hasMoreDocsInDb = false;
156
+ break;
157
+ }
158
+ boundaryDoc = snapshot.docs[0];
159
+ allFetchedDocs = [...snapshot.docs, ...allFetchedDocs];
160
+ const batchUsers = snapshot.docs
161
+ .map((doc) => ({
162
+ id: doc.id,
163
+ ...doc.data(),
164
+ }))
165
+ .filter(params.filterFn);
166
+ pageResults = [...batchUsers, ...pageResults];
167
+ if (snapshot.size < BATCH_FETCH_SIZE) {
168
+ hasMoreDocsInDb = false;
169
+ }
170
+ }
171
+ const finalItems = pageResults.slice(0, params.batchSize);
172
+ const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
173
+ const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
174
+ return {
175
+ items: finalItems,
176
+ filterLength: null,
177
+ firstDoc: firstDocOfPage,
178
+ lastDoc: lastDocOfPage,
179
+ hasNextPage: true,
180
+ currentClientPageIndex: undefined,
181
+ };
182
+ }
183
+ }
184
+ else {
185
+ let items = [];
186
+ let docs = [];
187
+ let hasNextPage = false;
188
+ let filterLength = null;
189
+ let query = this.ngFire.collection(params.collection).ref;
190
+ if (params.conditions) {
191
+ params.conditions.forEach((c) => {
192
+ if (c.operator === '!=') {
193
+ query = query.orderBy(c.firestoreProperty);
194
+ }
195
+ });
196
+ }
197
+ query = this.applyFilters(query, params.arrange, params.conditions);
198
+ if (params.navigation === 'reload') {
199
+ query = query.limit(params.batchSize + 1);
200
+ if (params.doc && params.doc.firstDoc) {
201
+ query = query.startAt(params.doc.firstDoc);
202
+ }
203
+ }
204
+ else if (params.navigation === 'forward') {
205
+ query = query.limit(params.batchSize + 1);
206
+ if (params.doc && params.doc.lastDoc) {
207
+ query = query.startAfter(params.doc.lastDoc);
208
+ }
209
+ }
210
+ else {
211
+ // backward
212
+ query = query.limitToLast(params.batchSize + 1);
213
+ if (params.doc && params.doc.firstDoc) {
214
+ query = query.endBefore(params.doc.firstDoc);
215
+ }
216
+ }
217
+ const itemCol = await query.get();
218
+ itemCol.docs.forEach((doc) => docs.push(doc));
219
+ const itemPromises = docs.map(async (item) => {
220
+ const itemData = item.data();
221
+ items.push({ id: item.id, ...itemData });
222
+ });
223
+ let lastDoc = docs[docs.length - 1] || null;
224
+ let firstDoc = docs[0];
225
+ if ((items.length > params.batchSize && params.navigation === 'forward') ||
226
+ (params.navigation === 'reload' && items.length > params.batchSize)) {
227
+ lastDoc = docs[docs.length - 2] || null;
228
+ items.pop();
229
+ hasNextPage = true;
230
+ }
231
+ if (items.length > params.batchSize && params.navigation === 'backward') {
232
+ firstDoc = docs[1];
233
+ items.shift();
234
+ hasNextPage = true;
235
+ }
236
+ await Promise.all(itemPromises);
237
+ return {
238
+ items,
239
+ filterLength,
240
+ lastDoc,
241
+ firstDoc,
242
+ hasNextPage,
243
+ currentClientPageIndex: undefined,
244
+ };
245
+ }
246
+ // Fallback para garantir que sempre retornamos algo
247
+ return {
248
+ items: [],
249
+ filterLength: null,
250
+ firstDoc: null,
251
+ lastDoc: null,
252
+ hasNextPage: false,
253
+ currentClientPageIndex: undefined,
254
+ };
255
+ }
256
+ applyFilters(query, arrange, conditions) {
257
+ if (conditions) {
258
+ conditions.map((cond) => {
259
+ query = query.where(cond.firestoreProperty, cond.operator, cond.dashProperty);
260
+ });
261
+ }
262
+ let hasFilterSpecificOrderBy = false;
263
+ let appliedOrderByField = null;
264
+ const equalsFilters = arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
265
+ const otherFilters = arrange.filters.filter((f) => f.arrange !== 'equals');
266
+ const equalsGroupedByProperty = equalsFilters.reduce((acc, current) => {
267
+ const prop = current.filter.property;
268
+ if (!acc[prop]) {
269
+ acc[prop] = [];
270
+ }
271
+ acc[prop].push(current.filter.filtering);
272
+ return acc;
273
+ }, {});
274
+ for (const prop in equalsGroupedByProperty) {
275
+ const values = equalsGroupedByProperty[prop];
276
+ if (values.length > 0) {
277
+ query = query.where(prop, 'in', values);
278
+ }
279
+ }
280
+ otherFilters.forEach((filterItem) => {
281
+ // Aplicar filtragem por busca
282
+ if (filterItem.filter?.filtering &&
283
+ filterItem.filter?.property !== '' &&
284
+ filterItem.arrange === 'filter') {
285
+ query = query
286
+ .where(filterItem.filter.property, '>=', filterItem.filter.filtering.trim().toUpperCase())
287
+ .where(filterItem.filter.property, '<=', filterItem.filter.filtering.trim().toUpperCase() + '\uf8ff');
288
+ if (!hasFilterSpecificOrderBy) {
289
+ query = query.orderBy(filterItem.filter.property);
290
+ hasFilterSpecificOrderBy = true;
291
+ appliedOrderByField = filterItem.filter.property;
292
+ }
293
+ }
294
+ // Aplicar filtro do tipo "filterByDate"
295
+ if (filterItem.dateFilter && filterItem.arrange === 'filterByDate') {
296
+ query = query
297
+ .where(arrange.sortBy.field, '>=', filterItem.dateFilter.initial)
298
+ .where(arrange.sortBy.field, '<=', filterItem.dateFilter.final);
299
+ if (!hasFilterSpecificOrderBy) {
300
+ query = query.orderBy(arrange.sortBy.field);
301
+ hasFilterSpecificOrderBy = true;
302
+ appliedOrderByField = arrange.sortBy.field;
303
+ }
304
+ }
305
+ });
306
+ // Aplicar sortBy
307
+ if (arrange.sortBy && arrange.sortBy.field && arrange.sortBy.order) {
308
+ if (appliedOrderByField !== arrange.sortBy.field) {
309
+ query = query.orderBy(arrange.sortBy.field, arrange.sortBy.order);
310
+ }
311
+ }
312
+ return query;
313
+ }
314
+ /**
315
+ * Detecta se a query vai precisar de index composto e deve usar fallback client-side
316
+ */
317
+ shouldUseClientSideFallback(params) {
318
+ const hasConditions = params.conditions && params.conditions.length > 0;
319
+ const hasArrangeFilters = params.arrange?.filters && params.arrange.filters.length > 0;
320
+ const hasSortBy = params.arrange?.sortBy?.field;
321
+ if (params.filterFn) {
322
+ return false;
323
+ }
324
+ if (hasConditions && hasArrangeFilters && hasSortBy) {
325
+ return true;
326
+ }
327
+ if (hasConditions && hasArrangeFilters) {
328
+ return true;
329
+ }
330
+ if (hasArrangeFilters && params.arrange.filters.length > 1 && hasSortBy) {
331
+ return true;
332
+ }
333
+ return false;
334
+ }
335
+ async getPaginated(params) {
336
+ // Detectar preventivamente se deve usar fallback
337
+ if (this.shouldUseClientSideFallback(params)) {
338
+ await this.trackMissingIndexPreventive(params.collection, params.arrange, params.conditions);
339
+ const result = await this.executeClientSideQuery(params);
340
+ console.log('📊 [TABLE] Resultados paginados via fallback client-side:', {
341
+ totalItems: result.filterLength,
342
+ returnedItems: result.items.length,
343
+ hasNextPage: result.hasNextPage,
344
+ currentPage: (result.currentClientPageIndex || 0) + 1,
345
+ });
346
+ return result;
347
+ }
348
+ try {
349
+ const result = await this.executeQuery(params);
350
+ console.log('📊 [TABLE] Resultados paginados via Firestore:', {
351
+ totalItems: result.filterLength || 'N/A',
352
+ returnedItems: result.items.length,
353
+ hasNextPage: result.hasNextPage,
354
+ });
355
+ return result;
356
+ }
357
+ catch (error) {
358
+ if (error && error.code === 'failed-precondition') {
359
+ await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
360
+ const result = await this.executeClientSideQuery(params);
361
+ console.log('📊 [TABLE] Resultados paginados via fallback (erro de index):', {
362
+ totalItems: result.filterLength,
363
+ returnedItems: result.items.length,
364
+ hasNextPage: result.hasNextPage,
365
+ currentPage: (result.currentClientPageIndex || 0) + 1,
366
+ });
367
+ return result;
368
+ }
369
+ else if (error && error.code === 'invalid-argument') {
370
+ await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
371
+ const result = await this.executeClientSideQuery(params);
372
+ console.log('📊 [TABLE] Resultados paginados via fallback (argumento inválido):', {
373
+ totalItems: result.filterLength,
374
+ returnedItems: result.items.length,
375
+ hasNextPage: result.hasNextPage,
376
+ currentPage: (result.currentClientPageIndex || 0) + 1,
377
+ });
378
+ return result;
379
+ }
380
+ else {
381
+ throw error;
382
+ }
383
+ }
384
+ }
385
+ async executeClientSideQuery(params) {
386
+ // Otimizar usando pelo menos uma cláusula .where() quando possível
387
+ let query = this.ngFire.collection(params.collection).ref;
388
+ let appliedCondition = null;
389
+ let hasAppliedWhereClause = false;
390
+ // Primeiro, tenta aplicar condições simples
391
+ if (params.conditions && params.conditions.length > 0) {
392
+ const simpleCondition = params.conditions.find((cond) => ['==', '>', '<', '>=', '<=', 'in', 'array-contains'].includes(cond.operator));
393
+ if (simpleCondition) {
394
+ query = query.where(simpleCondition.firestoreProperty, simpleCondition.operator, simpleCondition.dashProperty);
395
+ appliedCondition = simpleCondition;
396
+ hasAppliedWhereClause = true;
397
+ }
398
+ }
399
+ // Se não há condições disponíveis, tenta aplicar filtros do arrange
400
+ let appliedFirestoreFilter = null;
401
+ if (!hasAppliedWhereClause && params.arrange?.filters) {
402
+ const equalsFilter = params.arrange.filters.find((f) => f.arrange === 'equals' && f.filter?.filtering);
403
+ if (equalsFilter && equalsFilter.filter) {
404
+ query = query.where(equalsFilter.filter.property, '==', equalsFilter.filter.filtering);
405
+ hasAppliedWhereClause = true;
406
+ appliedFirestoreFilter = equalsFilter;
407
+ }
408
+ else {
409
+ const otherFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
410
+ f.filter?.filtering &&
411
+ f.filter?.property) ||
412
+ (f.arrange === 'filterByDate' &&
413
+ f.dateFilter?.initial &&
414
+ f.dateFilter?.final));
415
+ if (otherFilter) {
416
+ if (otherFilter.arrange === 'filter' && otherFilter.filter) {
417
+ const filterValue = otherFilter.filter.filtering
418
+ .trim()
419
+ .toUpperCase();
420
+ query = query
421
+ .where(otherFilter.filter.property, '>=', filterValue)
422
+ .where(otherFilter.filter.property, '<=', filterValue + '\uf8ff');
423
+ hasAppliedWhereClause = true;
424
+ appliedFirestoreFilter = otherFilter;
425
+ }
426
+ else if (otherFilter.arrange === 'filterByDate' &&
427
+ otherFilter.dateFilter &&
428
+ params.arrange.sortBy?.field) {
429
+ query = query
430
+ .where(params.arrange.sortBy.field, '>=', otherFilter.dateFilter.initial)
431
+ .where(params.arrange.sortBy.field, '<=', otherFilter.dateFilter.final);
432
+ hasAppliedWhereClause = true;
433
+ appliedFirestoreFilter = otherFilter;
434
+ }
435
+ }
436
+ }
437
+ }
438
+ const allDocsSnapshot = await query.get();
439
+ let items = allDocsSnapshot.docs.map((doc) => ({
440
+ id: doc.id,
441
+ ...doc.data(),
442
+ }));
443
+ // Aplicar condições restantes
444
+ if (params.conditions) {
445
+ const remainingConditions = params.conditions.filter((cond) => cond !== appliedCondition);
446
+ if (remainingConditions.length > 0) {
447
+ const operators = this.operators;
448
+ items = items.filter((item) => {
449
+ return remainingConditions.every((cond) => {
450
+ const operatorFn = operators[cond.operator];
451
+ return operatorFn
452
+ ? operatorFn(item[cond.firestoreProperty], cond.dashProperty)
453
+ : false;
454
+ });
455
+ });
456
+ }
457
+ }
458
+ const { filters, sortBy } = params.arrange;
459
+ // Track which filter was already applied in Firestore to avoid double filtering
460
+ if (hasAppliedWhereClause && !appliedCondition && params.arrange?.filters) {
461
+ const equalsFilter = params.arrange.filters.find((f) => f.arrange === 'equals' && f.filter?.filtering);
462
+ if (equalsFilter) {
463
+ appliedFirestoreFilter = equalsFilter;
464
+ }
465
+ else {
466
+ appliedFirestoreFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
467
+ f.filter?.filtering &&
468
+ f.filter?.property) ||
469
+ (f.arrange === 'filterByDate' &&
470
+ f.dateFilter?.initial &&
471
+ f.dateFilter?.final));
472
+ }
473
+ }
474
+ const equalsFilters = filters.filter((f) => f.arrange === 'equals');
475
+ const otherFilters = filters.filter((f) => f.arrange !== 'equals');
476
+ const remainingEqualsFilters = equalsFilters.filter((f) => f !== appliedFirestoreFilter);
477
+ if (remainingEqualsFilters.length > 0) {
478
+ items = items.filter((item) => {
479
+ return remainingEqualsFilters.every((f) => item[f.filter.property] === f.filter.filtering);
480
+ });
481
+ }
482
+ otherFilters.forEach((filterItem) => {
483
+ if (appliedFirestoreFilter === filterItem) {
484
+ return;
485
+ }
486
+ if (filterItem.arrange === 'filter' &&
487
+ filterItem.filter?.filtering &&
488
+ filterItem.filter?.property) {
489
+ const filterValue = String(filterItem.filter.filtering)
490
+ .trim()
491
+ .toLowerCase();
492
+ items = items.filter((item) => {
493
+ const itemValue = String(item[filterItem.filter.property]).toLowerCase();
494
+ return itemValue.includes(filterValue);
495
+ });
496
+ }
497
+ if (filterItem.arrange === 'filterByDate' &&
498
+ filterItem.dateFilter?.initial &&
499
+ filterItem.dateFilter?.final &&
500
+ sortBy.field) {
501
+ items = items.filter((item) => {
502
+ try {
503
+ const fieldValue = item[sortBy.field];
504
+ if (!fieldValue) {
505
+ return false;
506
+ }
507
+ let itemDate;
508
+ if (typeof fieldValue.toDate === 'function') {
509
+ itemDate = fieldValue.toDate();
510
+ }
511
+ else if (fieldValue instanceof Date) {
512
+ itemDate = fieldValue;
513
+ }
514
+ else if (typeof fieldValue === 'string') {
515
+ itemDate = new Date(fieldValue);
516
+ if (isNaN(itemDate.getTime())) {
517
+ return false;
518
+ }
519
+ }
520
+ else if (typeof fieldValue === 'number') {
521
+ itemDate = new Date(fieldValue);
522
+ }
523
+ else {
524
+ return false;
525
+ }
526
+ return (itemDate >= filterItem.dateFilter.initial &&
527
+ itemDate <= filterItem.dateFilter.final);
528
+ }
529
+ catch (error) {
530
+ console.warn('Erro ao processar filtro de data para o item:', item.id, error);
531
+ return false;
532
+ }
533
+ });
534
+ }
535
+ });
536
+ // Aplicar filterFn se existir
537
+ if (params.filterFn) {
538
+ items = items.filter(params.filterFn);
539
+ }
540
+ if (sortBy && sortBy.field && sortBy.order) {
541
+ items.sort((a, b) => {
542
+ const valA = a[sortBy.field];
543
+ const valB = b[sortBy.field];
544
+ if (valA < valB) {
545
+ return sortBy.order === 'asc' ? -1 : 1;
546
+ }
547
+ if (valA > valB) {
548
+ return sortBy.order === 'asc' ? 1 : -1;
549
+ }
550
+ return 0;
551
+ });
552
+ }
553
+ // Implementação adequada da paginação
554
+ let currentClientPageIndex = 0;
555
+ // Determinar a página atual baseada na navegação
556
+ if (params.navigation === 'reload') {
557
+ currentClientPageIndex = 0;
558
+ }
559
+ else if (params.navigation === 'forward') {
560
+ currentClientPageIndex = (params.clientPageIndex || 0) + 1;
561
+ }
562
+ else if (params.navigation === 'backward') {
563
+ currentClientPageIndex = Math.max(0, (params.clientPageIndex || 0) - 1);
564
+ }
565
+ const pageSize = params.batchSize;
566
+ const startIndex = currentClientPageIndex * pageSize;
567
+ const endIndex = startIndex + pageSize;
568
+ const paginatedItems = items.slice(startIndex, endIndex);
569
+ const totalPages = Math.ceil(items.length / pageSize);
570
+ const hasNextPage = currentClientPageIndex < totalPages - 1;
571
+ const hasPreviousPage = currentClientPageIndex > 0;
572
+ return {
573
+ items: paginatedItems,
574
+ filterLength: items.length,
575
+ lastDoc: null,
576
+ firstDoc: null,
577
+ hasNextPage: hasNextPage,
578
+ hasPreviousPage: hasPreviousPage,
579
+ currentClientPageIndex: currentClientPageIndex,
580
+ totalPages: totalPages,
581
+ };
582
+ }
583
+ async getItemsData(collection, arrange, conditions = undefined) {
584
+ try {
585
+ let query = this.ngFire.collection(collection).ref;
586
+ query = this.applyFilters(query, arrange, conditions);
587
+ const snapshot = await query.get();
588
+ return await Promise.all(snapshot.docs.map(async (doc) => {
589
+ const data = doc.data();
590
+ const id = doc.id;
591
+ return {
592
+ id,
593
+ ...data,
594
+ };
595
+ }));
596
+ }
597
+ catch (e) {
598
+ throw e;
599
+ }
600
+ }
601
+ async deleteIndex(id, col) {
602
+ try {
603
+ const batch = this.ngFire.firestore.batch();
604
+ const docRef = this.ngFire.collection(col).doc(id);
605
+ const docSnapshot = (await firstValueFrom(docRef.get()));
606
+ const doc = docSnapshot.data();
607
+ batch.delete(docRef.ref);
608
+ if (doc && typeof doc.index === 'number') {
609
+ await this.reindex(doc.index, col, batch);
610
+ }
611
+ await batch.commit();
612
+ this.toastr.success('Item excluído com sucesso!');
613
+ return true;
614
+ }
615
+ catch (e) {
616
+ const error = e;
617
+ console.error('Erro ao deletar item:', error);
618
+ this.toastr.error('Erro ao deletar item.');
619
+ return false;
620
+ }
621
+ }
622
+ async reindex(index, col, batch) {
623
+ try {
624
+ const snapshot = (await firstValueFrom(this.ngFire.collection(col).get()));
625
+ const docs = snapshot.docs;
626
+ for (let doc of docs) {
627
+ const data = doc.data();
628
+ if (data && typeof data.index === 'number' && data.index > index) {
629
+ data.index--;
630
+ const docRef = this.ngFire.collection(col).doc(doc.id).ref;
631
+ batch.update(docRef, data);
632
+ }
633
+ }
634
+ }
635
+ catch (error) {
636
+ console.error('Erro ao reindexar:', error);
637
+ }
638
+ return;
639
+ }
640
+ dateFormatValidator() {
641
+ return (control) => {
642
+ if (!control.value) {
643
+ return null;
644
+ }
645
+ const dateStr = control.value.trim();
646
+ const datePattern = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
647
+ if (!datePattern.test(dateStr)) {
648
+ return { invalidFormat: true };
649
+ }
650
+ const parts = dateStr.split('/');
651
+ const day = parts[0].padStart(2, '0');
652
+ const month = parts[1].padStart(2, '0');
653
+ const year = parts[2];
654
+ const normalizedDate = `${day}/${month}/${year}`;
655
+ const date = moment(normalizedDate, 'DD/MM/YYYY', true);
656
+ if (!date.isValid()) {
657
+ return { invalidDate: true };
658
+ }
659
+ return null;
660
+ };
661
+ }
662
+ async updateIndex(index, id, col) {
663
+ await this.ngFire.collection(col).doc(id).update({ index });
664
+ }
665
+ /**
666
+ * Extrai o link de criação de índice da mensagem de erro do Firestore
667
+ */
668
+ extractIndexLink(error) {
669
+ if (!error || !error.message)
670
+ return null;
671
+ const linkMatch = error.message.match(/(https:\/\/console\.firebase\.google\.com\/[^\s]+)/);
672
+ return linkMatch ? linkMatch[1] : null;
673
+ }
674
+ /**
675
+ * Rastreia índices ausentes ao usar fallback preventivo
676
+ */
677
+ async trackMissingIndexPreventive(collection, arrange, conditions = undefined) {
678
+ try {
679
+ const querySignature = this.generateQuerySignature(collection, arrange, conditions);
680
+ const docId = `${collection}_${querySignature}`;
681
+ const indexLink = this.generateIndexLink(collection, arrange, conditions);
682
+ const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
683
+ const trackingData = {
684
+ collection,
685
+ indexLink,
686
+ indexInstructions,
687
+ arrange: {
688
+ sortBy: arrange.sortBy,
689
+ filters: arrange.filters?.map((f) => ({
690
+ arrange: f.arrange,
691
+ property: f.filter?.property || null,
692
+ dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
693
+ })) || [],
694
+ },
695
+ conditions: conditions?.map((c) => ({
696
+ property: c.firestoreProperty,
697
+ operator: c.operator,
698
+ })) || [],
699
+ errorMessage: `Fallback preventivo usado para a collection ${collection}. A query exigiria índice composto.`,
700
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
701
+ };
702
+ console.log('📄 [INDEX LINK] Dados que serão salvos no documento:', {
703
+ docId,
704
+ collection: trackingData.collection,
705
+ indexLink: trackingData.indexLink,
706
+ arrange: trackingData.arrange,
707
+ conditions: trackingData.conditions,
708
+ errorMessage: trackingData.errorMessage,
709
+ });
710
+ const docRef = this.ngFire.collection('missingIndexes').doc(docId);
711
+ const doc = await docRef.get().toPromise();
712
+ if (doc && doc.exists) {
713
+ await docRef.update({
714
+ count: firebase.firestore.FieldValue.increment(1),
715
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
716
+ lastError: trackingData.errorMessage,
717
+ });
718
+ }
719
+ else {
720
+ await docRef.set({
721
+ ...trackingData,
722
+ count: 1,
723
+ createdAt: firebase.firestore.FieldValue.serverTimestamp(),
724
+ });
725
+ }
726
+ }
727
+ catch (trackingError) {
728
+ console.warn('Falha ao rastrear fallback preventivo:', trackingError);
729
+ }
730
+ }
731
+ /**
732
+ * Gera uma assinatura única para uma query
733
+ */
734
+ generateQuerySignature(collection, arrange, conditions = undefined) {
735
+ const signature = {
736
+ collection,
737
+ sortBy: arrange.sortBy,
738
+ filters: arrange.filters?.map((f) => ({
739
+ arrange: f.arrange,
740
+ property: f.filter?.property || null,
741
+ })) || [],
742
+ conditions: conditions?.map((c) => ({
743
+ property: c.firestoreProperty,
744
+ operator: c.operator,
745
+ })) || [],
746
+ };
747
+ return btoa(JSON.stringify(signature))
748
+ .replace(/[^a-zA-Z0-9]/g, '')
749
+ .substring(0, 20);
750
+ }
751
+ /**
752
+ * Gera instruções claras para criar o índice manualmente
753
+ */
754
+ generateIndexInstructions(collection, arrange, conditions = undefined) {
755
+ const instructions = {
756
+ summary: '',
757
+ collection: collection,
758
+ fields: [],
759
+ queryExample: '',
760
+ stepByStep: [],
761
+ notes: [],
762
+ };
763
+ const fields = [];
764
+ if (conditions && conditions.length > 0) {
765
+ conditions.forEach((condition) => {
766
+ if (condition.firestoreProperty) {
767
+ fields.push({
768
+ field: condition.firestoreProperty,
769
+ order: 'Ascending',
770
+ type: 'WHERE clause',
771
+ operator: condition.operator,
772
+ description: `Filtrar por ${condition.firestoreProperty} usando operador ${condition.operator}`,
773
+ });
774
+ }
775
+ });
776
+ }
777
+ if (arrange.filters && arrange.filters.length > 0) {
778
+ arrange.filters.forEach((filter) => {
779
+ if (filter.filter?.property) {
780
+ fields.push({
781
+ field: filter.filter.property,
782
+ order: 'Ascending',
783
+ type: 'WHERE clause (filter)',
784
+ operator: filter.arrange === 'filter' ? 'CONTAINS' : 'RANGE',
785
+ description: `Filtrar por ${filter.filter.property} usando filtro ${filter.arrange}`,
786
+ });
787
+ }
788
+ });
789
+ }
790
+ if (arrange.sortBy?.field) {
791
+ fields.push({
792
+ field: arrange.sortBy.field,
793
+ order: arrange.sortBy.order === 'desc' ? 'Descending' : 'Ascending',
794
+ type: 'ORDER BY clause',
795
+ operator: 'N/A',
796
+ description: `Ordenar resultados por ${arrange.sortBy.field} em ordem ${arrange.sortBy.order}`,
797
+ });
798
+ }
799
+ instructions.fields = fields;
800
+ const fieldNames = fields.map((f) => f.field).join(' + ');
801
+ instructions.summary = `Criar índice composto para ${collection}: ${fieldNames}`;
802
+ let queryExample = `db.collection('${collection}')`;
803
+ fields.forEach((field, index) => {
804
+ if (field.type.includes('WHERE')) {
805
+ if (field.operator === '==') {
806
+ queryExample += `\n .where('${field.field}', '==', 'value')`;
807
+ }
808
+ else if (field.operator === 'CONTAINS') {
809
+ queryExample += `\n .where('${field.field}', '>=', 'searchText')`;
810
+ }
811
+ else {
812
+ queryExample += `\n .where('${field.field}', '${field.operator}', 'value')`;
813
+ }
814
+ }
815
+ });
816
+ const orderByField = fields.find((f) => f.type.includes('ORDER BY'));
817
+ if (orderByField) {
818
+ queryExample += `\n .orderBy('${orderByField.field}', '${orderByField.order.toLowerCase()}')`;
819
+ }
820
+ instructions.queryExample = queryExample;
821
+ instructions.stepByStep = [
822
+ '1. Ir para Firebase Console → Firestore → Indexes',
823
+ '2. Clicar em "Create Index"',
824
+ `3. Definir Collection ID: ${collection}`,
825
+ '4. Configurar campos nesta ORDEM EXATA:',
826
+ ...fields.map((field, index) => ` ${index + 1}. Campo: ${field.field}, Order: ${field.order}, Array: No`),
827
+ '5. Definir Query scopes: Collection',
828
+ '6. Clicar em "Create" e aguardar conclusão',
829
+ ];
830
+ instructions.notes = [
831
+ '⚠️ A ordem dos campos é CRÍTICA - deve corresponder exatamente à ordem da query',
832
+ '⚠️ As cláusulas WHERE devem vir ANTES do campo ORDER BY',
833
+ '⚠️ Este índice só funcionará para queries com esta combinação EXATA de campos',
834
+ '⚠️ A criação do índice pode levar vários minutos',
835
+ ];
836
+ return instructions;
837
+ }
838
+ /**
839
+ * Gera um link de índice baseado na estrutura da query
840
+ */
841
+ generateIndexLink(collection, arrange, conditions = undefined) {
842
+ try {
843
+ const indexFields = [];
844
+ if (conditions && conditions.length > 0) {
845
+ conditions.forEach((condition) => {
846
+ if (condition.firestoreProperty) {
847
+ indexFields.push(condition.firestoreProperty);
848
+ }
849
+ });
850
+ }
851
+ if (arrange.filters && arrange.filters.length > 0) {
852
+ arrange.filters.forEach((filter) => {
853
+ if (filter.filter?.property) {
854
+ indexFields.push(filter.filter.property);
855
+ }
856
+ });
857
+ }
858
+ if (arrange.sortBy?.field) {
859
+ indexFields.push(arrange.sortBy.field);
860
+ }
861
+ if (indexFields.length > 1) {
862
+ const baseUrl = 'https://console.firebase.google.com/project/toppayy-dev/firestore/indexes';
863
+ const queryParams = new URLSearchParams({
864
+ create_composite: `collection=${collection}&fields=${indexFields.join(',')}`,
865
+ });
866
+ const finalLink = `${baseUrl}?${queryParams.toString()}`;
867
+ return finalLink;
868
+ }
869
+ return null;
870
+ }
871
+ catch (error) {
872
+ console.warn('Falha ao gerar link de índice:', error);
873
+ return null;
874
+ }
875
+ }
876
+ async trackMissingIndex(error, collection, arrange, conditions = undefined) {
877
+ try {
878
+ const indexLink = this.extractIndexLink(error);
879
+ if (!indexLink)
880
+ return;
881
+ const linkHash = btoa(indexLink)
882
+ .replace(/[^a-zA-Z0-9]/g, '')
883
+ .substring(0, 20);
884
+ const docId = `${collection}_${linkHash}`;
885
+ const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
886
+ const trackingData = {
887
+ collection,
888
+ indexLink,
889
+ indexInstructions,
890
+ arrange: {
891
+ sortBy: arrange.sortBy,
892
+ filters: arrange.filters?.map((f) => ({
893
+ arrange: f.arrange,
894
+ property: f.filter?.property || null,
895
+ dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
896
+ })) || [],
897
+ },
898
+ conditions: conditions?.map((c) => ({
899
+ property: c.firestoreProperty,
900
+ operator: c.operator,
901
+ })) || [],
902
+ errorMessage: error.message,
903
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
904
+ };
905
+ const docRef = this.ngFire.collection('missingIndexes').doc(docId);
906
+ const doc = await docRef.get().toPromise();
907
+ if (doc && doc.exists) {
908
+ await docRef.update({
909
+ count: firebase.firestore.FieldValue.increment(1),
910
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
911
+ lastError: error.message,
912
+ });
913
+ }
914
+ else {
915
+ await docRef.set({
916
+ ...trackingData,
917
+ count: 1,
918
+ createdAt: firebase.firestore.FieldValue.serverTimestamp(),
919
+ });
920
+ }
921
+ }
922
+ catch (trackingError) {
923
+ console.warn('Falha ao rastrear índice ausente:', trackingError);
924
+ }
925
+ }
926
+ }
927
+ TableService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, deps: [{ token: i1.AngularFirestore, optional: true }, { token: i2.MatDialog, optional: true }, { token: i3.ToastrService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
928
+ TableService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, providedIn: 'root' });
929
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, decorators: [{
930
+ type: Injectable,
931
+ args: [{
932
+ providedIn: 'root',
933
+ }]
934
+ }], ctorParameters: function () { return [{ type: i1.AngularFirestore, decorators: [{
935
+ type: Optional
936
+ }] }, { type: i2.MatDialog, decorators: [{
937
+ type: Optional
938
+ }] }, { type: i3.ToastrService, decorators: [{
939
+ type: Optional
948
940
  }] }]; } });
949
941
 
950
- class TableComponent {
951
- // CONSTRUCTOR
952
- constructor(router, tableService, firestore) {
953
- this.router = router;
954
- this.tableService = tableService;
955
- this.firestore = firestore;
956
- this.arrange = null;
957
- this.currentPageNumber = 1;
958
- this.currentClientPageIndex = 0;
959
- this.items = [];
960
- this.isLoading = false;
961
- this.lastDoc = null;
962
- this.firstDoc = null;
963
- this.sortBy = {
964
- field: 'createdAt',
965
- order: 'desc',
966
- };
967
- this.columnProperties = [];
968
- this.selectSort = new FormControl('');
969
- this.currentArrange = '';
970
- this.hasNextPage = false;
971
- this.dropdownItems = [];
972
- this.sortableDropdownItems = [];
973
- this.pageSize = 25;
974
- this.totalItems = 0;
975
- this.filterValue = null;
976
- this.hasFilterableColumn = false;
977
- this.hasSortableColumn = false;
978
- this.filterSubject = new Subject();
979
- this.debounceTimeMs = 500;
980
- this.selectedTab = 0;
981
- // Propriedades para controle do tooltip
982
- this.hoveredCell = null;
983
- this.showTooltip = false;
984
- this.tooltipContent = '';
985
- this.tooltipPosition = { x: 0, y: 0 };
986
- this.filtersForm = new FormArray([this.createFilterGroup()]);
987
- }
988
- createFilterGroup() {
989
- return new FormGroup({
990
- selectFilter: new FormControl(''),
991
- typeFilter: new FormControl(''),
992
- selectItem: new FormControl(''),
993
- initialDate: new FormControl('', this.tableService.dateFormatValidator()),
994
- finalDate: new FormControl('', this.tableService.dateFormatValidator()),
995
- });
996
- }
997
- addFilter(filterData) {
998
- const newFilterGroup = this.createFilterGroup();
999
- if (filterData) {
1000
- if (filterData.selectFilter) {
1001
- newFilterGroup.get('selectFilter')?.setValue(filterData.selectFilter);
1002
- }
1003
- if (filterData.typeFilter) {
1004
- newFilterGroup.get('typeFilter')?.setValue(filterData.typeFilter);
1005
- }
1006
- if (filterData.selectItem) {
1007
- newFilterGroup.get('selectItem')?.setValue(filterData.selectItem);
1008
- }
1009
- if (filterData.initialDate) {
1010
- newFilterGroup.get('initialDate')?.setValue(filterData.initialDate);
1011
- }
1012
- if (filterData.finalDate) {
1013
- newFilterGroup.get('finalDate')?.setValue(filterData.finalDate);
1014
- }
1015
- }
1016
- this.filtersForm.push(newFilterGroup);
1017
- }
1018
- onSelectFilterChange() {
1019
- const lastIndex = this.filtersForm.length - 1;
1020
- const lastFilter = this.filtersForm.at(lastIndex);
1021
- if (lastFilter.get('selectFilter')?.value) {
1022
- this.addFilter();
1023
- }
1024
- }
1025
- removeFilter(index) {
1026
- this.filtersForm.removeAt(index);
1027
- if (this.filtersForm.length === 0) {
1028
- this.addFilter();
1029
- }
1030
- }
1031
- removeAllFilters() {
1032
- this.filtersForm.clear();
1033
- this.addFilter();
1034
- this.resetFilter();
1035
- }
1036
- // METHODS
1037
- async ngOnInit() {
1038
- if (!this.data.color)
1039
- this.data.color = { bg: 'bg-primary', text: 'text-black' };
1040
- this.columnProperties = this.data.displayedColumns.map((column) => {
1041
- return column.property;
1042
- });
1043
- if (this.data.actionButton && !this.data.actionButton.condition) {
1044
- this.data.actionButton.condition = (_row) => true;
1045
- }
1046
- if (this.data.pagination) {
1047
- this.data.displayedColumns.forEach((col) => {
1048
- if (col.isFilterable) {
1049
- if (this.hasFilterableColumn === false)
1050
- this.hasFilterableColumn = true;
1051
- this.dropdownItems.push({
1052
- ...col,
1053
- arrange: 'filter',
1054
- title: col.title,
1055
- });
1056
- }
1057
- if (col.isSortable) {
1058
- if (this.hasSortableColumn === false)
1059
- this.hasSortableColumn = true;
1060
- this.sortableDropdownItems.push({
1061
- ...col,
1062
- arrange: 'ascending',
1063
- title: col.title + ': crescente',
1064
- });
1065
- this.sortableDropdownItems.push({
1066
- ...col,
1067
- arrange: 'descending',
1068
- title: col.title + ': decrescente',
1069
- });
1070
- }
1071
- if (col.isFilterableByDate) {
1072
- this.dropdownItems.push({
1073
- ...col,
1074
- arrange: 'filterByDate',
1075
- title: col.title + ': filtro por data',
1076
- });
1077
- }
1078
- });
1079
- if (this.data.filterableOptions) {
1080
- this.data.filterableOptions.forEach((option) => this.dropdownItems.push({ ...option, arrange: 'equals' }));
1081
- }
1082
- }
1083
- // Sem paginação
1084
- if (this.data.pagination === false) {
1085
- await this.loadItems();
1086
- }
1087
- // Com paginação
1088
- if (this.data.pagination === true) {
1089
- if (this.data.sortBy)
1090
- this.sortBy = {
1091
- field: this.data.sortBy.field,
1092
- order: this.data.sortBy.order,
1093
- };
1094
- this.filterSubject
1095
- .pipe(debounceTime(this.debounceTimeMs))
1096
- .subscribe(() => {
1097
- this.loadItemsPaginated('reload', true);
1098
- });
1099
- this.isLoading = true;
1100
- await this.loadItemsPaginated('reload', true);
1101
- this.sort.active = 'createdAt';
1102
- this.sort.direction = 'desc';
1103
- this.dataSource.paginator = this.paginator;
1104
- this.dataSource.sort = this.sort;
1105
- this.totalItems = 0;
1106
- if (this.data.totalRef) {
1107
- for (const totalRef of this.data.totalRef) {
1108
- const totalRefDoc = await totalRef.ref.get();
1109
- const docData = totalRefDoc.data();
1110
- if (docData && docData[totalRef.field])
1111
- this.totalItems = (this.totalItems +
1112
- docData[totalRef.field]);
1113
- }
1114
- }
1115
- this.isLoading = false;
1116
- }
1117
- }
1118
- getDisplayValue(col, row, withinLimit = false) {
1119
- let value;
1120
- if (row[col.property] === null || row[col.property] === undefined) {
1121
- return '';
1122
- }
1123
- if (col.calculateValue) {
1124
- value = String(col.calculateValue(row));
1125
- }
1126
- else {
1127
- value = this.getNestedValue(row, col.property);
1128
- }
1129
- // Verifica se é um array e tem arrayField definido
1130
- if (Array.isArray(value) && col.arrayField) {
1131
- value = this.formatArrayValue(value, col.arrayField);
1132
- }
1133
- if (col.queryLength && row[col.property]) {
1134
- value = row[col.property];
1135
- }
1136
- value = col.pipe ? col.pipe.transform(value) : value;
1137
- return withinLimit ? value : value.substring(0, col.charLimit) + '...';
1138
- }
1139
- getNestedValue(obj, path) {
1140
- if (!path)
1141
- return undefined;
1142
- const properties = path.split('.');
1143
- return properties.reduce((acc, currentPart) => acc && acc[currentPart], obj);
1144
- }
1145
- formatArrayValue(array, field) {
1146
- if (!Array.isArray(array) || array.length === 0) {
1147
- return '';
1148
- }
1149
- const values = array
1150
- .map((item) => {
1151
- if (typeof item === 'object' && item !== null) {
1152
- return item[field] || '';
1153
- }
1154
- return String(item);
1155
- })
1156
- .filter((value) => value !== '' && value !== null && value !== undefined);
1157
- return values.join(', ');
1158
- }
1159
- // Métodos sem paginação
1160
- async loadItems() {
1161
- this.items = await this.tableService.getItems(this.data.collectionRef);
1162
- if (this.data.conditions) {
1163
- this.filterItems();
1164
- }
1165
- await this.loadRelations();
1166
- await this.loadQueryLengths();
1167
- this.totalItems = this.items.length;
1168
- this.dataSource = new MatTableDataSource(this.items);
1169
- this.dataSource.paginator = this.paginator;
1170
- this.dataSource.sort = this.sort;
1171
- if (this.data.sortBy) {
1172
- this.dataSource.sort.active = this.data.sortBy.field;
1173
- this.dataSource.sort.direction = this.data.sortBy.order;
1174
- this.dataSource.sort.sortChange.emit();
1175
- }
1176
- this.dataSource.filterPredicate = (data, filter) => {
1177
- return this.data.displayedColumns.some((col) => {
1178
- if (col.filterPredicates) {
1179
- return col.filterPredicates.some((predicate) => {
1180
- const propertyValue = data[col.property];
1181
- if (!propertyValue || typeof propertyValue !== 'object') {
1182
- return false;
1183
- }
1184
- const predicateValue = propertyValue[predicate];
1185
- if (predicateValue === null || predicateValue === undefined) {
1186
- return false;
1187
- }
1188
- return String(predicateValue)
1189
- .trim()
1190
- .toLocaleLowerCase()
1191
- .includes(filter);
1192
- });
1193
- }
1194
- if (col.property && col.isFilterable) {
1195
- const propertyValue = data[col.property];
1196
- if (propertyValue === null || propertyValue === undefined) {
1197
- return false;
1198
- }
1199
- return String(propertyValue)
1200
- .trim()
1201
- .toLocaleLowerCase()
1202
- .includes(filter);
1203
- }
1204
- return false;
1205
- });
1206
- };
1207
- this.filterPredicate = this.dataSource.filterPredicate;
1208
- }
1209
- // Métodos com paginação
1210
- async loadItemsPaginated(navigation = 'reload', reset = false) {
1211
- if (reset && ['forward', 'reload'].includes(navigation)) {
1212
- this.lastDoc = null;
1213
- }
1214
- if (reset && ['backward', 'reload'].includes(navigation)) {
1215
- this.firstDoc = null;
1216
- }
1217
- const activeFilters = this.filtersForm.controls
1218
- .flatMap((control) => {
1219
- const group = control;
1220
- const selectedFilter = group.get('selectFilter')?.value;
1221
- if (!selectedFilter)
1222
- return [];
1223
- const arrange = selectedFilter.arrange;
1224
- if (arrange === 'filter') {
1225
- const filterValue = group.get('typeFilter')?.value;
1226
- if (!filterValue)
1227
- return [];
1228
- return {
1229
- arrange,
1230
- filter: {
1231
- property: selectedFilter.property,
1232
- filtering: filterValue,
1233
- },
1234
- dateFilter: undefined,
1235
- };
1236
- }
1237
- if (arrange === 'filterByDate') {
1238
- const initial = group.get('initialDate')?.value;
1239
- const final = group.get('finalDate')?.value;
1240
- if (initial && final) {
1241
- const [dayI, monthI, yearI] = initial.split('/');
1242
- const initialDate = new Date(`${monthI}/${dayI}/${yearI}`);
1243
- const [dayF, monthF, yearF] = final.split('/');
1244
- const finalDate = new Date(`${monthF}/${dayF}/${yearF}`);
1245
- finalDate.setHours(23, 59, 59);
1246
- return {
1247
- arrange,
1248
- filter: undefined,
1249
- dateFilter: {
1250
- initial: initialDate,
1251
- final: finalDate,
1252
- },
1253
- };
1254
- }
1255
- return [];
1256
- }
1257
- if (selectedFilter.hasOwnProperty('items') && arrange === 'equals') {
1258
- const selectedItems = group.get('selectItem')?.value;
1259
- if (Array.isArray(selectedItems) && selectedItems.length > 0) {
1260
- return selectedItems.map((item) => ({
1261
- arrange,
1262
- filter: {
1263
- property: item.property,
1264
- filtering: item.value,
1265
- },
1266
- dateFilter: undefined,
1267
- }));
1268
- }
1269
- }
1270
- return [];
1271
- })
1272
- .filter((f) => f && (f.filter?.filtering !== undefined || f.dateFilter));
1273
- this.arrange = {
1274
- filters: activeFilters,
1275
- sortBy: this.sortBy,
1276
- };
1277
- const paginated = {
1278
- batchSize: this.pageSize,
1279
- collection: this.data.collection,
1280
- doc: { lastDoc: this.lastDoc, firstDoc: this.firstDoc },
1281
- navigation,
1282
- arrange: this.arrange,
1283
- conditions: this.data.conditions,
1284
- size: this.totalItems,
1285
- filterFn: this.data.filterFn,
1286
- clientPageIndex: this.currentClientPageIndex,
1287
- };
1288
- const result = await this.tableService.getPaginated(paginated);
1289
- this.items = result.items;
1290
- await this.loadRelations();
1291
- await this.loadQueryLengths();
1292
- this.lastDoc = result.lastDoc;
1293
- this.firstDoc = result.firstDoc;
1294
- // Atualizar currentClientPageIndex se retornado pelo fallback
1295
- if (result.currentClientPageIndex !== undefined) {
1296
- this.currentClientPageIndex = result.currentClientPageIndex;
1297
- }
1298
- let sum = 0;
1299
- if (this.data.totalRef) {
1300
- for (const totalRef of this.data.totalRef) {
1301
- const totalRefDoc = await totalRef.ref.get();
1302
- const docData = totalRefDoc.data();
1303
- if (docData || result.filterLength) {
1304
- sum =
1305
- result.filterLength ??
1306
- (sum + (docData ? docData[totalRef.field] : 0));
1307
- }
1308
- }
1309
- this.totalItems = sum;
1310
- }
1311
- this.hasNextPage = result.hasNextPage;
1312
- this.dataSource = new MatTableDataSource(this.items);
1313
- this.filterPredicate = this.dataSource.filterPredicate;
1314
- }
1315
- async onPageChange(event) {
1316
- if (this.data.pagination === true && event) {
1317
- this.isLoading = true;
1318
- const previousPageIndex = event.previousPageIndex ?? 0;
1319
- const pageIndex = event.pageIndex;
1320
- const currentComponentPageSize = this.pageSize;
1321
- const eventPageSize = event.pageSize;
1322
- const totalItems = this.totalItems;
1323
- let navigationDirection;
1324
- let resetDocs = false;
1325
- let originalPageSize = null;
1326
- const lastPageIndex = Math.max(0, Math.ceil(totalItems / eventPageSize) - 1);
1327
- // Atualizar currentClientPageIndex sempre para o fallback
1328
- this.currentClientPageIndex = pageIndex;
1329
- if (previousPageIndex !== undefined && pageIndex > previousPageIndex) {
1330
- this.currentPageNumber++;
1331
- }
1332
- else if (previousPageIndex !== undefined &&
1333
- pageIndex < previousPageIndex) {
1334
- this.currentPageNumber = Math.max(1, this.currentPageNumber - 1);
1335
- }
1336
- if (eventPageSize !== currentComponentPageSize) {
1337
- console.log('Alterou a quantidade de elementos exibidos por página');
1338
- this.pageSize = eventPageSize;
1339
- navigationDirection = 'forward';
1340
- resetDocs = true;
1341
- this.currentClientPageIndex = 0;
1342
- }
1343
- else if (pageIndex === 0 &&
1344
- previousPageIndex !== undefined &&
1345
- pageIndex < previousPageIndex) {
1346
- console.log('Pulou para a primeira página');
1347
- navigationDirection = 'forward';
1348
- this.currentPageNumber = 1;
1349
- this.currentClientPageIndex = 0;
1350
- resetDocs = true;
1351
- }
1352
- else if (pageIndex === lastPageIndex &&
1353
- previousPageIndex !== undefined &&
1354
- pageIndex > previousPageIndex &&
1355
- pageIndex - previousPageIndex > 1) {
1356
- console.log('Pulou para a ultima página');
1357
- navigationDirection = 'backward';
1358
- resetDocs = true;
1359
- const itemsExpectedInLastPage = totalItems - lastPageIndex * eventPageSize;
1360
- if (itemsExpectedInLastPage > 0 &&
1361
- itemsExpectedInLastPage < eventPageSize) {
1362
- originalPageSize = this.pageSize;
1363
- this.pageSize = itemsExpectedInLastPage;
1364
- }
1365
- }
1366
- else if (previousPageIndex !== undefined &&
1367
- pageIndex > previousPageIndex) {
1368
- console.log('Procedendo');
1369
- navigationDirection = 'forward';
1370
- resetDocs = false;
1371
- }
1372
- else if (previousPageIndex !== undefined &&
1373
- pageIndex < previousPageIndex) {
1374
- console.log('Retrocedendo.');
1375
- navigationDirection = 'backward';
1376
- resetDocs = false;
1377
- }
1378
- else if (previousPageIndex !== undefined &&
1379
- pageIndex === previousPageIndex) {
1380
- console.log('Recarregando.');
1381
- navigationDirection = 'reload';
1382
- resetDocs = false;
1383
- }
1384
- else if (previousPageIndex === undefined && pageIndex === 0) {
1385
- console.log('Evento inicial do paginador para pág 0. ngOnInit carregou.');
1386
- this.isLoading = false;
1387
- if (event)
1388
- this.pageEvent = event;
1389
- return;
1390
- }
1391
- else {
1392
- console.warn('INESPERADO! Condição de navegação não tratada:', event);
1393
- this.isLoading = false;
1394
- if (event)
1395
- this.pageEvent = event;
1396
- return;
1397
- }
1398
- if (navigationDirection) {
1399
- try {
1400
- await this.loadItemsPaginated(navigationDirection, resetDocs);
1401
- }
1402
- catch (error) {
1403
- console.error('Erro ao carregar itens paginados:', error);
1404
- }
1405
- finally {
1406
- if (originalPageSize !== null) {
1407
- this.pageSize = originalPageSize;
1408
- }
1409
- }
1410
- }
1411
- if (event)
1412
- this.pageEvent = event;
1413
- this.isLoading = false;
1414
- }
1415
- }
1416
- // Outros métodos
1417
- applyFilter(value) {
1418
- // Sem paginação
1419
- if (this.data.pagination === false) {
1420
- this.dataSource.filter = String(value).trim().toLowerCase();
1421
- }
1422
- // Com paginação
1423
- if (this.data.pagination === true) {
1424
- this.filterValue = value;
1425
- this.filterSubject.next(this.filterValue);
1426
- }
1427
- }
1428
- goToDetails(row) {
1429
- if (this.data.isNotClickable) {
1430
- return;
1431
- }
1432
- const urlPath = this.data.url || this.data.name;
1433
- const url = this.router.serializeUrl(this.router.createUrlTree([`/${urlPath}`, row.id]));
1434
- window.open(url, '_blank');
1435
- }
1436
- async getRelation(params) {
1437
- try {
1438
- let snapshot;
1439
- if (params.id !== '' &&
1440
- params.id !== undefined &&
1441
- params.collection !== undefined &&
1442
- params.collection !== '') {
1443
- snapshot = await firstValueFrom(this.firestore.collection(params.collection).doc(params.id).get());
1444
- }
1445
- if (snapshot && snapshot.exists) {
1446
- const data = snapshot.data();
1447
- return data?.[params.newProperty] ?? '';
1448
- }
1449
- return '';
1450
- }
1451
- catch (e) {
1452
- console.log(e);
1453
- return '';
1454
- }
1455
- }
1456
- async loadRelations() {
1457
- const relationPromises = this.data.displayedColumns
1458
- .filter((col) => col.relation)
1459
- .flatMap((col) => this.items.map(async (item) => {
1460
- if (col.relation) {
1461
- item[col.property] = await this.getRelation({
1462
- id: item[col.relation.property],
1463
- collection: col.relation.collection,
1464
- newProperty: col.relation.newProperty,
1465
- });
1466
- }
1467
- }));
1468
- await Promise.all(relationPromises);
1469
- }
1470
- async getQueryLength(params) {
1471
- const snapshot = await this.firestore
1472
- .collection(params.relation.collection)
1473
- .ref.where(params.relation.property, params.relation.operator, params.item[params.relation.value])
1474
- .get();
1475
- return snapshot.size;
1476
- }
1477
- async loadQueryLengths() {
1478
- const lengthPromises = this.data.displayedColumns
1479
- .filter((col) => col.queryLength)
1480
- .flatMap((col) => this.items.map(async (item) => {
1481
- if (col.queryLength) {
1482
- item[col.property] = await this.getQueryLength({
1483
- item: item,
1484
- relation: col.queryLength,
1485
- });
1486
- }
1487
- }));
1488
- await Promise.all(lengthPromises);
1489
- }
1490
- filterItems() {
1491
- if (this.data.conditions) {
1492
- this.data.conditions.forEach((cond) => {
1493
- this.items = this.items.filter((item) => {
1494
- const operatorFunction = this.tableService.operators[cond.operator];
1495
- if (operatorFunction) {
1496
- return operatorFunction(item[cond.firestoreProperty], cond.dashProperty);
1497
- }
1498
- return false;
1499
- });
1500
- });
1501
- }
1502
- }
1503
- // Filtro de data
1504
- async search() {
1505
- if (this.selectSort.value) {
1506
- if (this.selectSort.value.arrange === 'ascending') {
1507
- this.sortBy = {
1508
- field: this.selectSort.value.property,
1509
- order: 'asc',
1510
- };
1511
- }
1512
- if (this.selectSort.value.arrange === 'descending') {
1513
- this.sortBy = {
1514
- field: this.selectSort.value.property,
1515
- order: 'desc',
1516
- };
1517
- }
1518
- }
1519
- await this.loadItemsPaginated('reload', true);
1520
- this.currentArrange =
1521
- this.filtersForm.length > 0
1522
- ? this.filtersForm.at(0).get('selectFilter')?.value?.arrange ?? ''
1523
- : '';
1524
- this.paginator.firstPage();
1525
- }
1526
- async resetFilter() {
1527
- if (!this.data.pagination) {
1528
- this.dataSource.filter = '';
1529
- if (this.filterPredicate) {
1530
- this.dataSource.filterPredicate = this.filterPredicate;
1531
- }
1532
- }
1533
- else {
1534
- this.dataSource.filter = '';
1535
- if (this.filterPredicate) {
1536
- this.dataSource.filterPredicate = this.filterPredicate;
1537
- }
1538
- }
1539
- this.filtersForm.clear();
1540
- this.addFilter();
1541
- this.selectSort.patchValue('');
1542
- this.sortBy = {
1543
- order: this.data.sortBy ? this.data.sortBy.order : 'desc',
1544
- field: this.data.sortBy ? this.data.sortBy.field : 'createdAt',
1545
- };
1546
- await this.loadItemsPaginated('reload', true);
1547
- this.currentArrange = '';
1548
- this.paginator.firstPage();
1549
- }
1550
- // Método público para recarregar a tabela
1551
- async reloadTable() {
1552
- if (this.data.pagination) {
1553
- await this.loadItemsPaginated('reload', true);
1554
- this.paginator.firstPage();
1555
- }
1556
- else {
1557
- await this.loadItems();
1558
- }
1559
- }
1560
- updateDisplayedColumns() {
1561
- if (this.dataSource) {
1562
- this.dataSource = new MatTableDataSource([]);
1563
- }
1564
- this.columnProperties = this.data.displayedColumns.map((column) => {
1565
- return column.property;
1566
- });
1567
- this.dropdownItems = [];
1568
- this.sortableDropdownItems = [];
1569
- this.data.displayedColumns.forEach((col) => {
1570
- if (col.isFilterable) {
1571
- this.dropdownItems.push({
1572
- ...col,
1573
- arrange: 'filter',
1574
- title: col.title,
1575
- });
1576
- }
1577
- if (col.isSortable) {
1578
- this.sortableDropdownItems.push({
1579
- ...col,
1580
- arrange: 'ascending',
1581
- title: col.title + ': crescente',
1582
- });
1583
- this.sortableDropdownItems.push({
1584
- ...col,
1585
- arrange: 'descending',
1586
- title: col.title + ': decrescente',
1587
- });
1588
- }
1589
- if (col.isFilterableByDate) {
1590
- this.dropdownItems.push({
1591
- ...col,
1592
- arrange: 'filterByDate',
1593
- title: col.title + ': filtro por data',
1594
- });
1595
- }
1596
- });
1597
- if (this.data.filterableOptions) {
1598
- this.data.filterableOptions.forEach((option) => this.dropdownItems.push({ ...option, arrange: 'equals' }));
1599
- }
1600
- }
1601
- isString(value) {
1602
- return typeof value === 'string';
1603
- }
1604
- // Métodos para controle do tooltip
1605
- onCellMouseEnter(event, row, col) {
1606
- // Só mostrar tooltip se a coluna tiver charLimit definido
1607
- if (!col.charLimit) {
1608
- return;
1609
- }
1610
- const fullValue = this.getDisplayValue(col, row, true);
1611
- // Só mostrar tooltip se o valor completo for maior que o limite
1612
- if (fullValue.length <= col.charLimit) {
1613
- return;
1614
- }
1615
- this.hoveredCell = { row, col };
1616
- this.tooltipContent = fullValue;
1617
- // Definir posição do tooltip
1618
- this.tooltipPosition = {
1619
- x: event.clientX + 10,
1620
- y: event.clientY - 10,
1621
- };
1622
- // Timeout para mostrar o tooltip
1623
- this.tooltipTimeout = setTimeout(() => {
1624
- if (this.hoveredCell &&
1625
- this.hoveredCell.row === row &&
1626
- this.hoveredCell.col === col) {
1627
- this.showTooltip = true;
1628
- }
1629
- }, 500);
1630
- }
1631
- onCellMouseLeave() {
1632
- if (this.tooltipTimeout) {
1633
- clearTimeout(this.tooltipTimeout);
1634
- this.tooltipTimeout = null;
1635
- }
1636
- this.showTooltip = false;
1637
- this.hoveredCell = null;
1638
- this.tooltipContent = '';
1639
- }
1640
- onCellMouseMove(event) {
1641
- if (this.showTooltip) {
1642
- this.tooltipPosition = {
1643
- x: event.clientX + 10,
1644
- y: event.clientY - 10,
1645
- };
1646
- }
1647
- }
1648
- // Métodos para inversão vertical dos tabs
1649
- getTabGroups(tabs) {
1650
- if (!tabs || tabs.length === 0)
1651
- return [];
1652
- const totalGroups = Math.ceil(tabs.length / 6);
1653
- const groups = [];
1654
- // Criar array de índices invertidos (último grupo primeiro)
1655
- for (let i = totalGroups - 1; i >= 0; i--) {
1656
- groups.push(i);
1657
- }
1658
- return groups;
1659
- }
1660
- getTabGroup(tabs, groupIndex) {
1661
- if (!tabs || tabs.length === 0)
1662
- return [];
1663
- const startIndex = groupIndex * 6;
1664
- const endIndex = Math.min(startIndex + 6, tabs.length);
1665
- return tabs.slice(startIndex, endIndex);
1666
- }
1667
- getRealTabIndex(groupIndex, tabIndexInGroup) {
1668
- if (!this.data.tabs?.tabsData)
1669
- return 0;
1670
- const totalGroups = Math.ceil(this.data.tabs.tabsData.length / 6);
1671
- const realGroupIndex = totalGroups - 1 - groupIndex;
1672
- return realGroupIndex * 6 + tabIndexInGroup;
1673
- }
1674
- onTableSelected(i, j) {
1675
- if (!this.data.tabs?.tabsData || !this.data.tabs.method)
1676
- return;
1677
- this.selectedTab = this.getRealTabIndex(i, j);
1678
- const tab = this.data.tabs.tabsData[this.selectedTab];
1679
- if (tab) {
1680
- this.data.tabs.method(tab, this.selectedTab);
1681
- }
1682
- }
1683
- isTabSelected(originalIndex) {
1684
- return this.selectedTab === originalIndex;
1685
- }
1686
- shouldShowActionButton() {
1687
- if (!this.data?.actionButton) {
1688
- return false;
1689
- }
1690
- if (!this.data.actionButton.condition) {
1691
- return true;
1692
- }
1693
- try {
1694
- return this.data.actionButton.condition(null) ?? true;
1695
- }
1696
- catch {
1697
- return true;
1698
- }
1699
- }
1700
- }
1701
- TableComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableComponent, deps: [{ token: i1$1.Router }, { token: TableService }, { token: i1.AngularFirestore }], target: i0.ɵɵFactoryTarget.Component });
1702
- TableComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: TableComponent, selector: "lib-table", inputs: { data: "data", downloadTable: "downloadTable" }, viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], ngImport: i0, template: "<div *ngIf=\"data\" class=\"card-body\">\r\n <div class=\"flex flex-col justify-between gap-6\">\r\n <!-- UNIFIED CONTROL PANEL: FILTERS, SORT & ACTIONS -->\r\n <div\r\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\r\n *ngIf=\"\r\n data.pagination === true &&\r\n (dropdownItems.length > 0 ||\r\n sortableDropdownItems.length > 0 ||\r\n data.actionButton)\r\n \"\r\n >\r\n <!-- PANEL HEADER: Title, Custom Action, and Global Actions -->\r\n <div\r\n 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\"\r\n >\r\n <!-- Left Side: Title & Main Action Button -->\r\n <div class=\"flex flex-wrap items-center gap-4\">\r\n <div class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-filter text-xl text-blue-500\"></i>\r\n <span class=\"text-lg font-semibold text-gray-700\"\r\n >Filtros e A\u00E7\u00F5es</span\r\n >\r\n </div>\r\n <button\r\n *ngIf=\"data.actionButton && data.actionButton.condition\"\r\n [ngClass]=\"\r\n (data.actionButton.colorClass || 'bg-blue-500') +\r\n ' flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\r\n \"\r\n [routerLink]=\"data.actionButton.routerLink\"\r\n (click)=\"\r\n data.actionButton.method ? data.actionButton.method($event) : null\r\n \"\r\n >\r\n <i\r\n *ngIf=\"data.actionButton.icon\"\r\n [class]=\"data.actionButton.icon\"\r\n ></i>\r\n {{ data.actionButton.label }}\r\n </button>\r\n </div>\r\n\r\n <!-- Right Side: Search, Reset, Export -->\r\n <div\r\n class=\"flex flex-wrap gap-3\"\r\n *ngIf=\"\r\n this.hasFilterableColumn === true || this.hasSortableColumn === true\r\n \"\r\n >\r\n <button\r\n (click)=\"search()\"\r\n type=\"button\"\r\n 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\"\r\n matTooltip=\"Aplicar filtros\"\r\n >\r\n <i class=\"fa fa-search\"></i>\r\n Pesquisar\r\n </button>\r\n\r\n <button\r\n (click)=\"resetFilter()\"\r\n 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\"\r\n matTooltip=\"Limpar filtros\"\r\n >\r\n <i class=\"fas fa-redo-alt\"></i>\r\n Resetar\r\n </button>\r\n\r\n <button\r\n *ngIf=\"data.download !== false && downloadTable\"\r\n 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\"\r\n matTooltipPosition=\"above\"\r\n matTooltip=\"Exportar Tabela\"\r\n [disabled]=\"\r\n this.dataSource && this.dataSource.filteredData.length <= 0\r\n \"\r\n (click)=\"\r\n $any(arrange) && downloadTable !== undefined\r\n ? downloadTable($any(arrange), data.conditions || [])\r\n : null\r\n \"\r\n >\r\n <i class=\"fa fa-download\"></i>\r\n Exportar\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- FILTERS CONTENT (WITH REFINEMENTS) -->\r\n <div class=\"mb-4 space-y-3\" *ngIf=\"filtersForm.controls.length > 0\">\r\n <div\r\n [formGroup]=\"$any(filterGroup)\"\r\n *ngFor=\"let filterGroup of filtersForm.controls; let i = index\"\r\n class=\"flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 p-2\"\r\n >\r\n <!-- FILTER TYPE SELECTOR -->\r\n <div class=\"min-w-[200px] flex-1\" *ngIf=\"dropdownItems.length > 0\">\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label>Tipo de filtro</mat-label>\r\n <mat-select\r\n placeholder=\"Selecione o tipo...\"\r\n formControlName=\"selectFilter\"\r\n (selectionChange)=\"onSelectFilterChange()\"\r\n >\r\n <mat-option *ngFor=\"let item of dropdownItems\" [value]=\"item\">\r\n <div class=\"flex items-center gap-2\">\r\n <i\r\n [class]=\"item.icon || 'fa fa-filter'\"\r\n class=\"text-sm text-blue-500\"\r\n ></i>\r\n <span>{{ item.title }}</span>\r\n </div>\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- TEXT FILTER -->\r\n <div\r\n class=\"min-w-[200px] flex-1\"\r\n *ngIf=\"\r\n $any(filterGroup).get('selectFilter')?.value?.arrange === 'filter'\r\n \"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-search text-gray-400\"></i>\r\n <span>{{\r\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\r\n \"Filtrar\"\r\n }}</span>\r\n </mat-label>\r\n <input\r\n (keyup.enter)=\"search()\"\r\n formControlName=\"typeFilter\"\r\n matInput\r\n placeholder=\"Digite para filtrar...\"\r\n #input\r\n />\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- DROPDOWN FILTER -->\r\n <div\r\n class=\"min-w-[200px] flex-1\"\r\n *ngIf=\"\r\n $any(filterGroup).get('selectFilter')?.value &&\r\n $any(filterGroup)\r\n .get('selectFilter')\r\n ?.value.hasOwnProperty('items')\r\n \"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label>{{\r\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\r\n \"Selecione\"\r\n }}</mat-label>\r\n <mat-select\r\n placeholder=\"Selecione...\"\r\n formControlName=\"selectItem\"\r\n multiple\r\n >\r\n <mat-option\r\n *ngFor=\"\r\n let item of $any(filterGroup).get('selectFilter')?.value\r\n .items\r\n \"\r\n [value]=\"item\"\r\n >\r\n {{ item.label }}\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- DATE FILTER -->\r\n <div\r\n class=\"min-w-[340px] flex-auto\"\r\n *ngIf=\"\r\n $any(filterGroup).get('selectFilter')?.value?.arrange ===\r\n 'filterByDate'\r\n \"\r\n >\r\n <div\r\n class=\"flex flex-col items-stretch gap-3 sm:flex-row sm:items-center\"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-calendar text-gray-400\"></i>\r\n <span>Data Inicial</span>\r\n </mat-label>\r\n <input\r\n matInput\r\n (keyup.enter)=\"search()\"\r\n formControlName=\"initialDate\"\r\n placeholder=\"DD/MM/AAAA\"\r\n maxlength=\"10\"\r\n />\r\n </mat-form-field>\r\n\r\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-calendar text-gray-400\"></i>\r\n <span>Data Final</span>\r\n </mat-label>\r\n <input\r\n (keyup.enter)=\"search()\"\r\n matInput\r\n formControlName=\"finalDate\"\r\n placeholder=\"DD/MM/AAAA\"\r\n maxlength=\"10\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n\r\n <!-- REMOVE FILTER BUTTON -->\r\n <div *ngIf=\"filtersForm.length > 1\" class=\"ml-auto flex-shrink-0\">\r\n <button\r\n (click)=\"removeFilter(i)\"\r\n class=\"flex h-10 w-10 items-center justify-center rounded-full transition-colors duration-300 hover:bg-red-100\"\r\n matTooltip=\"Remover filtro\"\r\n >\r\n <i class=\"fa fa-trash text-red-500 hover:text-red-600\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- PANEL FOOTER: Add Filter & Sort -->\r\n <div\r\n class=\"-mb-2 flex flex-col items-center justify-between gap-4 border-t border-gray-200 pt-4 sm:flex-row\"\r\n >\r\n <!-- Add Filter Button -->\r\n <div *ngIf=\"dropdownItems.length > 0\">\r\n <button\r\n (click)=\"addFilter()\"\r\n 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\"\r\n matTooltip=\"Adicionar novo filtro\"\r\n >\r\n <i class=\"fa fa-plus mr-2\"></i>\r\n Adicionar Filtro\r\n </button>\r\n </div>\r\n\r\n <!-- Sort Dropdown -->\r\n <div\r\n class=\"w-full sm:w-auto sm:min-w-[250px]\"\r\n *ngIf=\"sortableDropdownItems.length > 0\"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label>Ordenar por</mat-label>\r\n <mat-select placeholder=\"Selecione...\" [formControl]=\"selectSort\">\r\n <mat-option\r\n *ngFor=\"let item of sortableDropdownItems\"\r\n [value]=\"item\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-sort-alpha-down text-cyan-600\"></i>\r\n <span>{{ item.title }}</span>\r\n </div>\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- SIMPLE SEARCH (for non-paginated tables) -->\r\n <div\r\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\r\n *ngIf=\"data.pagination === false\"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-search text-blue-500\"></i>\r\n Buscar\r\n </mat-label>\r\n <input\r\n matInput\r\n (keyup.enter)=\"search()\"\r\n (keyup)=\"applyFilter(filterInput.value)\"\r\n placeholder=\"Digite para filtrar...\"\r\n #filterInput\r\n />\r\n <mat-icon matSuffix class=\"text-gray-500\">search</mat-icon>\r\n </mat-form-field>\r\n <button\r\n *ngIf=\"data.actionButton\"\r\n [ngClass]=\"\r\n (data.actionButton.colorClass || 'bg-blue-500') +\r\n ' float-right flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\r\n \"\r\n [routerLink]=\"data.actionButton.routerLink\"\r\n (click)=\"\r\n data.actionButton.method ? data.actionButton.method($event) : null\r\n \"\r\n >\r\n <i *ngIf=\"data.actionButton.icon\" [class]=\"data.actionButton.icon\"></i>\r\n {{ data.actionButton.label }}\r\n </button>\r\n </div>\r\n\r\n <div class=\"flex flex-col\">\r\n <div\r\n class=\"mx-auto flex flex-col\"\r\n *ngIf=\"data.tabs && data.tabs.tabsData && data.tabs.tabsData.length > 0\"\r\n >\r\n <!-- Calcular quantos grupos de 6 tabs existem -->\r\n <ng-container\r\n *ngFor=\"\r\n let groupIndex of getTabGroups(data.tabs.tabsData);\r\n let i = index\r\n \"\r\n >\r\n <div class=\"mx-auto flex flex-row\">\r\n <ng-container\r\n *ngFor=\"\r\n let tab of getTabGroup(data.tabs.tabsData, groupIndex);\r\n let j = index\r\n \"\r\n >\r\n <button\r\n class=\"border-2 border-gray-300 bg-gray-200 px-4 py-2 font-medium transition hover:brightness-95\"\r\n [ngClass]=\"\r\n isTabSelected(getRealTabIndex(i, j))\r\n ? 'border-b-0 brightness-110'\r\n : ''\r\n \"\r\n (click)=\"onTableSelected(i, j)\"\r\n >\r\n {{ tab.label }}\r\n <span\r\n *ngIf=\"tab.counter !== undefined\"\r\n class=\"ml-2 text-xs font-bold\"\r\n [ngClass]=\"tab.counterClass\"\r\n >\r\n {{ tab.counter }}\r\n </span>\r\n </button>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n </div>\r\n <div class=\"mat-elevation-z8 w-full overflow-x-auto rounded-xl\">\r\n <table\r\n mat-table\r\n [dataSource]=\"dataSource\"\r\n matSort\r\n #sort=\"matSort\"\r\n matSortActive=\"createdAt\"\r\n matSortDirection=\"desc\"\r\n >\r\n <ng-container\r\n *ngFor=\"let col of data.displayedColumns\"\r\n matColumnDef=\"{{ col.property }}\"\r\n >\r\n <ng-container *matHeaderCellDef>\r\n <!-- IF THE COLUMN IS NOT SORTABLE, THEN DON'T SHOW THE SORT BUTTONS -->\r\n <th\r\n *ngIf=\"!col.isSortable || data.pagination === true\"\r\n mat-header-cell\r\n [ngClass]=\"\r\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\r\n (data?.color?.text ? ' ' + $any(data.color).text : '')\r\n \"\r\n >\r\n {{ col.title }}\r\n </th>\r\n <!-- IF THE COLUMN IS SORTABLE, THEN SHOW THE SORT BUTTONS -->\r\n <th\r\n *ngIf=\"col.isSortable && data.pagination === false\"\r\n mat-header-cell\r\n mat-sort-header\r\n [ngClass]=\"\r\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\r\n (data?.color?.text ? ' ' + $any(data.color).text : '')\r\n \"\r\n >\r\n {{ col.title }}\r\n </th>\r\n <td\r\n mat-cell\r\n *matCellDef=\"let row\"\r\n (click)=\"col.method ? col.method(row) : null\"\r\n (mouseenter)=\"onCellMouseEnter($event, row, col)\"\r\n (mouseleave)=\"onCellMouseLeave()\"\r\n (mousemove)=\"onCellMouseMove($event)\"\r\n >\r\n <!-- CHECK IF THE COLUMN MUST BE DISPLAYED -->\r\n <span *ngIf=\"!col.image && !col.iconClass && !col.method\">\r\n <ng-container>\r\n <span\r\n *ngIf=\"\r\n col.charLimit &&\r\n row[col.property] &&\r\n row[col.property].length > col.charLimit;\r\n else withinLimit\r\n \"\r\n >\r\n <a\r\n *ngIf=\"col.hasLink === true\"\r\n [href]=\"row[col.property]\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row) }}\r\n </a>\r\n <a\r\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\r\n [href]=\"col.hasLink\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row) }}\r\n </a>\r\n <span\r\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\r\n >\r\n {{ getDisplayValue(col, row) }}\r\n </span>\r\n </span>\r\n </ng-container>\r\n <ng-template #withinLimit>\r\n <a\r\n *ngIf=\"col.hasLink === true\"\r\n [href]=\"row[col.property]\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row, true) }}\r\n </a>\r\n <a\r\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\r\n [href]=\"col.hasLink\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row, true) }}\r\n </a>\r\n <span\r\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\r\n >\r\n {{ getDisplayValue(col, row, true) }}\r\n </span>\r\n </ng-template>\r\n </span>\r\n <!------------------- IMAGE ------------------>\r\n <img\r\n *ngIf=\"\r\n col.image && col.image.path && !col.iconClass && !col.method\r\n \"\r\n [src]=\"col.image.path + '/' + row[col.property]\"\r\n [ngClass]=\"col.image.class\"\r\n alt=\"Imagem\"\r\n />\r\n <img\r\n *ngIf=\"\r\n col.image && col.image.url && !col.iconClass && !col.method\r\n \"\r\n [src]=\"row[col.property]\"\r\n [ngClass]=\"col.image.class\"\r\n alt=\"Imagem\"\r\n />\r\n <ng-container *ngIf=\"col.iconClass\">\r\n <button\r\n *ngFor=\"let iconClass of col.iconClass\"\r\n (click)=\"\r\n iconClass.buttonMethod\r\n ? iconClass.buttonMethod(row, $event)\r\n : $event.stopPropagation()\r\n \"\r\n >\r\n <span\r\n [ngClass]=\"iconClass.class\"\r\n *ngIf=\"\r\n iconClass.condition === undefined ||\r\n (iconClass.condition !== undefined &&\r\n $any(iconClass.condition)(row))\r\n \"\r\n >{{ iconClass.text }}</span\r\n >\r\n </button>\r\n </ng-container>\r\n </td>\r\n </ng-container>\r\n </ng-container>\r\n\r\n <tr mat-header-row *matHeaderRowDef=\"columnProperties\"></tr>\r\n <tr\r\n [ngClass]=\"{\r\n 'example-element-row': data.isNotClickable === true,\r\n 'example-element-row cursor-pointer': !data.isNotClickable\r\n }\"\r\n mat-row\r\n *matRowDef=\"let row; columns: columnProperties\"\r\n (click)=\"goToDetails(row)\"\r\n ></tr>\r\n\r\n <!-- ROW SHOWN WHEN THERE IS NO MATCHING DATA. -->\r\n <tr class=\"mat-row\" *matNoDataRow>\r\n <td *ngIf=\"!isLoading\" class=\"mat-cell p-4\" colspan=\"4\">\r\n Nenhum resultado encontrado para a busca\r\n </td>\r\n </tr>\r\n </table>\r\n\r\n <div class=\"flex justify-center\" *ngIf=\"isLoading\">\r\n <mat-spinner></mat-spinner>\r\n </div>\r\n\r\n <div class=\"paginator-container\">\r\n <mat-paginator\r\n #paginator\r\n [pageSizeOptions]=\"[25, 50, 100]\"\r\n [pageSize]=\"pageSize\"\r\n [length]=\"totalItems\"\r\n showFirstLastButtons\r\n aria-label=\"Select page of periodic elements\"\r\n (page)=\"onPageChange($event)\"\r\n [ngClass]=\"{\r\n 'hide-length':\r\n ['filter', 'filterByDate', 'equals'].includes(\r\n this.currentArrange\r\n ) || this.data.filterFn,\r\n 'hide-next-button': !hasNextPage && data.pagination === true,\r\n 'hide-last-button':\r\n (!hasNextPage && data.pagination === true) || this.data.filterFn\r\n }\"\r\n >\r\n </mat-paginator>\r\n <div\r\n *ngIf=\"\r\n !isLoading &&\r\n dataSource?.data &&\r\n dataSource.data.length > 0 &&\r\n data?.filterFn\r\n \"\r\n class=\"page-number-display\"\r\n >\r\n {{ currentPageNumber }}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- TOOLTIP PERSONALIZADO -->\r\n <div\r\n *ngIf=\"showTooltip\"\r\n class=\"fixed z-50 max-w-md break-words rounded-lg bg-gray-800 px-3 py-2 text-sm text-white shadow-lg\"\r\n [style.left.px]=\"tooltipPosition.x\"\r\n [style.top.px]=\"tooltipPosition.y\"\r\n [style.pointer-events]=\"'none'\"\r\n >\r\n {{ tooltipContent }}\r\n </div>\r\n</div>\r\n", styles: ["::ng-deep .hide-length .mat-mdc-paginator-range-label{display:none}::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{visibility:hidden}::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{visibility:hidden}::ng-deep .mat-mdc-text-field-wrapper.mdc-text-field.ng-tns-c162-1.mdc-text-field--filled{width:25dvw}::ng-deep .custom-filter .mat-mdc-text-field-wrapper{width:20dvw;max-width:300px}\n"], dependencies: [{ kind: "directive", type: i4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i5.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i5.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i5.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i5.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i5.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: i6.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i6.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i6.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i6.MatColumnDef, selector: "[matColumnDef]", inputs: ["sticky", "matColumnDef"] }, { kind: "directive", type: i6.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i6.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i6.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i6.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i6.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i6.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: i6.MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "component", type: i7.MatPaginator, selector: "mat-paginator", inputs: ["disabled"], exportAs: ["matPaginator"] }, { kind: "directive", type: i8.MatSort, selector: "[matSort]", inputs: ["matSortDisabled", "matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i8.MatSortHeader, selector: "[mat-sort-header]", inputs: ["disabled", "mat-sort-header", "arrowPosition", "start", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "component", type: i9.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i9.MatLabel, selector: "mat-label" }, { kind: "directive", type: i9.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i10.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: i11.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator"], exportAs: ["matSelect"] }, { kind: "component", type: i12.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { kind: "directive", type: i13.MatTooltip, selector: "[matTooltip]", exportAs: ["matTooltip"] }, { kind: "component", type: i14.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i15.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] });
1703
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableComponent, decorators: [{
1704
- type: Component,
1705
- args: [{ selector: 'lib-table', template: "<div *ngIf=\"data\" class=\"card-body\">\r\n <div class=\"flex flex-col justify-between gap-6\">\r\n <!-- UNIFIED CONTROL PANEL: FILTERS, SORT & ACTIONS -->\r\n <div\r\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\r\n *ngIf=\"\r\n data.pagination === true &&\r\n (dropdownItems.length > 0 ||\r\n sortableDropdownItems.length > 0 ||\r\n data.actionButton)\r\n \"\r\n >\r\n <!-- PANEL HEADER: Title, Custom Action, and Global Actions -->\r\n <div\r\n 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\"\r\n >\r\n <!-- Left Side: Title & Main Action Button -->\r\n <div class=\"flex flex-wrap items-center gap-4\">\r\n <div class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-filter text-xl text-blue-500\"></i>\r\n <span class=\"text-lg font-semibold text-gray-700\"\r\n >Filtros e A\u00E7\u00F5es</span\r\n >\r\n </div>\r\n <button\r\n *ngIf=\"data.actionButton && data.actionButton.condition\"\r\n [ngClass]=\"\r\n (data.actionButton.colorClass || 'bg-blue-500') +\r\n ' flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\r\n \"\r\n [routerLink]=\"data.actionButton.routerLink\"\r\n (click)=\"\r\n data.actionButton.method ? data.actionButton.method($event) : null\r\n \"\r\n >\r\n <i\r\n *ngIf=\"data.actionButton.icon\"\r\n [class]=\"data.actionButton.icon\"\r\n ></i>\r\n {{ data.actionButton.label }}\r\n </button>\r\n </div>\r\n\r\n <!-- Right Side: Search, Reset, Export -->\r\n <div\r\n class=\"flex flex-wrap gap-3\"\r\n *ngIf=\"\r\n this.hasFilterableColumn === true || this.hasSortableColumn === true\r\n \"\r\n >\r\n <button\r\n (click)=\"search()\"\r\n type=\"button\"\r\n 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\"\r\n matTooltip=\"Aplicar filtros\"\r\n >\r\n <i class=\"fa fa-search\"></i>\r\n Pesquisar\r\n </button>\r\n\r\n <button\r\n (click)=\"resetFilter()\"\r\n 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\"\r\n matTooltip=\"Limpar filtros\"\r\n >\r\n <i class=\"fas fa-redo-alt\"></i>\r\n Resetar\r\n </button>\r\n\r\n <button\r\n *ngIf=\"data.download !== false && downloadTable\"\r\n 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\"\r\n matTooltipPosition=\"above\"\r\n matTooltip=\"Exportar Tabela\"\r\n [disabled]=\"\r\n this.dataSource && this.dataSource.filteredData.length <= 0\r\n \"\r\n (click)=\"\r\n $any(arrange) && downloadTable !== undefined\r\n ? downloadTable($any(arrange), data.conditions || [])\r\n : null\r\n \"\r\n >\r\n <i class=\"fa fa-download\"></i>\r\n Exportar\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- FILTERS CONTENT (WITH REFINEMENTS) -->\r\n <div class=\"mb-4 space-y-3\" *ngIf=\"filtersForm.controls.length > 0\">\r\n <div\r\n [formGroup]=\"$any(filterGroup)\"\r\n *ngFor=\"let filterGroup of filtersForm.controls; let i = index\"\r\n class=\"flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 p-2\"\r\n >\r\n <!-- FILTER TYPE SELECTOR -->\r\n <div class=\"min-w-[200px] flex-1\" *ngIf=\"dropdownItems.length > 0\">\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label>Tipo de filtro</mat-label>\r\n <mat-select\r\n placeholder=\"Selecione o tipo...\"\r\n formControlName=\"selectFilter\"\r\n (selectionChange)=\"onSelectFilterChange()\"\r\n >\r\n <mat-option *ngFor=\"let item of dropdownItems\" [value]=\"item\">\r\n <div class=\"flex items-center gap-2\">\r\n <i\r\n [class]=\"item.icon || 'fa fa-filter'\"\r\n class=\"text-sm text-blue-500\"\r\n ></i>\r\n <span>{{ item.title }}</span>\r\n </div>\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- TEXT FILTER -->\r\n <div\r\n class=\"min-w-[200px] flex-1\"\r\n *ngIf=\"\r\n $any(filterGroup).get('selectFilter')?.value?.arrange === 'filter'\r\n \"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-search text-gray-400\"></i>\r\n <span>{{\r\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\r\n \"Filtrar\"\r\n }}</span>\r\n </mat-label>\r\n <input\r\n (keyup.enter)=\"search()\"\r\n formControlName=\"typeFilter\"\r\n matInput\r\n placeholder=\"Digite para filtrar...\"\r\n #input\r\n />\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- DROPDOWN FILTER -->\r\n <div\r\n class=\"min-w-[200px] flex-1\"\r\n *ngIf=\"\r\n $any(filterGroup).get('selectFilter')?.value &&\r\n $any(filterGroup)\r\n .get('selectFilter')\r\n ?.value.hasOwnProperty('items')\r\n \"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label>{{\r\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\r\n \"Selecione\"\r\n }}</mat-label>\r\n <mat-select\r\n placeholder=\"Selecione...\"\r\n formControlName=\"selectItem\"\r\n multiple\r\n >\r\n <mat-option\r\n *ngFor=\"\r\n let item of $any(filterGroup).get('selectFilter')?.value\r\n .items\r\n \"\r\n [value]=\"item\"\r\n >\r\n {{ item.label }}\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- DATE FILTER -->\r\n <div\r\n class=\"min-w-[340px] flex-auto\"\r\n *ngIf=\"\r\n $any(filterGroup).get('selectFilter')?.value?.arrange ===\r\n 'filterByDate'\r\n \"\r\n >\r\n <div\r\n class=\"flex flex-col items-stretch gap-3 sm:flex-row sm:items-center\"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-calendar text-gray-400\"></i>\r\n <span>Data Inicial</span>\r\n </mat-label>\r\n <input\r\n matInput\r\n (keyup.enter)=\"search()\"\r\n formControlName=\"initialDate\"\r\n placeholder=\"DD/MM/AAAA\"\r\n maxlength=\"10\"\r\n />\r\n </mat-form-field>\r\n\r\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-calendar text-gray-400\"></i>\r\n <span>Data Final</span>\r\n </mat-label>\r\n <input\r\n (keyup.enter)=\"search()\"\r\n matInput\r\n formControlName=\"finalDate\"\r\n placeholder=\"DD/MM/AAAA\"\r\n maxlength=\"10\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n\r\n <!-- REMOVE FILTER BUTTON -->\r\n <div *ngIf=\"filtersForm.length > 1\" class=\"ml-auto flex-shrink-0\">\r\n <button\r\n (click)=\"removeFilter(i)\"\r\n class=\"flex h-10 w-10 items-center justify-center rounded-full transition-colors duration-300 hover:bg-red-100\"\r\n matTooltip=\"Remover filtro\"\r\n >\r\n <i class=\"fa fa-trash text-red-500 hover:text-red-600\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- PANEL FOOTER: Add Filter & Sort -->\r\n <div\r\n class=\"-mb-2 flex flex-col items-center justify-between gap-4 border-t border-gray-200 pt-4 sm:flex-row\"\r\n >\r\n <!-- Add Filter Button -->\r\n <div *ngIf=\"dropdownItems.length > 0\">\r\n <button\r\n (click)=\"addFilter()\"\r\n 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\"\r\n matTooltip=\"Adicionar novo filtro\"\r\n >\r\n <i class=\"fa fa-plus mr-2\"></i>\r\n Adicionar Filtro\r\n </button>\r\n </div>\r\n\r\n <!-- Sort Dropdown -->\r\n <div\r\n class=\"w-full sm:w-auto sm:min-w-[250px]\"\r\n *ngIf=\"sortableDropdownItems.length > 0\"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label>Ordenar por</mat-label>\r\n <mat-select placeholder=\"Selecione...\" [formControl]=\"selectSort\">\r\n <mat-option\r\n *ngFor=\"let item of sortableDropdownItems\"\r\n [value]=\"item\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-sort-alpha-down text-cyan-600\"></i>\r\n <span>{{ item.title }}</span>\r\n </div>\r\n </mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- SIMPLE SEARCH (for non-paginated tables) -->\r\n <div\r\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\r\n *ngIf=\"data.pagination === false\"\r\n >\r\n <mat-form-field appearance=\"outline\" class=\"w-full\">\r\n <mat-label class=\"flex items-center gap-2\">\r\n <i class=\"fa fa-search text-blue-500\"></i>\r\n Buscar\r\n </mat-label>\r\n <input\r\n matInput\r\n (keyup.enter)=\"search()\"\r\n (keyup)=\"applyFilter(filterInput.value)\"\r\n placeholder=\"Digite para filtrar...\"\r\n #filterInput\r\n />\r\n <mat-icon matSuffix class=\"text-gray-500\">search</mat-icon>\r\n </mat-form-field>\r\n <button\r\n *ngIf=\"data.actionButton\"\r\n [ngClass]=\"\r\n (data.actionButton.colorClass || 'bg-blue-500') +\r\n ' float-right flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\r\n \"\r\n [routerLink]=\"data.actionButton.routerLink\"\r\n (click)=\"\r\n data.actionButton.method ? data.actionButton.method($event) : null\r\n \"\r\n >\r\n <i *ngIf=\"data.actionButton.icon\" [class]=\"data.actionButton.icon\"></i>\r\n {{ data.actionButton.label }}\r\n </button>\r\n </div>\r\n\r\n <div class=\"flex flex-col\">\r\n <div\r\n class=\"mx-auto flex flex-col\"\r\n *ngIf=\"data.tabs && data.tabs.tabsData && data.tabs.tabsData.length > 0\"\r\n >\r\n <!-- Calcular quantos grupos de 6 tabs existem -->\r\n <ng-container\r\n *ngFor=\"\r\n let groupIndex of getTabGroups(data.tabs.tabsData);\r\n let i = index\r\n \"\r\n >\r\n <div class=\"mx-auto flex flex-row\">\r\n <ng-container\r\n *ngFor=\"\r\n let tab of getTabGroup(data.tabs.tabsData, groupIndex);\r\n let j = index\r\n \"\r\n >\r\n <button\r\n class=\"border-2 border-gray-300 bg-gray-200 px-4 py-2 font-medium transition hover:brightness-95\"\r\n [ngClass]=\"\r\n isTabSelected(getRealTabIndex(i, j))\r\n ? 'border-b-0 brightness-110'\r\n : ''\r\n \"\r\n (click)=\"onTableSelected(i, j)\"\r\n >\r\n {{ tab.label }}\r\n <span\r\n *ngIf=\"tab.counter !== undefined\"\r\n class=\"ml-2 text-xs font-bold\"\r\n [ngClass]=\"tab.counterClass\"\r\n >\r\n {{ tab.counter }}\r\n </span>\r\n </button>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n </div>\r\n <div class=\"mat-elevation-z8 w-full overflow-x-auto rounded-xl\">\r\n <table\r\n mat-table\r\n [dataSource]=\"dataSource\"\r\n matSort\r\n #sort=\"matSort\"\r\n matSortActive=\"createdAt\"\r\n matSortDirection=\"desc\"\r\n >\r\n <ng-container\r\n *ngFor=\"let col of data.displayedColumns\"\r\n matColumnDef=\"{{ col.property }}\"\r\n >\r\n <ng-container *matHeaderCellDef>\r\n <!-- IF THE COLUMN IS NOT SORTABLE, THEN DON'T SHOW THE SORT BUTTONS -->\r\n <th\r\n *ngIf=\"!col.isSortable || data.pagination === true\"\r\n mat-header-cell\r\n [ngClass]=\"\r\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\r\n (data?.color?.text ? ' ' + $any(data.color).text : '')\r\n \"\r\n >\r\n {{ col.title }}\r\n </th>\r\n <!-- IF THE COLUMN IS SORTABLE, THEN SHOW THE SORT BUTTONS -->\r\n <th\r\n *ngIf=\"col.isSortable && data.pagination === false\"\r\n mat-header-cell\r\n mat-sort-header\r\n [ngClass]=\"\r\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\r\n (data?.color?.text ? ' ' + $any(data.color).text : '')\r\n \"\r\n >\r\n {{ col.title }}\r\n </th>\r\n <td\r\n mat-cell\r\n *matCellDef=\"let row\"\r\n (click)=\"col.method ? col.method(row) : null\"\r\n (mouseenter)=\"onCellMouseEnter($event, row, col)\"\r\n (mouseleave)=\"onCellMouseLeave()\"\r\n (mousemove)=\"onCellMouseMove($event)\"\r\n >\r\n <!-- CHECK IF THE COLUMN MUST BE DISPLAYED -->\r\n <span *ngIf=\"!col.image && !col.iconClass && !col.method\">\r\n <ng-container>\r\n <span\r\n *ngIf=\"\r\n col.charLimit &&\r\n row[col.property] &&\r\n row[col.property].length > col.charLimit;\r\n else withinLimit\r\n \"\r\n >\r\n <a\r\n *ngIf=\"col.hasLink === true\"\r\n [href]=\"row[col.property]\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row) }}\r\n </a>\r\n <a\r\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\r\n [href]=\"col.hasLink\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row) }}\r\n </a>\r\n <span\r\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\r\n >\r\n {{ getDisplayValue(col, row) }}\r\n </span>\r\n </span>\r\n </ng-container>\r\n <ng-template #withinLimit>\r\n <a\r\n *ngIf=\"col.hasLink === true\"\r\n [href]=\"row[col.property]\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row, true) }}\r\n </a>\r\n <a\r\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\r\n [href]=\"col.hasLink\"\r\n target=\"_blank\"\r\n >\r\n {{ getDisplayValue(col, row, true) }}\r\n </a>\r\n <span\r\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\r\n >\r\n {{ getDisplayValue(col, row, true) }}\r\n </span>\r\n </ng-template>\r\n </span>\r\n <!------------------- IMAGE ------------------>\r\n <img\r\n *ngIf=\"\r\n col.image && col.image.path && !col.iconClass && !col.method\r\n \"\r\n [src]=\"col.image.path + '/' + row[col.property]\"\r\n [ngClass]=\"col.image.class\"\r\n alt=\"Imagem\"\r\n />\r\n <img\r\n *ngIf=\"\r\n col.image && col.image.url && !col.iconClass && !col.method\r\n \"\r\n [src]=\"row[col.property]\"\r\n [ngClass]=\"col.image.class\"\r\n alt=\"Imagem\"\r\n />\r\n <ng-container *ngIf=\"col.iconClass\">\r\n <button\r\n *ngFor=\"let iconClass of col.iconClass\"\r\n (click)=\"\r\n iconClass.buttonMethod\r\n ? iconClass.buttonMethod(row, $event)\r\n : $event.stopPropagation()\r\n \"\r\n >\r\n <span\r\n [ngClass]=\"iconClass.class\"\r\n *ngIf=\"\r\n iconClass.condition === undefined ||\r\n (iconClass.condition !== undefined &&\r\n $any(iconClass.condition)(row))\r\n \"\r\n >{{ iconClass.text }}</span\r\n >\r\n </button>\r\n </ng-container>\r\n </td>\r\n </ng-container>\r\n </ng-container>\r\n\r\n <tr mat-header-row *matHeaderRowDef=\"columnProperties\"></tr>\r\n <tr\r\n [ngClass]=\"{\r\n 'example-element-row': data.isNotClickable === true,\r\n 'example-element-row cursor-pointer': !data.isNotClickable\r\n }\"\r\n mat-row\r\n *matRowDef=\"let row; columns: columnProperties\"\r\n (click)=\"goToDetails(row)\"\r\n ></tr>\r\n\r\n <!-- ROW SHOWN WHEN THERE IS NO MATCHING DATA. -->\r\n <tr class=\"mat-row\" *matNoDataRow>\r\n <td *ngIf=\"!isLoading\" class=\"mat-cell p-4\" colspan=\"4\">\r\n Nenhum resultado encontrado para a busca\r\n </td>\r\n </tr>\r\n </table>\r\n\r\n <div class=\"flex justify-center\" *ngIf=\"isLoading\">\r\n <mat-spinner></mat-spinner>\r\n </div>\r\n\r\n <div class=\"paginator-container\">\r\n <mat-paginator\r\n #paginator\r\n [pageSizeOptions]=\"[25, 50, 100]\"\r\n [pageSize]=\"pageSize\"\r\n [length]=\"totalItems\"\r\n showFirstLastButtons\r\n aria-label=\"Select page of periodic elements\"\r\n (page)=\"onPageChange($event)\"\r\n [ngClass]=\"{\r\n 'hide-length':\r\n ['filter', 'filterByDate', 'equals'].includes(\r\n this.currentArrange\r\n ) || this.data.filterFn,\r\n 'hide-next-button': !hasNextPage && data.pagination === true,\r\n 'hide-last-button':\r\n (!hasNextPage && data.pagination === true) || this.data.filterFn\r\n }\"\r\n >\r\n </mat-paginator>\r\n <div\r\n *ngIf=\"\r\n !isLoading &&\r\n dataSource?.data &&\r\n dataSource.data.length > 0 &&\r\n data?.filterFn\r\n \"\r\n class=\"page-number-display\"\r\n >\r\n {{ currentPageNumber }}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- TOOLTIP PERSONALIZADO -->\r\n <div\r\n *ngIf=\"showTooltip\"\r\n class=\"fixed z-50 max-w-md break-words rounded-lg bg-gray-800 px-3 py-2 text-sm text-white shadow-lg\"\r\n [style.left.px]=\"tooltipPosition.x\"\r\n [style.top.px]=\"tooltipPosition.y\"\r\n [style.pointer-events]=\"'none'\"\r\n >\r\n {{ tooltipContent }}\r\n </div>\r\n</div>\r\n", styles: ["::ng-deep .hide-length .mat-mdc-paginator-range-label{display:none}::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{visibility:hidden}::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{visibility:hidden}::ng-deep .mat-mdc-text-field-wrapper.mdc-text-field.ng-tns-c162-1.mdc-text-field--filled{width:25dvw}::ng-deep .custom-filter .mat-mdc-text-field-wrapper{width:20dvw;max-width:300px}\n"] }]
1706
- }], ctorParameters: function () { return [{ type: i1$1.Router }, { type: TableService }, { type: i1.AngularFirestore }]; }, propDecorators: { data: [{
1707
- type: Input
1708
- }], downloadTable: [{
1709
- type: Input
1710
- }], paginator: [{
1711
- type: ViewChild,
1712
- args: [MatPaginator]
1713
- }], sort: [{
1714
- type: ViewChild,
1715
- args: [MatSort]
942
+ class TableComponent {
943
+ // CONSTRUCTOR
944
+ constructor(router, tableService, firestore) {
945
+ this.router = router;
946
+ this.tableService = tableService;
947
+ this.firestore = firestore;
948
+ this.arrange = null;
949
+ this.currentPageNumber = 1;
950
+ this.currentClientPageIndex = 0;
951
+ this.items = [];
952
+ this.isLoading = false;
953
+ this.lastDoc = null;
954
+ this.firstDoc = null;
955
+ this.sortBy = {
956
+ field: 'createdAt',
957
+ order: 'desc',
958
+ };
959
+ this.columnProperties = [];
960
+ this.selectSort = new FormControl('');
961
+ this.currentArrange = '';
962
+ this.hasNextPage = false;
963
+ this.dropdownItems = [];
964
+ this.sortableDropdownItems = [];
965
+ this.pageSize = 25;
966
+ this.totalItems = 0;
967
+ this.filterValue = null;
968
+ this.hasFilterableColumn = false;
969
+ this.hasSortableColumn = false;
970
+ this.filterSubject = new Subject();
971
+ this.debounceTimeMs = 500;
972
+ this.selectedTab = 0;
973
+ // Propriedades para controle do tooltip
974
+ this.hoveredCell = null;
975
+ this.showTooltip = false;
976
+ this.tooltipContent = '';
977
+ this.tooltipPosition = { x: 0, y: 0 };
978
+ this.filtersForm = new FormArray([this.createFilterGroup()]);
979
+ }
980
+ createFilterGroup() {
981
+ return new FormGroup({
982
+ selectFilter: new FormControl(''),
983
+ typeFilter: new FormControl(''),
984
+ selectItem: new FormControl(''),
985
+ initialDate: new FormControl('', this.tableService.dateFormatValidator()),
986
+ finalDate: new FormControl('', this.tableService.dateFormatValidator()),
987
+ });
988
+ }
989
+ addFilter(filterData) {
990
+ const newFilterGroup = this.createFilterGroup();
991
+ if (filterData) {
992
+ if (filterData.selectFilter) {
993
+ newFilterGroup.get('selectFilter')?.setValue(filterData.selectFilter);
994
+ }
995
+ if (filterData.typeFilter) {
996
+ newFilterGroup.get('typeFilter')?.setValue(filterData.typeFilter);
997
+ }
998
+ if (filterData.selectItem) {
999
+ newFilterGroup.get('selectItem')?.setValue(filterData.selectItem);
1000
+ }
1001
+ if (filterData.initialDate) {
1002
+ newFilterGroup.get('initialDate')?.setValue(filterData.initialDate);
1003
+ }
1004
+ if (filterData.finalDate) {
1005
+ newFilterGroup.get('finalDate')?.setValue(filterData.finalDate);
1006
+ }
1007
+ }
1008
+ this.filtersForm.push(newFilterGroup);
1009
+ }
1010
+ onSelectFilterChange() {
1011
+ const lastIndex = this.filtersForm.length - 1;
1012
+ const lastFilter = this.filtersForm.at(lastIndex);
1013
+ if (lastFilter.get('selectFilter')?.value) {
1014
+ this.addFilter();
1015
+ }
1016
+ }
1017
+ removeFilter(index) {
1018
+ this.filtersForm.removeAt(index);
1019
+ if (this.filtersForm.length === 0) {
1020
+ this.addFilter();
1021
+ }
1022
+ }
1023
+ removeAllFilters() {
1024
+ this.filtersForm.clear();
1025
+ this.addFilter();
1026
+ this.resetFilter();
1027
+ }
1028
+ // METHODS
1029
+ async ngOnInit() {
1030
+ if (!this.data.color)
1031
+ this.data.color = { bg: 'bg-primary', text: 'text-black' };
1032
+ this.columnProperties = this.data.displayedColumns.map((column) => {
1033
+ return column.property;
1034
+ });
1035
+ if (this.data.actionButton && !this.data.actionButton.condition) {
1036
+ this.data.actionButton.condition = (_row) => true;
1037
+ }
1038
+ if (this.data.pagination) {
1039
+ this.data.displayedColumns.forEach((col) => {
1040
+ if (col.isFilterable) {
1041
+ if (this.hasFilterableColumn === false)
1042
+ this.hasFilterableColumn = true;
1043
+ this.dropdownItems.push({
1044
+ ...col,
1045
+ arrange: 'filter',
1046
+ title: col.title,
1047
+ });
1048
+ }
1049
+ if (col.isSortable) {
1050
+ if (this.hasSortableColumn === false)
1051
+ this.hasSortableColumn = true;
1052
+ this.sortableDropdownItems.push({
1053
+ ...col,
1054
+ arrange: 'ascending',
1055
+ title: col.title + ': crescente',
1056
+ });
1057
+ this.sortableDropdownItems.push({
1058
+ ...col,
1059
+ arrange: 'descending',
1060
+ title: col.title + ': decrescente',
1061
+ });
1062
+ }
1063
+ if (col.isFilterableByDate) {
1064
+ this.dropdownItems.push({
1065
+ ...col,
1066
+ arrange: 'filterByDate',
1067
+ title: col.title + ': filtro por data',
1068
+ });
1069
+ }
1070
+ });
1071
+ if (this.data.filterableOptions) {
1072
+ this.data.filterableOptions.forEach((option) => this.dropdownItems.push({ ...option, arrange: 'equals' }));
1073
+ }
1074
+ }
1075
+ // Sem paginação
1076
+ if (this.data.pagination === false) {
1077
+ await this.loadItems();
1078
+ }
1079
+ // Com paginação
1080
+ if (this.data.pagination === true) {
1081
+ if (this.data.sortBy)
1082
+ this.sortBy = {
1083
+ field: this.data.sortBy.field,
1084
+ order: this.data.sortBy.order,
1085
+ };
1086
+ this.filterSubject
1087
+ .pipe(debounceTime(this.debounceTimeMs))
1088
+ .subscribe(() => {
1089
+ this.loadItemsPaginated('reload', true);
1090
+ });
1091
+ this.isLoading = true;
1092
+ await this.loadItemsPaginated('reload', true);
1093
+ this.sort.active = 'createdAt';
1094
+ this.sort.direction = 'desc';
1095
+ this.dataSource.paginator = this.paginator;
1096
+ this.dataSource.sort = this.sort;
1097
+ this.totalItems = 0;
1098
+ if (this.data.totalRef) {
1099
+ for (const totalRef of this.data.totalRef) {
1100
+ const totalRefDoc = await totalRef.ref.get();
1101
+ const docData = totalRefDoc.data();
1102
+ if (docData && docData[totalRef.field])
1103
+ this.totalItems = (this.totalItems +
1104
+ docData[totalRef.field]);
1105
+ }
1106
+ }
1107
+ this.isLoading = false;
1108
+ }
1109
+ }
1110
+ getDisplayValue(col, row, withinLimit = false) {
1111
+ let value;
1112
+ if (row[col.property] === null || row[col.property] === undefined) {
1113
+ return '';
1114
+ }
1115
+ if (col.calculateValue) {
1116
+ value = String(col.calculateValue(row));
1117
+ }
1118
+ else {
1119
+ value = this.getNestedValue(row, col.property);
1120
+ }
1121
+ // Verifica se é um array e tem arrayField definido
1122
+ if (Array.isArray(value) && col.arrayField) {
1123
+ value = this.formatArrayValue(value, col.arrayField);
1124
+ }
1125
+ if (col.queryLength && row[col.property]) {
1126
+ value = row[col.property];
1127
+ }
1128
+ value = col.pipe ? col.pipe.transform(value) : value;
1129
+ return withinLimit ? value : value.substring(0, col.charLimit) + '...';
1130
+ }
1131
+ getNestedValue(obj, path) {
1132
+ if (!path)
1133
+ return undefined;
1134
+ const properties = path.split('.');
1135
+ return properties.reduce((acc, currentPart) => acc && acc[currentPart], obj);
1136
+ }
1137
+ formatArrayValue(array, field) {
1138
+ if (!Array.isArray(array) || array.length === 0) {
1139
+ return '';
1140
+ }
1141
+ const values = array
1142
+ .map((item) => {
1143
+ if (typeof item === 'object' && item !== null) {
1144
+ return item[field] || '';
1145
+ }
1146
+ return String(item);
1147
+ })
1148
+ .filter((value) => value !== '' && value !== null && value !== undefined);
1149
+ return values.join(', ');
1150
+ }
1151
+ // Métodos sem paginação
1152
+ async loadItems() {
1153
+ this.items = await this.tableService.getItems(this.data.collectionRef);
1154
+ if (this.data.conditions) {
1155
+ this.filterItems();
1156
+ }
1157
+ await this.loadRelations();
1158
+ await this.loadQueryLengths();
1159
+ this.totalItems = this.items.length;
1160
+ this.dataSource = new MatTableDataSource(this.items);
1161
+ this.dataSource.paginator = this.paginator;
1162
+ this.dataSource.sort = this.sort;
1163
+ if (this.data.sortBy) {
1164
+ this.dataSource.sort.active = this.data.sortBy.field;
1165
+ this.dataSource.sort.direction = this.data.sortBy.order;
1166
+ this.dataSource.sort.sortChange.emit();
1167
+ }
1168
+ this.dataSource.filterPredicate = (data, filter) => {
1169
+ return this.data.displayedColumns.some((col) => {
1170
+ if (col.filterPredicates) {
1171
+ return col.filterPredicates.some((predicate) => {
1172
+ const propertyValue = data[col.property];
1173
+ if (!propertyValue || typeof propertyValue !== 'object') {
1174
+ return false;
1175
+ }
1176
+ const predicateValue = propertyValue[predicate];
1177
+ if (predicateValue === null || predicateValue === undefined) {
1178
+ return false;
1179
+ }
1180
+ return String(predicateValue)
1181
+ .trim()
1182
+ .toLocaleLowerCase()
1183
+ .includes(filter);
1184
+ });
1185
+ }
1186
+ if (col.property && col.isFilterable) {
1187
+ const propertyValue = data[col.property];
1188
+ if (propertyValue === null || propertyValue === undefined) {
1189
+ return false;
1190
+ }
1191
+ return String(propertyValue)
1192
+ .trim()
1193
+ .toLocaleLowerCase()
1194
+ .includes(filter);
1195
+ }
1196
+ return false;
1197
+ });
1198
+ };
1199
+ this.filterPredicate = this.dataSource.filterPredicate;
1200
+ }
1201
+ // Métodos com paginação
1202
+ async loadItemsPaginated(navigation = 'reload', reset = false) {
1203
+ if (reset && ['forward', 'reload'].includes(navigation)) {
1204
+ this.lastDoc = null;
1205
+ }
1206
+ if (reset && ['backward', 'reload'].includes(navigation)) {
1207
+ this.firstDoc = null;
1208
+ }
1209
+ const activeFilters = this.filtersForm.controls
1210
+ .flatMap((control) => {
1211
+ const group = control;
1212
+ const selectedFilter = group.get('selectFilter')?.value;
1213
+ if (!selectedFilter)
1214
+ return [];
1215
+ const arrange = selectedFilter.arrange;
1216
+ if (arrange === 'filter') {
1217
+ const filterValue = group.get('typeFilter')?.value;
1218
+ if (!filterValue)
1219
+ return [];
1220
+ return {
1221
+ arrange,
1222
+ filter: {
1223
+ property: selectedFilter.property,
1224
+ filtering: filterValue,
1225
+ },
1226
+ dateFilter: undefined,
1227
+ };
1228
+ }
1229
+ if (arrange === 'filterByDate') {
1230
+ const initial = group.get('initialDate')?.value;
1231
+ const final = group.get('finalDate')?.value;
1232
+ if (initial && final) {
1233
+ const [dayI, monthI, yearI] = initial.split('/');
1234
+ const initialDate = new Date(`${monthI}/${dayI}/${yearI}`);
1235
+ const [dayF, monthF, yearF] = final.split('/');
1236
+ const finalDate = new Date(`${monthF}/${dayF}/${yearF}`);
1237
+ finalDate.setHours(23, 59, 59);
1238
+ return {
1239
+ arrange,
1240
+ filter: undefined,
1241
+ dateFilter: {
1242
+ initial: initialDate,
1243
+ final: finalDate,
1244
+ },
1245
+ };
1246
+ }
1247
+ return [];
1248
+ }
1249
+ if (selectedFilter.hasOwnProperty('items') && arrange === 'equals') {
1250
+ const selectedItems = group.get('selectItem')?.value;
1251
+ if (Array.isArray(selectedItems) && selectedItems.length > 0) {
1252
+ return selectedItems.map((item) => ({
1253
+ arrange,
1254
+ filter: {
1255
+ property: item.property,
1256
+ filtering: item.value,
1257
+ },
1258
+ dateFilter: undefined,
1259
+ }));
1260
+ }
1261
+ }
1262
+ return [];
1263
+ })
1264
+ .filter((f) => f && (f.filter?.filtering !== undefined || f.dateFilter));
1265
+ this.arrange = {
1266
+ filters: activeFilters,
1267
+ sortBy: this.sortBy,
1268
+ };
1269
+ const paginated = {
1270
+ batchSize: this.pageSize,
1271
+ collection: this.data.collection,
1272
+ doc: { lastDoc: this.lastDoc, firstDoc: this.firstDoc },
1273
+ navigation,
1274
+ arrange: this.arrange,
1275
+ conditions: this.data.conditions,
1276
+ size: this.totalItems,
1277
+ filterFn: this.data.filterFn,
1278
+ clientPageIndex: this.currentClientPageIndex,
1279
+ };
1280
+ const result = await this.tableService.getPaginated(paginated);
1281
+ this.items = result.items;
1282
+ await this.loadRelations();
1283
+ await this.loadQueryLengths();
1284
+ this.lastDoc = result.lastDoc;
1285
+ this.firstDoc = result.firstDoc;
1286
+ // Atualizar currentClientPageIndex se retornado pelo fallback
1287
+ if (result.currentClientPageIndex !== undefined) {
1288
+ this.currentClientPageIndex = result.currentClientPageIndex;
1289
+ }
1290
+ let sum = 0;
1291
+ if (this.data.totalRef) {
1292
+ for (const totalRef of this.data.totalRef) {
1293
+ const totalRefDoc = await totalRef.ref.get();
1294
+ const docData = totalRefDoc.data();
1295
+ if (docData || result.filterLength) {
1296
+ sum =
1297
+ result.filterLength ??
1298
+ (sum + (docData ? docData[totalRef.field] : 0));
1299
+ }
1300
+ }
1301
+ this.totalItems = sum;
1302
+ }
1303
+ this.hasNextPage = result.hasNextPage;
1304
+ this.dataSource = new MatTableDataSource(this.items);
1305
+ this.filterPredicate = this.dataSource.filterPredicate;
1306
+ }
1307
+ async onPageChange(event) {
1308
+ if (this.data.pagination === true && event) {
1309
+ this.isLoading = true;
1310
+ const previousPageIndex = event.previousPageIndex ?? 0;
1311
+ const pageIndex = event.pageIndex;
1312
+ const currentComponentPageSize = this.pageSize;
1313
+ const eventPageSize = event.pageSize;
1314
+ const totalItems = this.totalItems;
1315
+ let navigationDirection;
1316
+ let resetDocs = false;
1317
+ let originalPageSize = null;
1318
+ const lastPageIndex = Math.max(0, Math.ceil(totalItems / eventPageSize) - 1);
1319
+ // Atualizar currentClientPageIndex sempre para o fallback
1320
+ this.currentClientPageIndex = pageIndex;
1321
+ if (previousPageIndex !== undefined && pageIndex > previousPageIndex) {
1322
+ this.currentPageNumber++;
1323
+ }
1324
+ else if (previousPageIndex !== undefined &&
1325
+ pageIndex < previousPageIndex) {
1326
+ this.currentPageNumber = Math.max(1, this.currentPageNumber - 1);
1327
+ }
1328
+ if (eventPageSize !== currentComponentPageSize) {
1329
+ console.log('Alterou a quantidade de elementos exibidos por página');
1330
+ this.pageSize = eventPageSize;
1331
+ navigationDirection = 'forward';
1332
+ resetDocs = true;
1333
+ this.currentClientPageIndex = 0;
1334
+ }
1335
+ else if (pageIndex === 0 &&
1336
+ previousPageIndex !== undefined &&
1337
+ pageIndex < previousPageIndex) {
1338
+ console.log('Pulou para a primeira página');
1339
+ navigationDirection = 'forward';
1340
+ this.currentPageNumber = 1;
1341
+ this.currentClientPageIndex = 0;
1342
+ resetDocs = true;
1343
+ }
1344
+ else if (pageIndex === lastPageIndex &&
1345
+ previousPageIndex !== undefined &&
1346
+ pageIndex > previousPageIndex &&
1347
+ pageIndex - previousPageIndex > 1) {
1348
+ console.log('Pulou para a ultima página');
1349
+ navigationDirection = 'backward';
1350
+ resetDocs = true;
1351
+ const itemsExpectedInLastPage = totalItems - lastPageIndex * eventPageSize;
1352
+ if (itemsExpectedInLastPage > 0 &&
1353
+ itemsExpectedInLastPage < eventPageSize) {
1354
+ originalPageSize = this.pageSize;
1355
+ this.pageSize = itemsExpectedInLastPage;
1356
+ }
1357
+ }
1358
+ else if (previousPageIndex !== undefined &&
1359
+ pageIndex > previousPageIndex) {
1360
+ console.log('Procedendo');
1361
+ navigationDirection = 'forward';
1362
+ resetDocs = false;
1363
+ }
1364
+ else if (previousPageIndex !== undefined &&
1365
+ pageIndex < previousPageIndex) {
1366
+ console.log('Retrocedendo.');
1367
+ navigationDirection = 'backward';
1368
+ resetDocs = false;
1369
+ }
1370
+ else if (previousPageIndex !== undefined &&
1371
+ pageIndex === previousPageIndex) {
1372
+ console.log('Recarregando.');
1373
+ navigationDirection = 'reload';
1374
+ resetDocs = false;
1375
+ }
1376
+ else if (previousPageIndex === undefined && pageIndex === 0) {
1377
+ console.log('Evento inicial do paginador para pág 0. ngOnInit carregou.');
1378
+ this.isLoading = false;
1379
+ if (event)
1380
+ this.pageEvent = event;
1381
+ return;
1382
+ }
1383
+ else {
1384
+ console.warn('INESPERADO! Condição de navegação não tratada:', event);
1385
+ this.isLoading = false;
1386
+ if (event)
1387
+ this.pageEvent = event;
1388
+ return;
1389
+ }
1390
+ if (navigationDirection) {
1391
+ try {
1392
+ await this.loadItemsPaginated(navigationDirection, resetDocs);
1393
+ }
1394
+ catch (error) {
1395
+ console.error('Erro ao carregar itens paginados:', error);
1396
+ }
1397
+ finally {
1398
+ if (originalPageSize !== null) {
1399
+ this.pageSize = originalPageSize;
1400
+ }
1401
+ }
1402
+ }
1403
+ if (event)
1404
+ this.pageEvent = event;
1405
+ this.isLoading = false;
1406
+ }
1407
+ }
1408
+ // Outros métodos
1409
+ applyFilter(value) {
1410
+ // Sem paginação
1411
+ if (this.data.pagination === false) {
1412
+ this.dataSource.filter = String(value).trim().toLowerCase();
1413
+ }
1414
+ // Com paginação
1415
+ if (this.data.pagination === true) {
1416
+ this.filterValue = value;
1417
+ this.filterSubject.next(this.filterValue);
1418
+ }
1419
+ }
1420
+ goToDetails(row) {
1421
+ if (this.data.isNotClickable) {
1422
+ return;
1423
+ }
1424
+ const urlPath = this.data.url || this.data.name;
1425
+ const url = this.router.serializeUrl(this.router.createUrlTree([`/${urlPath}`, row.id]));
1426
+ window.open(url, '_blank');
1427
+ }
1428
+ async getRelation(params) {
1429
+ try {
1430
+ let snapshot;
1431
+ if (params.id !== '' &&
1432
+ params.id !== undefined &&
1433
+ params.collection !== undefined &&
1434
+ params.collection !== '') {
1435
+ snapshot = await firstValueFrom(this.firestore.collection(params.collection).doc(params.id).get());
1436
+ }
1437
+ if (snapshot && snapshot.exists) {
1438
+ const data = snapshot.data();
1439
+ return data?.[params.newProperty] ?? '';
1440
+ }
1441
+ return '';
1442
+ }
1443
+ catch (e) {
1444
+ console.log(e);
1445
+ return '';
1446
+ }
1447
+ }
1448
+ async loadRelations() {
1449
+ const relationPromises = this.data.displayedColumns
1450
+ .filter((col) => col.relation)
1451
+ .flatMap((col) => this.items.map(async (item) => {
1452
+ if (col.relation) {
1453
+ item[col.property] = await this.getRelation({
1454
+ id: item[col.relation.property],
1455
+ collection: col.relation.collection,
1456
+ newProperty: col.relation.newProperty,
1457
+ });
1458
+ }
1459
+ }));
1460
+ await Promise.all(relationPromises);
1461
+ }
1462
+ async getQueryLength(params) {
1463
+ const snapshot = await this.firestore
1464
+ .collection(params.relation.collection)
1465
+ .ref.where(params.relation.property, params.relation.operator, params.item[params.relation.value])
1466
+ .get();
1467
+ return snapshot.size;
1468
+ }
1469
+ async loadQueryLengths() {
1470
+ const lengthPromises = this.data.displayedColumns
1471
+ .filter((col) => col.queryLength)
1472
+ .flatMap((col) => this.items.map(async (item) => {
1473
+ if (col.queryLength) {
1474
+ item[col.property] = await this.getQueryLength({
1475
+ item: item,
1476
+ relation: col.queryLength,
1477
+ });
1478
+ }
1479
+ }));
1480
+ await Promise.all(lengthPromises);
1481
+ }
1482
+ filterItems() {
1483
+ if (this.data.conditions) {
1484
+ this.data.conditions.forEach((cond) => {
1485
+ this.items = this.items.filter((item) => {
1486
+ const operatorFunction = this.tableService.operators[cond.operator];
1487
+ if (operatorFunction) {
1488
+ return operatorFunction(item[cond.firestoreProperty], cond.dashProperty);
1489
+ }
1490
+ return false;
1491
+ });
1492
+ });
1493
+ }
1494
+ }
1495
+ // Filtro de data
1496
+ async search() {
1497
+ if (this.selectSort.value) {
1498
+ if (this.selectSort.value.arrange === 'ascending') {
1499
+ this.sortBy = {
1500
+ field: this.selectSort.value.property,
1501
+ order: 'asc',
1502
+ };
1503
+ }
1504
+ if (this.selectSort.value.arrange === 'descending') {
1505
+ this.sortBy = {
1506
+ field: this.selectSort.value.property,
1507
+ order: 'desc',
1508
+ };
1509
+ }
1510
+ }
1511
+ await this.loadItemsPaginated('reload', true);
1512
+ this.currentArrange =
1513
+ this.filtersForm.length > 0
1514
+ ? this.filtersForm.at(0).get('selectFilter')?.value?.arrange ?? ''
1515
+ : '';
1516
+ this.paginator.firstPage();
1517
+ }
1518
+ async resetFilter() {
1519
+ if (!this.data.pagination) {
1520
+ this.dataSource.filter = '';
1521
+ if (this.filterPredicate) {
1522
+ this.dataSource.filterPredicate = this.filterPredicate;
1523
+ }
1524
+ }
1525
+ else {
1526
+ this.dataSource.filter = '';
1527
+ if (this.filterPredicate) {
1528
+ this.dataSource.filterPredicate = this.filterPredicate;
1529
+ }
1530
+ }
1531
+ this.filtersForm.clear();
1532
+ this.addFilter();
1533
+ this.selectSort.patchValue('');
1534
+ this.sortBy = {
1535
+ order: this.data.sortBy ? this.data.sortBy.order : 'desc',
1536
+ field: this.data.sortBy ? this.data.sortBy.field : 'createdAt',
1537
+ };
1538
+ await this.loadItemsPaginated('reload', true);
1539
+ this.currentArrange = '';
1540
+ this.paginator.firstPage();
1541
+ }
1542
+ // Método público para recarregar a tabela
1543
+ async reloadTable() {
1544
+ if (this.data.pagination) {
1545
+ await this.loadItemsPaginated('reload', true);
1546
+ this.paginator.firstPage();
1547
+ }
1548
+ else {
1549
+ await this.loadItems();
1550
+ }
1551
+ }
1552
+ updateDisplayedColumns() {
1553
+ if (this.dataSource) {
1554
+ this.dataSource = new MatTableDataSource([]);
1555
+ }
1556
+ this.columnProperties = this.data.displayedColumns.map((column) => {
1557
+ return column.property;
1558
+ });
1559
+ this.dropdownItems = [];
1560
+ this.sortableDropdownItems = [];
1561
+ this.data.displayedColumns.forEach((col) => {
1562
+ if (col.isFilterable) {
1563
+ this.dropdownItems.push({
1564
+ ...col,
1565
+ arrange: 'filter',
1566
+ title: col.title,
1567
+ });
1568
+ }
1569
+ if (col.isSortable) {
1570
+ this.sortableDropdownItems.push({
1571
+ ...col,
1572
+ arrange: 'ascending',
1573
+ title: col.title + ': crescente',
1574
+ });
1575
+ this.sortableDropdownItems.push({
1576
+ ...col,
1577
+ arrange: 'descending',
1578
+ title: col.title + ': decrescente',
1579
+ });
1580
+ }
1581
+ if (col.isFilterableByDate) {
1582
+ this.dropdownItems.push({
1583
+ ...col,
1584
+ arrange: 'filterByDate',
1585
+ title: col.title + ': filtro por data',
1586
+ });
1587
+ }
1588
+ });
1589
+ if (this.data.filterableOptions) {
1590
+ this.data.filterableOptions.forEach((option) => this.dropdownItems.push({ ...option, arrange: 'equals' }));
1591
+ }
1592
+ }
1593
+ isString(value) {
1594
+ return typeof value === 'string';
1595
+ }
1596
+ // Métodos para controle do tooltip
1597
+ onCellMouseEnter(event, row, col) {
1598
+ // Só mostrar tooltip se a coluna tiver charLimit definido
1599
+ if (!col.charLimit) {
1600
+ return;
1601
+ }
1602
+ const fullValue = this.getDisplayValue(col, row, true);
1603
+ // Só mostrar tooltip se o valor completo for maior que o limite
1604
+ if (fullValue.length <= col.charLimit) {
1605
+ return;
1606
+ }
1607
+ this.hoveredCell = { row, col };
1608
+ this.tooltipContent = fullValue;
1609
+ // Definir posição do tooltip
1610
+ this.tooltipPosition = {
1611
+ x: event.clientX + 10,
1612
+ y: event.clientY - 10,
1613
+ };
1614
+ // Timeout para mostrar o tooltip
1615
+ this.tooltipTimeout = setTimeout(() => {
1616
+ if (this.hoveredCell &&
1617
+ this.hoveredCell.row === row &&
1618
+ this.hoveredCell.col === col) {
1619
+ this.showTooltip = true;
1620
+ }
1621
+ }, 500);
1622
+ }
1623
+ onCellMouseLeave() {
1624
+ if (this.tooltipTimeout) {
1625
+ clearTimeout(this.tooltipTimeout);
1626
+ this.tooltipTimeout = null;
1627
+ }
1628
+ this.showTooltip = false;
1629
+ this.hoveredCell = null;
1630
+ this.tooltipContent = '';
1631
+ }
1632
+ onCellMouseMove(event) {
1633
+ if (this.showTooltip) {
1634
+ this.tooltipPosition = {
1635
+ x: event.clientX + 10,
1636
+ y: event.clientY - 10,
1637
+ };
1638
+ }
1639
+ }
1640
+ // Métodos para inversão vertical dos tabs
1641
+ getTabGroups(tabs) {
1642
+ if (!tabs || tabs.length === 0)
1643
+ return [];
1644
+ const totalGroups = Math.ceil(tabs.length / 6);
1645
+ const groups = [];
1646
+ // Criar array de índices invertidos (último grupo primeiro)
1647
+ for (let i = totalGroups - 1; i >= 0; i--) {
1648
+ groups.push(i);
1649
+ }
1650
+ return groups;
1651
+ }
1652
+ getTabGroup(tabs, groupIndex) {
1653
+ if (!tabs || tabs.length === 0)
1654
+ return [];
1655
+ const startIndex = groupIndex * 6;
1656
+ const endIndex = Math.min(startIndex + 6, tabs.length);
1657
+ return tabs.slice(startIndex, endIndex);
1658
+ }
1659
+ getRealTabIndex(groupIndex, tabIndexInGroup) {
1660
+ if (!this.data.tabs?.tabsData)
1661
+ return 0;
1662
+ const totalGroups = Math.ceil(this.data.tabs.tabsData.length / 6);
1663
+ const realGroupIndex = totalGroups - 1 - groupIndex;
1664
+ return realGroupIndex * 6 + tabIndexInGroup;
1665
+ }
1666
+ onTableSelected(i, j) {
1667
+ if (!this.data.tabs?.tabsData || !this.data.tabs.method)
1668
+ return;
1669
+ this.selectedTab = this.getRealTabIndex(i, j);
1670
+ const tab = this.data.tabs.tabsData[this.selectedTab];
1671
+ if (tab) {
1672
+ this.data.tabs.method(tab, this.selectedTab);
1673
+ }
1674
+ }
1675
+ isTabSelected(originalIndex) {
1676
+ return this.selectedTab === originalIndex;
1677
+ }
1678
+ shouldShowActionButton() {
1679
+ if (!this.data?.actionButton) {
1680
+ return false;
1681
+ }
1682
+ if (!this.data.actionButton.condition) {
1683
+ return true;
1684
+ }
1685
+ try {
1686
+ return this.data.actionButton.condition(null) ?? true;
1687
+ }
1688
+ catch {
1689
+ return true;
1690
+ }
1691
+ }
1692
+ }
1693
+ TableComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableComponent, deps: [{ token: i1$1.Router }, { token: TableService }, { token: i1.AngularFirestore }], target: i0.ɵɵFactoryTarget.Component });
1694
+ TableComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: TableComponent, selector: "lib-table", inputs: { data: "data", downloadTable: "downloadTable" }, viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], ngImport: i0, template: "<div *ngIf=\"data\" class=\"card-body\">\n <div class=\"flex flex-col justify-between gap-6\">\n <!-- UNIFIED CONTROL PANEL: FILTERS, SORT & ACTIONS -->\n <div\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\n *ngIf=\"\n data.pagination === true &&\n (dropdownItems.length > 0 ||\n sortableDropdownItems.length > 0 ||\n data.actionButton)\n \"\n >\n <!-- PANEL HEADER: Title, Custom Action, and Global Actions -->\n <div\n 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\"\n >\n <!-- Left Side: Title & Main Action Button -->\n <div class=\"flex flex-wrap items-center gap-4\">\n <div class=\"flex items-center gap-2\">\n <i class=\"fa fa-filter text-xl text-blue-500\"></i>\n <span class=\"text-lg font-semibold text-gray-700\"\n >Filtros e A\u00E7\u00F5es</span\n >\n </div>\n <button\n *ngIf=\"data.actionButton && data.actionButton.condition\"\n [ngClass]=\"\n (data.actionButton.colorClass || 'bg-blue-500') +\n ' flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\n \"\n [routerLink]=\"data.actionButton.routerLink\"\n (click)=\"\n data.actionButton.method ? data.actionButton.method($event) : null\n \"\n >\n <i\n *ngIf=\"data.actionButton.icon\"\n [class]=\"data.actionButton.icon\"\n ></i>\n {{ data.actionButton.label }}\n </button>\n </div>\n\n <!-- Right Side: Search, Reset, Export -->\n <div\n class=\"flex flex-wrap gap-3\"\n *ngIf=\"\n this.hasFilterableColumn === true || this.hasSortableColumn === true\n \"\n >\n <button\n (click)=\"search()\"\n type=\"button\"\n 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\"\n matTooltip=\"Aplicar filtros\"\n >\n <i class=\"fa fa-search\"></i>\n Pesquisar\n </button>\n\n <button\n (click)=\"resetFilter()\"\n 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\"\n matTooltip=\"Limpar filtros\"\n >\n <i class=\"fas fa-redo-alt\"></i>\n Resetar\n </button>\n\n <button\n *ngIf=\"data.download !== false && downloadTable\"\n 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\"\n matTooltipPosition=\"above\"\n matTooltip=\"Exportar Tabela\"\n [disabled]=\"\n this.dataSource && this.dataSource.filteredData.length <= 0\n \"\n (click)=\"\n $any(arrange) && downloadTable !== undefined\n ? downloadTable($any(arrange), data.conditions || [])\n : null\n \"\n >\n <i class=\"fa fa-download\"></i>\n Exportar\n </button>\n </div>\n </div>\n\n <!-- FILTERS CONTENT (WITH REFINEMENTS) -->\n <div class=\"mb-4 space-y-3\" *ngIf=\"filtersForm.controls.length > 0\">\n <div\n [formGroup]=\"$any(filterGroup)\"\n *ngFor=\"let filterGroup of filtersForm.controls; let i = index\"\n class=\"flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 p-2\"\n >\n <!-- FILTER TYPE SELECTOR -->\n <div class=\"min-w-[200px] flex-1\" *ngIf=\"dropdownItems.length > 0\">\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Tipo de filtro</mat-label>\n <mat-select\n placeholder=\"Selecione o tipo...\"\n formControlName=\"selectFilter\"\n (selectionChange)=\"onSelectFilterChange()\"\n >\n <mat-option *ngFor=\"let item of dropdownItems\" [value]=\"item\">\n <div class=\"flex items-center gap-2\">\n <i\n [class]=\"item.icon || 'fa fa-filter'\"\n class=\"text-sm text-blue-500\"\n ></i>\n <span>{{ item.title }}</span>\n </div>\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- TEXT FILTER -->\n <div\n class=\"min-w-[200px] flex-1\"\n *ngIf=\"\n $any(filterGroup).get('selectFilter')?.value?.arrange === 'filter'\n \"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-search text-gray-400\"></i>\n <span>{{\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\n \"Filtrar\"\n }}</span>\n </mat-label>\n <input\n (keyup.enter)=\"search()\"\n formControlName=\"typeFilter\"\n matInput\n placeholder=\"Digite para filtrar...\"\n #input\n />\n </mat-form-field>\n </div>\n\n <!-- DROPDOWN FILTER -->\n <div\n class=\"min-w-[200px] flex-1\"\n *ngIf=\"\n $any(filterGroup).get('selectFilter')?.value &&\n $any(filterGroup)\n .get('selectFilter')\n ?.value.hasOwnProperty('items')\n \"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\n \"Selecione\"\n }}</mat-label>\n <mat-select\n placeholder=\"Selecione...\"\n formControlName=\"selectItem\"\n multiple\n >\n <mat-option\n *ngFor=\"\n let item of $any(filterGroup).get('selectFilter')?.value\n .items\n \"\n [value]=\"item\"\n >\n {{ item.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- DATE FILTER -->\n <div\n class=\"min-w-[340px] flex-auto\"\n *ngIf=\"\n $any(filterGroup).get('selectFilter')?.value?.arrange ===\n 'filterByDate'\n \"\n >\n <div\n class=\"flex flex-col items-stretch gap-3 sm:flex-row sm:items-center\"\n >\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-calendar text-gray-400\"></i>\n <span>Data Inicial</span>\n </mat-label>\n <input\n matInput\n (keyup.enter)=\"search()\"\n formControlName=\"initialDate\"\n placeholder=\"DD/MM/AAAA\"\n maxlength=\"10\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-calendar text-gray-400\"></i>\n <span>Data Final</span>\n </mat-label>\n <input\n (keyup.enter)=\"search()\"\n matInput\n formControlName=\"finalDate\"\n placeholder=\"DD/MM/AAAA\"\n maxlength=\"10\"\n />\n </mat-form-field>\n </div>\n </div>\n\n <!-- REMOVE FILTER BUTTON -->\n <div *ngIf=\"filtersForm.length > 1\" class=\"ml-auto flex-shrink-0\">\n <button\n (click)=\"removeFilter(i)\"\n class=\"flex h-10 w-10 items-center justify-center rounded-full transition-colors duration-300 hover:bg-red-100\"\n matTooltip=\"Remover filtro\"\n >\n <i class=\"fa fa-trash text-red-500 hover:text-red-600\"></i>\n </button>\n </div>\n </div>\n </div>\n\n <!-- PANEL FOOTER: Add Filter & Sort -->\n <div\n class=\"-mb-2 flex flex-col items-center justify-between gap-4 border-t border-gray-200 pt-4 sm:flex-row\"\n >\n <!-- Add Filter Button -->\n <div *ngIf=\"dropdownItems.length > 0\">\n <button\n (click)=\"addFilter()\"\n 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\"\n matTooltip=\"Adicionar novo filtro\"\n >\n <i class=\"fa fa-plus mr-2\"></i>\n Adicionar Filtro\n </button>\n </div>\n\n <!-- Sort Dropdown -->\n <div\n class=\"w-full sm:w-auto sm:min-w-[250px]\"\n *ngIf=\"sortableDropdownItems.length > 0\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Ordenar por</mat-label>\n <mat-select placeholder=\"Selecione...\" [formControl]=\"selectSort\">\n <mat-option\n *ngFor=\"let item of sortableDropdownItems\"\n [value]=\"item\"\n >\n <div class=\"flex items-center gap-2\">\n <i class=\"fa fa-sort-alpha-down text-cyan-600\"></i>\n <span>{{ item.title }}</span>\n </div>\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </div>\n\n <!-- SIMPLE SEARCH (for non-paginated tables) -->\n <div\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\n *ngIf=\"data.pagination === false\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-search text-blue-500\"></i>\n Buscar\n </mat-label>\n <input\n matInput\n (keyup.enter)=\"search()\"\n (keyup)=\"applyFilter(filterInput.value)\"\n placeholder=\"Digite para filtrar...\"\n #filterInput\n />\n <mat-icon matSuffix class=\"text-gray-500\">search</mat-icon>\n </mat-form-field>\n <button\n *ngIf=\"data.actionButton\"\n [ngClass]=\"\n (data.actionButton.colorClass || 'bg-blue-500') +\n ' float-right flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\n \"\n [routerLink]=\"data.actionButton.routerLink\"\n (click)=\"\n data.actionButton.method ? data.actionButton.method($event) : null\n \"\n >\n <i *ngIf=\"data.actionButton.icon\" [class]=\"data.actionButton.icon\"></i>\n {{ data.actionButton.label }}\n </button>\n </div>\n\n <div class=\"flex flex-col\">\n <div\n class=\"mx-auto flex flex-col\"\n *ngIf=\"data.tabs && data.tabs.tabsData && data.tabs.tabsData.length > 0\"\n >\n <!-- Calcular quantos grupos de 6 tabs existem -->\n <ng-container\n *ngFor=\"\n let groupIndex of getTabGroups(data.tabs.tabsData);\n let i = index\n \"\n >\n <div class=\"mx-auto flex flex-row\">\n <ng-container\n *ngFor=\"\n let tab of getTabGroup(data.tabs.tabsData, groupIndex);\n let j = index\n \"\n >\n <button\n class=\"border-2 border-gray-300 bg-gray-200 px-4 py-2 font-medium transition hover:brightness-95\"\n [ngClass]=\"\n isTabSelected(getRealTabIndex(i, j))\n ? 'border-b-0 brightness-110'\n : ''\n \"\n (click)=\"onTableSelected(i, j)\"\n >\n {{ tab.label }}\n <span\n *ngIf=\"tab.counter !== undefined\"\n class=\"ml-2 text-xs font-bold\"\n [ngClass]=\"tab.counterClass\"\n >\n {{ tab.counter }}\n </span>\n </button>\n </ng-container>\n </div>\n </ng-container>\n </div>\n <div class=\"mat-elevation-z8 w-full overflow-x-auto rounded-xl\">\n <table\n mat-table\n [dataSource]=\"dataSource\"\n matSort\n #sort=\"matSort\"\n matSortActive=\"createdAt\"\n matSortDirection=\"desc\"\n >\n <ng-container\n *ngFor=\"let col of data.displayedColumns\"\n matColumnDef=\"{{ col.property }}\"\n >\n <ng-container *matHeaderCellDef>\n <!-- IF THE COLUMN IS NOT SORTABLE, THEN DON'T SHOW THE SORT BUTTONS -->\n <th\n *ngIf=\"!col.isSortable || data.pagination === true\"\n mat-header-cell\n [ngClass]=\"\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\n (data?.color?.text ? ' ' + $any(data.color).text : '')\n \"\n >\n {{ col.title }}\n </th>\n <!-- IF THE COLUMN IS SORTABLE, THEN SHOW THE SORT BUTTONS -->\n <th\n *ngIf=\"col.isSortable && data.pagination === false\"\n mat-header-cell\n mat-sort-header\n [ngClass]=\"\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\n (data?.color?.text ? ' ' + $any(data.color).text : '')\n \"\n >\n {{ col.title }}\n </th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n (click)=\"col.method ? col.method(row) : null\"\n (mouseenter)=\"onCellMouseEnter($event, row, col)\"\n (mouseleave)=\"onCellMouseLeave()\"\n (mousemove)=\"onCellMouseMove($event)\"\n >\n <!-- CHECK IF THE COLUMN MUST BE DISPLAYED -->\n <span *ngIf=\"!col.image && !col.iconClass && !col.method\">\n <ng-container>\n <span\n *ngIf=\"\n col.charLimit &&\n row[col.property] &&\n row[col.property].length > col.charLimit;\n else withinLimit\n \"\n >\n <a\n *ngIf=\"col.hasLink === true\"\n [href]=\"row[col.property]\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row) }}\n </a>\n <a\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\n [href]=\"col.hasLink\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row) }}\n </a>\n <span\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\n >\n {{ getDisplayValue(col, row) }}\n </span>\n </span>\n </ng-container>\n <ng-template #withinLimit>\n <a\n *ngIf=\"col.hasLink === true\"\n [href]=\"row[col.property]\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row, true) }}\n </a>\n <a\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\n [href]=\"col.hasLink\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row, true) }}\n </a>\n <span\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\n >\n {{ getDisplayValue(col, row, true) }}\n </span>\n </ng-template>\n </span>\n <!------------------- IMAGE ------------------>\n <img\n *ngIf=\"\n col.image && col.image.path && !col.iconClass && !col.method\n \"\n [src]=\"col.image.path + '/' + row[col.property]\"\n [ngClass]=\"col.image.class\"\n alt=\"Imagem\"\n />\n <img\n *ngIf=\"\n col.image && col.image.url && !col.iconClass && !col.method\n \"\n [src]=\"row[col.property]\"\n [ngClass]=\"col.image.class\"\n alt=\"Imagem\"\n />\n <ng-container *ngIf=\"col.iconClass\">\n <button\n *ngFor=\"let iconClass of col.iconClass\"\n (click)=\"\n iconClass.buttonMethod\n ? iconClass.buttonMethod(row, $event)\n : $event.stopPropagation()\n \"\n >\n <span\n [ngClass]=\"iconClass.class\"\n *ngIf=\"\n iconClass.condition === undefined ||\n (iconClass.condition !== undefined &&\n $any(iconClass.condition)(row))\n \"\n >{{ iconClass.text }}</span\n >\n </button>\n </ng-container>\n </td>\n </ng-container>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"columnProperties\"></tr>\n <tr\n [ngClass]=\"{\n 'example-element-row': data.isNotClickable === true,\n 'example-element-row cursor-pointer': !data.isNotClickable\n }\"\n mat-row\n *matRowDef=\"let row; columns: columnProperties\"\n (click)=\"goToDetails(row)\"\n ></tr>\n\n <!-- ROW SHOWN WHEN THERE IS NO MATCHING DATA. -->\n <tr class=\"mat-row\" *matNoDataRow>\n <td *ngIf=\"!isLoading\" class=\"mat-cell p-4\" colspan=\"4\">\n Nenhum resultado encontrado para a busca\n </td>\n </tr>\n </table>\n\n <div class=\"flex justify-center\" *ngIf=\"isLoading\">\n <mat-spinner></mat-spinner>\n </div>\n\n <div class=\"paginator-container\">\n <mat-paginator\n #paginator\n [pageSizeOptions]=\"[25, 50, 100]\"\n [pageSize]=\"pageSize\"\n [length]=\"totalItems\"\n showFirstLastButtons\n aria-label=\"Select page of periodic elements\"\n (page)=\"onPageChange($event)\"\n [ngClass]=\"{\n 'hide-length':\n ['filter', 'filterByDate', 'equals'].includes(\n this.currentArrange\n ) || this.data.filterFn,\n 'hide-next-button': !hasNextPage && data.pagination === true,\n 'hide-last-button':\n (!hasNextPage && data.pagination === true) || this.data.filterFn\n }\"\n >\n </mat-paginator>\n <div\n *ngIf=\"\n !isLoading &&\n dataSource?.data &&\n dataSource.data.length > 0 &&\n data?.filterFn\n \"\n class=\"page-number-display\"\n >\n {{ currentPageNumber }}\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- TOOLTIP PERSONALIZADO -->\n <div\n *ngIf=\"showTooltip\"\n class=\"fixed z-50 max-w-md break-words rounded-lg bg-gray-800 px-3 py-2 text-sm text-white shadow-lg\"\n [style.left.px]=\"tooltipPosition.x\"\n [style.top.px]=\"tooltipPosition.y\"\n [style.pointer-events]=\"'none'\"\n >\n {{ tooltipContent }}\n </div>\n</div>\n", styles: ["::ng-deep .hide-length .mat-mdc-paginator-range-label{display:none}::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{visibility:hidden}::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{visibility:hidden}::ng-deep .mat-mdc-text-field-wrapper.mdc-text-field.ng-tns-c162-1.mdc-text-field--filled{width:25dvw}::ng-deep .custom-filter .mat-mdc-text-field-wrapper{width:20dvw;max-width:300px}\n"], dependencies: [{ kind: "directive", type: i4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i5.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i5.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i5.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i5.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i5.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: i6.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i6.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i6.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i6.MatColumnDef, selector: "[matColumnDef]", inputs: ["sticky", "matColumnDef"] }, { kind: "directive", type: i6.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i6.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i6.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i6.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i6.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i6.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: i6.MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "component", type: i7.MatPaginator, selector: "mat-paginator", inputs: ["disabled"], exportAs: ["matPaginator"] }, { kind: "directive", type: i8.MatSort, selector: "[matSort]", inputs: ["matSortDisabled", "matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i8.MatSortHeader, selector: "[mat-sort-header]", inputs: ["disabled", "mat-sort-header", "arrowPosition", "start", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "component", type: i9.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i9.MatLabel, selector: "mat-label" }, { kind: "directive", type: i9.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i10.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: i11.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator"], exportAs: ["matSelect"] }, { kind: "component", type: i12.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { kind: "directive", type: i13.MatTooltip, selector: "[matTooltip]", exportAs: ["matTooltip"] }, { kind: "component", type: i14.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i15.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] });
1695
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableComponent, decorators: [{
1696
+ type: Component,
1697
+ args: [{ selector: 'lib-table', template: "<div *ngIf=\"data\" class=\"card-body\">\n <div class=\"flex flex-col justify-between gap-6\">\n <!-- UNIFIED CONTROL PANEL: FILTERS, SORT & ACTIONS -->\n <div\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\n *ngIf=\"\n data.pagination === true &&\n (dropdownItems.length > 0 ||\n sortableDropdownItems.length > 0 ||\n data.actionButton)\n \"\n >\n <!-- PANEL HEADER: Title, Custom Action, and Global Actions -->\n <div\n 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\"\n >\n <!-- Left Side: Title & Main Action Button -->\n <div class=\"flex flex-wrap items-center gap-4\">\n <div class=\"flex items-center gap-2\">\n <i class=\"fa fa-filter text-xl text-blue-500\"></i>\n <span class=\"text-lg font-semibold text-gray-700\"\n >Filtros e A\u00E7\u00F5es</span\n >\n </div>\n <button\n *ngIf=\"data.actionButton && data.actionButton.condition\"\n [ngClass]=\"\n (data.actionButton.colorClass || 'bg-blue-500') +\n ' flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\n \"\n [routerLink]=\"data.actionButton.routerLink\"\n (click)=\"\n data.actionButton.method ? data.actionButton.method($event) : null\n \"\n >\n <i\n *ngIf=\"data.actionButton.icon\"\n [class]=\"data.actionButton.icon\"\n ></i>\n {{ data.actionButton.label }}\n </button>\n </div>\n\n <!-- Right Side: Search, Reset, Export -->\n <div\n class=\"flex flex-wrap gap-3\"\n *ngIf=\"\n this.hasFilterableColumn === true || this.hasSortableColumn === true\n \"\n >\n <button\n (click)=\"search()\"\n type=\"button\"\n 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\"\n matTooltip=\"Aplicar filtros\"\n >\n <i class=\"fa fa-search\"></i>\n Pesquisar\n </button>\n\n <button\n (click)=\"resetFilter()\"\n 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\"\n matTooltip=\"Limpar filtros\"\n >\n <i class=\"fas fa-redo-alt\"></i>\n Resetar\n </button>\n\n <button\n *ngIf=\"data.download !== false && downloadTable\"\n 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\"\n matTooltipPosition=\"above\"\n matTooltip=\"Exportar Tabela\"\n [disabled]=\"\n this.dataSource && this.dataSource.filteredData.length <= 0\n \"\n (click)=\"\n $any(arrange) && downloadTable !== undefined\n ? downloadTable($any(arrange), data.conditions || [])\n : null\n \"\n >\n <i class=\"fa fa-download\"></i>\n Exportar\n </button>\n </div>\n </div>\n\n <!-- FILTERS CONTENT (WITH REFINEMENTS) -->\n <div class=\"mb-4 space-y-3\" *ngIf=\"filtersForm.controls.length > 0\">\n <div\n [formGroup]=\"$any(filterGroup)\"\n *ngFor=\"let filterGroup of filtersForm.controls; let i = index\"\n class=\"flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 p-2\"\n >\n <!-- FILTER TYPE SELECTOR -->\n <div class=\"min-w-[200px] flex-1\" *ngIf=\"dropdownItems.length > 0\">\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Tipo de filtro</mat-label>\n <mat-select\n placeholder=\"Selecione o tipo...\"\n formControlName=\"selectFilter\"\n (selectionChange)=\"onSelectFilterChange()\"\n >\n <mat-option *ngFor=\"let item of dropdownItems\" [value]=\"item\">\n <div class=\"flex items-center gap-2\">\n <i\n [class]=\"item.icon || 'fa fa-filter'\"\n class=\"text-sm text-blue-500\"\n ></i>\n <span>{{ item.title }}</span>\n </div>\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- TEXT FILTER -->\n <div\n class=\"min-w-[200px] flex-1\"\n *ngIf=\"\n $any(filterGroup).get('selectFilter')?.value?.arrange === 'filter'\n \"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-search text-gray-400\"></i>\n <span>{{\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\n \"Filtrar\"\n }}</span>\n </mat-label>\n <input\n (keyup.enter)=\"search()\"\n formControlName=\"typeFilter\"\n matInput\n placeholder=\"Digite para filtrar...\"\n #input\n />\n </mat-form-field>\n </div>\n\n <!-- DROPDOWN FILTER -->\n <div\n class=\"min-w-[200px] flex-1\"\n *ngIf=\"\n $any(filterGroup).get('selectFilter')?.value &&\n $any(filterGroup)\n .get('selectFilter')\n ?.value.hasOwnProperty('items')\n \"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{\n $any(filterGroup).get(\"selectFilter\")?.value?.title ||\n \"Selecione\"\n }}</mat-label>\n <mat-select\n placeholder=\"Selecione...\"\n formControlName=\"selectItem\"\n multiple\n >\n <mat-option\n *ngFor=\"\n let item of $any(filterGroup).get('selectFilter')?.value\n .items\n \"\n [value]=\"item\"\n >\n {{ item.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- DATE FILTER -->\n <div\n class=\"min-w-[340px] flex-auto\"\n *ngIf=\"\n $any(filterGroup).get('selectFilter')?.value?.arrange ===\n 'filterByDate'\n \"\n >\n <div\n class=\"flex flex-col items-stretch gap-3 sm:flex-row sm:items-center\"\n >\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-calendar text-gray-400\"></i>\n <span>Data Inicial</span>\n </mat-label>\n <input\n matInput\n (keyup.enter)=\"search()\"\n formControlName=\"initialDate\"\n placeholder=\"DD/MM/AAAA\"\n maxlength=\"10\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"flex-1\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-calendar text-gray-400\"></i>\n <span>Data Final</span>\n </mat-label>\n <input\n (keyup.enter)=\"search()\"\n matInput\n formControlName=\"finalDate\"\n placeholder=\"DD/MM/AAAA\"\n maxlength=\"10\"\n />\n </mat-form-field>\n </div>\n </div>\n\n <!-- REMOVE FILTER BUTTON -->\n <div *ngIf=\"filtersForm.length > 1\" class=\"ml-auto flex-shrink-0\">\n <button\n (click)=\"removeFilter(i)\"\n class=\"flex h-10 w-10 items-center justify-center rounded-full transition-colors duration-300 hover:bg-red-100\"\n matTooltip=\"Remover filtro\"\n >\n <i class=\"fa fa-trash text-red-500 hover:text-red-600\"></i>\n </button>\n </div>\n </div>\n </div>\n\n <!-- PANEL FOOTER: Add Filter & Sort -->\n <div\n class=\"-mb-2 flex flex-col items-center justify-between gap-4 border-t border-gray-200 pt-4 sm:flex-row\"\n >\n <!-- Add Filter Button -->\n <div *ngIf=\"dropdownItems.length > 0\">\n <button\n (click)=\"addFilter()\"\n 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\"\n matTooltip=\"Adicionar novo filtro\"\n >\n <i class=\"fa fa-plus mr-2\"></i>\n Adicionar Filtro\n </button>\n </div>\n\n <!-- Sort Dropdown -->\n <div\n class=\"w-full sm:w-auto sm:min-w-[250px]\"\n *ngIf=\"sortableDropdownItems.length > 0\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Ordenar por</mat-label>\n <mat-select placeholder=\"Selecione...\" [formControl]=\"selectSort\">\n <mat-option\n *ngFor=\"let item of sortableDropdownItems\"\n [value]=\"item\"\n >\n <div class=\"flex items-center gap-2\">\n <i class=\"fa fa-sort-alpha-down text-cyan-600\"></i>\n <span>{{ item.title }}</span>\n </div>\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </div>\n\n <!-- SIMPLE SEARCH (for non-paginated tables) -->\n <div\n class=\"rounded-xl border border-gray-200 bg-white p-4 shadow-lg\"\n *ngIf=\"data.pagination === false\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label class=\"flex items-center gap-2\">\n <i class=\"fa fa-search text-blue-500\"></i>\n Buscar\n </mat-label>\n <input\n matInput\n (keyup.enter)=\"search()\"\n (keyup)=\"applyFilter(filterInput.value)\"\n placeholder=\"Digite para filtrar...\"\n #filterInput\n />\n <mat-icon matSuffix class=\"text-gray-500\">search</mat-icon>\n </mat-form-field>\n <button\n *ngIf=\"data.actionButton\"\n [ngClass]=\"\n (data.actionButton.colorClass || 'bg-blue-500') +\n ' float-right flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white hover:opacity-70'\n \"\n [routerLink]=\"data.actionButton.routerLink\"\n (click)=\"\n data.actionButton.method ? data.actionButton.method($event) : null\n \"\n >\n <i *ngIf=\"data.actionButton.icon\" [class]=\"data.actionButton.icon\"></i>\n {{ data.actionButton.label }}\n </button>\n </div>\n\n <div class=\"flex flex-col\">\n <div\n class=\"mx-auto flex flex-col\"\n *ngIf=\"data.tabs && data.tabs.tabsData && data.tabs.tabsData.length > 0\"\n >\n <!-- Calcular quantos grupos de 6 tabs existem -->\n <ng-container\n *ngFor=\"\n let groupIndex of getTabGroups(data.tabs.tabsData);\n let i = index\n \"\n >\n <div class=\"mx-auto flex flex-row\">\n <ng-container\n *ngFor=\"\n let tab of getTabGroup(data.tabs.tabsData, groupIndex);\n let j = index\n \"\n >\n <button\n class=\"border-2 border-gray-300 bg-gray-200 px-4 py-2 font-medium transition hover:brightness-95\"\n [ngClass]=\"\n isTabSelected(getRealTabIndex(i, j))\n ? 'border-b-0 brightness-110'\n : ''\n \"\n (click)=\"onTableSelected(i, j)\"\n >\n {{ tab.label }}\n <span\n *ngIf=\"tab.counter !== undefined\"\n class=\"ml-2 text-xs font-bold\"\n [ngClass]=\"tab.counterClass\"\n >\n {{ tab.counter }}\n </span>\n </button>\n </ng-container>\n </div>\n </ng-container>\n </div>\n <div class=\"mat-elevation-z8 w-full overflow-x-auto rounded-xl\">\n <table\n mat-table\n [dataSource]=\"dataSource\"\n matSort\n #sort=\"matSort\"\n matSortActive=\"createdAt\"\n matSortDirection=\"desc\"\n >\n <ng-container\n *ngFor=\"let col of data.displayedColumns\"\n matColumnDef=\"{{ col.property }}\"\n >\n <ng-container *matHeaderCellDef>\n <!-- IF THE COLUMN IS NOT SORTABLE, THEN DON'T SHOW THE SORT BUTTONS -->\n <th\n *ngIf=\"!col.isSortable || data.pagination === true\"\n mat-header-cell\n [ngClass]=\"\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\n (data?.color?.text ? ' ' + $any(data.color).text : '')\n \"\n >\n {{ col.title }}\n </th>\n <!-- IF THE COLUMN IS SORTABLE, THEN SHOW THE SORT BUTTONS -->\n <th\n *ngIf=\"col.isSortable && data.pagination === false\"\n mat-header-cell\n mat-sort-header\n [ngClass]=\"\n (data?.color?.bg ? ' ' + $any(data.color).bg : '') +\n (data?.color?.text ? ' ' + $any(data.color).text : '')\n \"\n >\n {{ col.title }}\n </th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n (click)=\"col.method ? col.method(row) : null\"\n (mouseenter)=\"onCellMouseEnter($event, row, col)\"\n (mouseleave)=\"onCellMouseLeave()\"\n (mousemove)=\"onCellMouseMove($event)\"\n >\n <!-- CHECK IF THE COLUMN MUST BE DISPLAYED -->\n <span *ngIf=\"!col.image && !col.iconClass && !col.method\">\n <ng-container>\n <span\n *ngIf=\"\n col.charLimit &&\n row[col.property] &&\n row[col.property].length > col.charLimit;\n else withinLimit\n \"\n >\n <a\n *ngIf=\"col.hasLink === true\"\n [href]=\"row[col.property]\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row) }}\n </a>\n <a\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\n [href]=\"col.hasLink\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row) }}\n </a>\n <span\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\n >\n {{ getDisplayValue(col, row) }}\n </span>\n </span>\n </ng-container>\n <ng-template #withinLimit>\n <a\n *ngIf=\"col.hasLink === true\"\n [href]=\"row[col.property]\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row, true) }}\n </a>\n <a\n *ngIf=\"col.hasLink && isString(col.hasLink)\"\n [href]=\"col.hasLink\"\n target=\"_blank\"\n >\n {{ getDisplayValue(col, row, true) }}\n </a>\n <span\n *ngIf=\"col.hasLink !== true && !isString(col.hasLink)\"\n >\n {{ getDisplayValue(col, row, true) }}\n </span>\n </ng-template>\n </span>\n <!------------------- IMAGE ------------------>\n <img\n *ngIf=\"\n col.image && col.image.path && !col.iconClass && !col.method\n \"\n [src]=\"col.image.path + '/' + row[col.property]\"\n [ngClass]=\"col.image.class\"\n alt=\"Imagem\"\n />\n <img\n *ngIf=\"\n col.image && col.image.url && !col.iconClass && !col.method\n \"\n [src]=\"row[col.property]\"\n [ngClass]=\"col.image.class\"\n alt=\"Imagem\"\n />\n <ng-container *ngIf=\"col.iconClass\">\n <button\n *ngFor=\"let iconClass of col.iconClass\"\n (click)=\"\n iconClass.buttonMethod\n ? iconClass.buttonMethod(row, $event)\n : $event.stopPropagation()\n \"\n >\n <span\n [ngClass]=\"iconClass.class\"\n *ngIf=\"\n iconClass.condition === undefined ||\n (iconClass.condition !== undefined &&\n $any(iconClass.condition)(row))\n \"\n >{{ iconClass.text }}</span\n >\n </button>\n </ng-container>\n </td>\n </ng-container>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"columnProperties\"></tr>\n <tr\n [ngClass]=\"{\n 'example-element-row': data.isNotClickable === true,\n 'example-element-row cursor-pointer': !data.isNotClickable\n }\"\n mat-row\n *matRowDef=\"let row; columns: columnProperties\"\n (click)=\"goToDetails(row)\"\n ></tr>\n\n <!-- ROW SHOWN WHEN THERE IS NO MATCHING DATA. -->\n <tr class=\"mat-row\" *matNoDataRow>\n <td *ngIf=\"!isLoading\" class=\"mat-cell p-4\" colspan=\"4\">\n Nenhum resultado encontrado para a busca\n </td>\n </tr>\n </table>\n\n <div class=\"flex justify-center\" *ngIf=\"isLoading\">\n <mat-spinner></mat-spinner>\n </div>\n\n <div class=\"paginator-container\">\n <mat-paginator\n #paginator\n [pageSizeOptions]=\"[25, 50, 100]\"\n [pageSize]=\"pageSize\"\n [length]=\"totalItems\"\n showFirstLastButtons\n aria-label=\"Select page of periodic elements\"\n (page)=\"onPageChange($event)\"\n [ngClass]=\"{\n 'hide-length':\n ['filter', 'filterByDate', 'equals'].includes(\n this.currentArrange\n ) || this.data.filterFn,\n 'hide-next-button': !hasNextPage && data.pagination === true,\n 'hide-last-button':\n (!hasNextPage && data.pagination === true) || this.data.filterFn\n }\"\n >\n </mat-paginator>\n <div\n *ngIf=\"\n !isLoading &&\n dataSource?.data &&\n dataSource.data.length > 0 &&\n data?.filterFn\n \"\n class=\"page-number-display\"\n >\n {{ currentPageNumber }}\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- TOOLTIP PERSONALIZADO -->\n <div\n *ngIf=\"showTooltip\"\n class=\"fixed z-50 max-w-md break-words rounded-lg bg-gray-800 px-3 py-2 text-sm text-white shadow-lg\"\n [style.left.px]=\"tooltipPosition.x\"\n [style.top.px]=\"tooltipPosition.y\"\n [style.pointer-events]=\"'none'\"\n >\n {{ tooltipContent }}\n </div>\n</div>\n", styles: ["::ng-deep .hide-length .mat-mdc-paginator-range-label{display:none}::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{visibility:hidden}::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{visibility:hidden}::ng-deep .mat-mdc-text-field-wrapper.mdc-text-field.ng-tns-c162-1.mdc-text-field--filled{width:25dvw}::ng-deep .custom-filter .mat-mdc-text-field-wrapper{width:20dvw;max-width:300px}\n"] }]
1698
+ }], ctorParameters: function () { return [{ type: i1$1.Router }, { type: TableService }, { type: i1.AngularFirestore }]; }, propDecorators: { data: [{
1699
+ type: Input
1700
+ }], downloadTable: [{
1701
+ type: Input
1702
+ }], paginator: [{
1703
+ type: ViewChild,
1704
+ args: [MatPaginator]
1705
+ }], sort: [{
1706
+ type: ViewChild,
1707
+ args: [MatSort]
1716
1708
  }] } });
1717
1709
 
1718
- class FirebaseTableKxpLibModule {
1719
- }
1720
- FirebaseTableKxpLibModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1721
- FirebaseTableKxpLibModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibModule, declarations: [FirebaseTableKxpLibComponent, TableComponent], imports: [CommonModule,
1722
- ReactiveFormsModule,
1723
- FormsModule,
1724
- RouterModule,
1725
- MatTableModule,
1726
- MatPaginatorModule,
1727
- MatSortModule,
1728
- MatFormFieldModule,
1729
- MatInputModule,
1730
- MatSelectModule,
1731
- MatTooltipModule,
1732
- MatProgressSpinnerModule,
1733
- MatIconModule,
1734
- MatDialogModule], exports: [FirebaseTableKxpLibComponent, TableComponent] });
1735
- FirebaseTableKxpLibModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibModule, imports: [CommonModule,
1736
- ReactiveFormsModule,
1737
- FormsModule,
1738
- RouterModule,
1739
- MatTableModule,
1740
- MatPaginatorModule,
1741
- MatSortModule,
1742
- MatFormFieldModule,
1743
- MatInputModule,
1744
- MatSelectModule,
1745
- MatTooltipModule,
1746
- MatProgressSpinnerModule,
1747
- MatIconModule,
1748
- MatDialogModule] });
1749
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibModule, decorators: [{
1750
- type: NgModule,
1751
- args: [{
1752
- declarations: [FirebaseTableKxpLibComponent, TableComponent],
1753
- imports: [
1754
- CommonModule,
1755
- ReactiveFormsModule,
1756
- FormsModule,
1757
- RouterModule,
1758
- MatTableModule,
1759
- MatPaginatorModule,
1760
- MatSortModule,
1761
- MatFormFieldModule,
1762
- MatInputModule,
1763
- MatSelectModule,
1764
- MatTooltipModule,
1765
- MatProgressSpinnerModule,
1766
- MatIconModule,
1767
- MatDialogModule,
1768
- ],
1769
- exports: [FirebaseTableKxpLibComponent, TableComponent],
1770
- }]
1710
+ class NgFirebaseTableKxpModule {
1711
+ }
1712
+ NgFirebaseTableKxpModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1713
+ NgFirebaseTableKxpModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, declarations: [NgFirebaseTableKxpComponent, TableComponent], imports: [CommonModule,
1714
+ ReactiveFormsModule,
1715
+ FormsModule,
1716
+ RouterModule,
1717
+ MatTableModule,
1718
+ MatPaginatorModule,
1719
+ MatSortModule,
1720
+ MatFormFieldModule,
1721
+ MatInputModule,
1722
+ MatSelectModule,
1723
+ MatTooltipModule,
1724
+ MatProgressSpinnerModule,
1725
+ MatIconModule,
1726
+ MatDialogModule], exports: [NgFirebaseTableKxpComponent, TableComponent] });
1727
+ NgFirebaseTableKxpModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, imports: [CommonModule,
1728
+ ReactiveFormsModule,
1729
+ FormsModule,
1730
+ RouterModule,
1731
+ MatTableModule,
1732
+ MatPaginatorModule,
1733
+ MatSortModule,
1734
+ MatFormFieldModule,
1735
+ MatInputModule,
1736
+ MatSelectModule,
1737
+ MatTooltipModule,
1738
+ MatProgressSpinnerModule,
1739
+ MatIconModule,
1740
+ MatDialogModule] });
1741
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, decorators: [{
1742
+ type: NgModule,
1743
+ args: [{
1744
+ declarations: [NgFirebaseTableKxpComponent, TableComponent],
1745
+ imports: [
1746
+ CommonModule,
1747
+ ReactiveFormsModule,
1748
+ FormsModule,
1749
+ RouterModule,
1750
+ MatTableModule,
1751
+ MatPaginatorModule,
1752
+ MatSortModule,
1753
+ MatFormFieldModule,
1754
+ MatInputModule,
1755
+ MatSelectModule,
1756
+ MatTooltipModule,
1757
+ MatProgressSpinnerModule,
1758
+ MatIconModule,
1759
+ MatDialogModule,
1760
+ ],
1761
+ exports: [NgFirebaseTableKxpComponent, TableComponent],
1762
+ }]
1771
1763
  }] });
1772
1764
 
1773
- class FirebaseTableKxpLibService {
1774
- constructor() { }
1775
- }
1776
- FirebaseTableKxpLibService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1777
- FirebaseTableKxpLibService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibService, providedIn: 'root' });
1778
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: FirebaseTableKxpLibService, decorators: [{
1779
- type: Injectable,
1780
- args: [{
1781
- providedIn: 'root'
1782
- }]
1765
+ class NgFirebaseTableKxpService {
1766
+ constructor() { }
1767
+ }
1768
+ NgFirebaseTableKxpService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1769
+ NgFirebaseTableKxpService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, providedIn: 'root' });
1770
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, decorators: [{
1771
+ type: Injectable,
1772
+ args: [{
1773
+ providedIn: 'root',
1774
+ }]
1783
1775
  }], ctorParameters: function () { return []; } });
1784
1776
 
1785
- /*
1786
- * Public API Surface of firebase-table-kxp-lib
1787
- */
1777
+ /*
1778
+ * Public API Surface of ng-firebase-table-kxp
1779
+ */
1788
1780
  // Main module
1789
1781
 
1790
- /**
1791
- * Generated bundle index. Do not edit.
1782
+ /**
1783
+ * Generated bundle index. Do not edit.
1792
1784
  */
1793
1785
 
1794
- export { FirebaseTableKxpLibComponent, FirebaseTableKxpLibModule, FirebaseTableKxpLibService, TableComponent, TableService };
1786
+ export { NgFirebaseTableKxpComponent, NgFirebaseTableKxpModule, NgFirebaseTableKxpService, TableComponent, TableService };
1795
1787
  //# sourceMappingURL=ng-firebase-table-kxp.mjs.map