ng-firebase-table-kxp 1.0.11 → 1.0.13
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.
- package/README.md +602 -602
- package/esm2020/lib/components/table/table.component.mjs +1092 -1020
- package/esm2020/lib/ng-firebase-table-kxp.component.mjs +11 -11
- package/esm2020/lib/ng-firebase-table-kxp.module.mjs +80 -80
- package/esm2020/lib/ng-firebase-table-kxp.service.mjs +14 -14
- package/esm2020/lib/services/table.service.mjs +1112 -1112
- package/esm2020/lib/types/Table.mjs +2 -2
- package/esm2020/ng-firebase-table-kxp.mjs +4 -4
- package/esm2020/public-api.mjs +15 -15
- package/fesm2015/ng-firebase-table-kxp.mjs +2282 -2208
- package/fesm2015/ng-firebase-table-kxp.mjs.map +1 -1
- package/fesm2020/ng-firebase-table-kxp.mjs +2250 -2178
- package/fesm2020/ng-firebase-table-kxp.mjs.map +1 -1
- package/index.d.ts +5 -5
- package/lib/components/table/table.component.d.ts +120 -118
- package/lib/ng-firebase-table-kxp.component.d.ts +5 -5
- package/lib/ng-firebase-table-kxp.module.d.ts +22 -22
- package/lib/ng-firebase-table-kxp.service.d.ts +6 -6
- package/lib/services/table.service.d.ts +74 -74
- package/lib/types/Table.d.ts +139 -139
- package/package.json +1 -1
- package/public-api.d.ts +6 -6
|
@@ -35,2196 +35,2268 @@ import * as i1 from '@angular/fire/compat/firestore';
|
|
|
35
35
|
import * as i3 from 'ngx-toastr';
|
|
36
36
|
import * as i12 from '@angular/material/core';
|
|
37
37
|
|
|
38
|
-
class NgFirebaseTableKxpComponent {
|
|
39
|
-
}
|
|
40
|
-
NgFirebaseTableKxpComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
41
|
-
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 });
|
|
42
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpComponent, decorators: [{
|
|
43
|
-
type: Component,
|
|
44
|
-
args: [{ selector: 'lib-ng-firebase-table-kxp', template: ` <p>ng-firebase-table-kxp works!</p> ` }]
|
|
38
|
+
class NgFirebaseTableKxpComponent {
|
|
39
|
+
}
|
|
40
|
+
NgFirebaseTableKxpComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
41
|
+
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 });
|
|
42
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpComponent, decorators: [{
|
|
43
|
+
type: Component,
|
|
44
|
+
args: [{ selector: 'lib-ng-firebase-table-kxp', template: ` <p>ng-firebase-table-kxp works!</p> ` }]
|
|
45
45
|
}] });
|
|
46
46
|
|
|
47
|
-
class TableService {
|
|
48
|
-
constructor(ngFire, dialog, toastr) {
|
|
49
|
-
this.ngFire = ngFire;
|
|
50
|
-
this.dialog = dialog;
|
|
51
|
-
this.toastr = toastr;
|
|
52
|
-
this.operators = {
|
|
53
|
-
'==': (a, b) => a === b,
|
|
54
|
-
'!=': (a, b) => a !== b,
|
|
55
|
-
'>': (a, b) => a > b,
|
|
56
|
-
'<': (a, b) => a < b,
|
|
57
|
-
'>=': (a, b) => a >= b,
|
|
58
|
-
'<=': (a, b) => a <= b,
|
|
59
|
-
in: (a, b) => Array.isArray(b) && b.includes(a),
|
|
60
|
-
'not-in': (a, b) => Array.isArray(b) && !b.includes(a),
|
|
61
|
-
'array-contains': (a, b) => Array.isArray(a) && a.includes(b),
|
|
62
|
-
'array-contains-any': (a, b) => Array.isArray(a) &&
|
|
63
|
-
Array.isArray(b) &&
|
|
64
|
-
b.some((item) => a.includes(item)),
|
|
65
|
-
includes: (a, b) => a.includes(b), // Para strings ou arrays
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
async getItems(collection) {
|
|
69
|
-
try {
|
|
70
|
-
const querySnapshot = await collection.get();
|
|
71
|
-
return querySnapshot.docs.map((doc) => {
|
|
72
|
-
return { ...doc.data(), id: doc.id };
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
console.warn('Collection não encontrada:', error);
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async executeQuery(params) {
|
|
81
|
-
if (params.filterFn) {
|
|
82
|
-
// Lógica com filtro no cliente (filterFn)
|
|
83
|
-
const BATCH_FETCH_SIZE = params.batchSize;
|
|
84
|
-
const GOAL_SIZE = params.batchSize + 1;
|
|
85
|
-
if (params.navigation === 'forward' || params.navigation === 'reload') {
|
|
86
|
-
if (params.navigation === 'reload' && params.doc) {
|
|
87
|
-
params.doc.lastDoc = null;
|
|
88
|
-
}
|
|
89
|
-
let lastDocCursor = params.doc ? params.doc.lastDoc : null;
|
|
90
|
-
let pageResults = [];
|
|
91
|
-
let allFetchedDocs = [];
|
|
92
|
-
let hasMoreDocsInDb = true;
|
|
93
|
-
while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
|
|
94
|
-
let query = this.ngFire.collection(params.collection).ref;
|
|
95
|
-
query = this.applyFilters(query, params.arrange, params.conditions);
|
|
96
|
-
if (lastDocCursor) {
|
|
97
|
-
query = query.startAfter(lastDocCursor);
|
|
98
|
-
}
|
|
99
|
-
query = query.limit(BATCH_FETCH_SIZE);
|
|
100
|
-
const snapshot = await query.get();
|
|
101
|
-
if (snapshot.empty) {
|
|
102
|
-
hasMoreDocsInDb = false;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
lastDocCursor = snapshot.docs[snapshot.docs.length - 1];
|
|
106
|
-
allFetchedDocs.push(...snapshot.docs);
|
|
107
|
-
const batchUsers = snapshot.docs
|
|
108
|
-
.map((doc) => ({
|
|
109
|
-
id: doc.id,
|
|
110
|
-
...doc.data(),
|
|
111
|
-
}))
|
|
112
|
-
.filter(params.filterFn);
|
|
113
|
-
pageResults.push(...batchUsers);
|
|
114
|
-
if (snapshot.size < BATCH_FETCH_SIZE) {
|
|
115
|
-
hasMoreDocsInDb = false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const hasNextPage = pageResults.length > params.batchSize;
|
|
119
|
-
const finalItems = pageResults.slice(0, params.batchSize);
|
|
120
|
-
const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
|
|
121
|
-
const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
|
|
122
|
-
return {
|
|
123
|
-
items: finalItems,
|
|
124
|
-
filterLength: null,
|
|
125
|
-
firstDoc: firstDocOfPage,
|
|
126
|
-
lastDoc: lastDocOfPage,
|
|
127
|
-
hasNextPage: hasNextPage,
|
|
128
|
-
hasPreviousPage: !!(params.doc && params.doc.lastDoc) &&
|
|
129
|
-
params.navigation !== 'reload',
|
|
130
|
-
currentClientPageIndex: undefined,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
// Lógica para trás (backward)
|
|
134
|
-
else if (params.navigation === 'backward') {
|
|
135
|
-
if (!params.doc || !params.doc.firstDoc) {
|
|
136
|
-
return {
|
|
137
|
-
items: [],
|
|
138
|
-
filterLength: null,
|
|
139
|
-
firstDoc: null,
|
|
140
|
-
lastDoc: null,
|
|
141
|
-
hasNextPage: true,
|
|
142
|
-
hasPreviousPage: false,
|
|
143
|
-
currentClientPageIndex: undefined,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
let pageResults = [];
|
|
147
|
-
let allFetchedDocs = [];
|
|
148
|
-
let hasMoreDocsInDb = true;
|
|
149
|
-
let boundaryDoc = params.doc.firstDoc;
|
|
150
|
-
while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
|
|
151
|
-
let query = this.ngFire.collection(params.collection).ref;
|
|
152
|
-
query = this.applyFilters(query, params.arrange, params.conditions);
|
|
153
|
-
query = query.endBefore(boundaryDoc);
|
|
154
|
-
query = query.limitToLast(BATCH_FETCH_SIZE);
|
|
155
|
-
const snapshot = await query.get();
|
|
156
|
-
if (snapshot.empty) {
|
|
157
|
-
hasMoreDocsInDb = false;
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
boundaryDoc = snapshot.docs[0];
|
|
161
|
-
allFetchedDocs = [...snapshot.docs, ...allFetchedDocs];
|
|
162
|
-
const batchUsers = snapshot.docs
|
|
163
|
-
.map((doc) => ({
|
|
164
|
-
id: doc.id,
|
|
165
|
-
...doc.data(),
|
|
166
|
-
}))
|
|
167
|
-
.filter(params.filterFn);
|
|
168
|
-
pageResults = [...batchUsers, ...pageResults];
|
|
169
|
-
if (snapshot.size < BATCH_FETCH_SIZE) {
|
|
170
|
-
hasMoreDocsInDb = false;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const finalItems = pageResults.slice(0, params.batchSize);
|
|
174
|
-
const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
|
|
175
|
-
const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
|
|
176
|
-
return {
|
|
177
|
-
items: finalItems,
|
|
178
|
-
filterLength: null,
|
|
179
|
-
firstDoc: firstDocOfPage,
|
|
180
|
-
lastDoc: lastDocOfPage,
|
|
181
|
-
hasNextPage: true,
|
|
182
|
-
currentClientPageIndex: undefined,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
let items = [];
|
|
188
|
-
let docs = [];
|
|
189
|
-
let hasNextPage = false;
|
|
190
|
-
let filterLength = null;
|
|
191
|
-
let query = this.ngFire.collection(params.collection).ref;
|
|
192
|
-
if (params.conditions) {
|
|
193
|
-
params.conditions.forEach((c) => {
|
|
194
|
-
if (c.operator === '!=') {
|
|
195
|
-
query = query.orderBy(c.firestoreProperty);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
query = this.applyFilters(query, params.arrange, params.conditions);
|
|
200
|
-
if (params.navigation === 'reload') {
|
|
201
|
-
query = query.limit(params.batchSize + 1);
|
|
202
|
-
if (params.doc && params.doc.firstDoc) {
|
|
203
|
-
query = query.startAt(params.doc.firstDoc);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
else if (params.navigation === 'forward') {
|
|
207
|
-
query = query.limit(params.batchSize + 1);
|
|
208
|
-
if (params.doc && params.doc.lastDoc) {
|
|
209
|
-
query = query.startAfter(params.doc.lastDoc);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
// backward
|
|
214
|
-
query = query.limitToLast(params.batchSize + 1);
|
|
215
|
-
if (params.doc && params.doc.firstDoc) {
|
|
216
|
-
query = query.endBefore(params.doc.firstDoc);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const itemCol = await query.get();
|
|
220
|
-
itemCol.docs.forEach((doc) => docs.push(doc));
|
|
221
|
-
const itemPromises = docs.map(async (item) => {
|
|
222
|
-
const itemData = item.data();
|
|
223
|
-
items.push({ id: item.id, ...itemData });
|
|
224
|
-
});
|
|
225
|
-
let lastDoc = docs[docs.length - 1] || null;
|
|
226
|
-
let firstDoc = docs[0];
|
|
227
|
-
if ((items.length > params.batchSize && params.navigation === 'forward') ||
|
|
228
|
-
(params.navigation === 'reload' && items.length > params.batchSize)) {
|
|
229
|
-
lastDoc = docs[docs.length - 2] || null;
|
|
230
|
-
items.pop();
|
|
231
|
-
hasNextPage = true;
|
|
232
|
-
}
|
|
233
|
-
if (items.length > params.batchSize && params.navigation === 'backward') {
|
|
234
|
-
firstDoc = docs[1];
|
|
235
|
-
items.shift();
|
|
236
|
-
hasNextPage = true;
|
|
237
|
-
}
|
|
238
|
-
await Promise.all(itemPromises);
|
|
239
|
-
return {
|
|
240
|
-
items,
|
|
241
|
-
filterLength,
|
|
242
|
-
lastDoc,
|
|
243
|
-
firstDoc,
|
|
244
|
-
hasNextPage,
|
|
245
|
-
currentClientPageIndex: undefined,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
// Fallback para garantir que sempre retornamos algo
|
|
249
|
-
return {
|
|
250
|
-
items: [],
|
|
251
|
-
filterLength: null,
|
|
252
|
-
firstDoc: null,
|
|
253
|
-
lastDoc: null,
|
|
254
|
-
hasNextPage: false,
|
|
255
|
-
currentClientPageIndex: undefined,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
applyFilters(query, arrange, conditions) {
|
|
259
|
-
if (conditions) {
|
|
260
|
-
conditions.map((cond) => {
|
|
261
|
-
query = query.where(cond.firestoreProperty, cond.operator, cond.dashProperty);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
let hasFilterSpecificOrderBy = false;
|
|
265
|
-
let appliedOrderByField = null;
|
|
266
|
-
const equalsFilters = arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
267
|
-
const otherFilters = arrange.filters.filter((f) => f.arrange !== 'equals');
|
|
268
|
-
const equalsGroupedByProperty = equalsFilters.reduce((acc, current) => {
|
|
269
|
-
const prop = current.filter.property;
|
|
270
|
-
if (!acc[prop]) {
|
|
271
|
-
acc[prop] = [];
|
|
272
|
-
}
|
|
273
|
-
acc[prop].push(current.filter.filtering);
|
|
274
|
-
return acc;
|
|
275
|
-
}, {});
|
|
276
|
-
for (const prop in equalsGroupedByProperty) {
|
|
277
|
-
const values = equalsGroupedByProperty[prop];
|
|
278
|
-
if (values.length > 0) {
|
|
279
|
-
query = query.where(prop, 'in', values);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
otherFilters.forEach((filterItem) => {
|
|
283
|
-
// Aplicar filtragem por busca
|
|
284
|
-
if (filterItem.filter?.filtering &&
|
|
285
|
-
filterItem.filter?.property !== '' &&
|
|
286
|
-
filterItem.arrange === 'filter') {
|
|
287
|
-
query = query
|
|
288
|
-
.where(filterItem.filter.property, '>=', filterItem.filter.filtering.trim().toUpperCase())
|
|
289
|
-
.where(filterItem.filter.property, '<=', filterItem.filter.filtering.trim().toUpperCase() + '\uf8ff');
|
|
290
|
-
if (!hasFilterSpecificOrderBy) {
|
|
291
|
-
query = query.orderBy(filterItem.filter.property);
|
|
292
|
-
hasFilterSpecificOrderBy = true;
|
|
293
|
-
appliedOrderByField = filterItem.filter.property;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
// Aplicar filtro do tipo "filterByDate"
|
|
297
|
-
if (filterItem.dateFilter && filterItem.arrange === 'filterByDate') {
|
|
298
|
-
query = query
|
|
299
|
-
.where(arrange.sortBy.field, '>=', filterItem.dateFilter.initial)
|
|
300
|
-
.where(arrange.sortBy.field, '<=', filterItem.dateFilter.final);
|
|
301
|
-
if (!hasFilterSpecificOrderBy) {
|
|
302
|
-
query = query.orderBy(arrange.sortBy.field);
|
|
303
|
-
hasFilterSpecificOrderBy = true;
|
|
304
|
-
appliedOrderByField = arrange.sortBy.field;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
// Aplicar sortBy
|
|
309
|
-
if (arrange.sortBy && arrange.sortBy.field && arrange.sortBy.order) {
|
|
310
|
-
if (appliedOrderByField !== arrange.sortBy.field) {
|
|
311
|
-
query = query.orderBy(arrange.sortBy.field, arrange.sortBy.order);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return query;
|
|
315
|
-
}
|
|
316
|
-
getIdFilter(params) {
|
|
317
|
-
if (!params.arrange?.filters)
|
|
318
|
-
return null;
|
|
319
|
-
const idFilter = params.arrange.filters.find((f) => f.arrange === 'filter' &&
|
|
320
|
-
f.filter?.property === 'id' &&
|
|
321
|
-
f.filter?.filtering);
|
|
322
|
-
return idFilter?.filter?.filtering?.trim() || null;
|
|
323
|
-
}
|
|
324
|
-
async getDocumentById(collection, docId) {
|
|
325
|
-
try {
|
|
326
|
-
const docRef = this.ngFire.collection(collection).doc(docId);
|
|
327
|
-
const docSnapshot = await docRef.get().toPromise();
|
|
328
|
-
if (docSnapshot && docSnapshot.exists) {
|
|
329
|
-
return {
|
|
330
|
-
id: docSnapshot.id,
|
|
331
|
-
...docSnapshot.data(),
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
catch (error) {
|
|
337
|
-
console.warn('Erro ao buscar documento por ID:', error);
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
async searchByIdPartial(params, searchTerm) {
|
|
342
|
-
const exactMatch = await this.getDocumentById(params.collection, searchTerm);
|
|
343
|
-
if (exactMatch) {
|
|
344
|
-
if (params.conditions) {
|
|
345
|
-
const operators = this.operators;
|
|
346
|
-
const passesConditions = params.conditions.every((cond) => {
|
|
347
|
-
const operatorFn = operators[cond.operator];
|
|
348
|
-
return operatorFn
|
|
349
|
-
? operatorFn(exactMatch[cond.firestoreProperty], cond.dashProperty)
|
|
350
|
-
: false;
|
|
351
|
-
});
|
|
352
|
-
if (!passesConditions) {
|
|
353
|
-
return {
|
|
354
|
-
items: [],
|
|
355
|
-
filterLength: 0,
|
|
356
|
-
firstDoc: null,
|
|
357
|
-
lastDoc: null,
|
|
358
|
-
hasNextPage: false,
|
|
359
|
-
hasPreviousPage: false,
|
|
360
|
-
currentClientPageIndex: 0,
|
|
361
|
-
totalPages: 0,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (params.filterFn && !params.filterFn(exactMatch)) {
|
|
366
|
-
return {
|
|
367
|
-
items: [],
|
|
368
|
-
filterLength: 0,
|
|
369
|
-
firstDoc: null,
|
|
370
|
-
lastDoc: null,
|
|
371
|
-
hasNextPage: false,
|
|
372
|
-
hasPreviousPage: false,
|
|
373
|
-
currentClientPageIndex: 0,
|
|
374
|
-
totalPages: 0,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
return {
|
|
378
|
-
items: [exactMatch],
|
|
379
|
-
filterLength: 1,
|
|
380
|
-
firstDoc: null,
|
|
381
|
-
lastDoc: null,
|
|
382
|
-
hasNextPage: false,
|
|
383
|
-
hasPreviousPage: false,
|
|
384
|
-
currentClientPageIndex: 0,
|
|
385
|
-
totalPages: 1,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
const searchTermLower = searchTerm.toLowerCase();
|
|
389
|
-
const paramsWithoutIdFilter = {
|
|
390
|
-
...params,
|
|
391
|
-
arrange: {
|
|
392
|
-
...params.arrange,
|
|
393
|
-
filters: params.arrange.filters.filter((f) => !(f.arrange === 'filter' && f.filter?.property === 'id')),
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
let query = this.ngFire.collection(params.collection).ref;
|
|
397
|
-
// Aplicar conditions
|
|
398
|
-
if (params.conditions) {
|
|
399
|
-
params.conditions.forEach((cond) => {
|
|
400
|
-
query = query.where(cond.firestoreProperty, cond.operator, cond.dashProperty);
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
// Aplicar sortBy
|
|
404
|
-
if (params.arrange?.sortBy?.field && params.arrange?.sortBy?.order) {
|
|
405
|
-
query = query.orderBy(params.arrange.sortBy.field, params.arrange.sortBy.order);
|
|
406
|
-
}
|
|
407
|
-
const snapshot = await query.get();
|
|
408
|
-
let items = snapshot.docs
|
|
409
|
-
.map((doc) => ({
|
|
410
|
-
id: doc.id,
|
|
411
|
-
...doc.data(),
|
|
412
|
-
}))
|
|
413
|
-
.filter((item) => item.id.toLowerCase().includes(searchTermLower));
|
|
414
|
-
// Separar equals filters e outros filtros
|
|
415
|
-
const equalsFilters = paramsWithoutIdFilter.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
416
|
-
const otherFilters = paramsWithoutIdFilter.arrange.filters.filter((f) => f.arrange !== 'equals' &&
|
|
417
|
-
(f.arrange !== 'filter' || f.filter?.property !== 'id'));
|
|
418
|
-
// Aplicar equals filters com lógica OR dentro de cada propriedade
|
|
419
|
-
if (equalsFilters.length > 0) {
|
|
420
|
-
// Agrupar por propriedade para aplicar OR
|
|
421
|
-
const groupedByProperty = equalsFilters.reduce((acc, f) => {
|
|
422
|
-
const prop = f.filter.property;
|
|
423
|
-
if (!acc[prop]) {
|
|
424
|
-
acc[prop] = [];
|
|
425
|
-
}
|
|
426
|
-
acc[prop].push(f.filter.filtering);
|
|
427
|
-
return acc;
|
|
428
|
-
}, {});
|
|
429
|
-
// Filtrar: item deve ter pelo menos um valor de CADA propriedade
|
|
430
|
-
items = items.filter((item) => {
|
|
431
|
-
return Object.entries(groupedByProperty).every(([prop, values]) => {
|
|
432
|
-
return values.includes(item[prop]);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
// Aplicar outros filtros
|
|
437
|
-
otherFilters.forEach((filterItem) => {
|
|
438
|
-
if (filterItem.arrange === 'filter' &&
|
|
439
|
-
filterItem.filter?.filtering &&
|
|
440
|
-
filterItem.filter?.property) {
|
|
441
|
-
const filterValue = String(filterItem.filter.filtering)
|
|
442
|
-
.trim()
|
|
443
|
-
.toLowerCase();
|
|
444
|
-
items = items.filter((item) => {
|
|
445
|
-
const itemValue = String(item[filterItem.filter.property]).toLowerCase();
|
|
446
|
-
return itemValue.includes(filterValue);
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
// Aplicar filterFn se existir
|
|
451
|
-
if (params.filterFn) {
|
|
452
|
-
items = items.filter(params.filterFn);
|
|
453
|
-
}
|
|
454
|
-
// Paginação
|
|
455
|
-
const pageSize = params.batchSize;
|
|
456
|
-
let currentClientPageIndex = 0;
|
|
457
|
-
if (params.navigation === 'reload') {
|
|
458
|
-
currentClientPageIndex = 0;
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
// Usar o índice passado pelo componente sem incrementar/decrementar
|
|
462
|
-
currentClientPageIndex = params.clientPageIndex || 0;
|
|
463
|
-
}
|
|
464
|
-
const startIndex = currentClientPageIndex * pageSize;
|
|
465
|
-
const endIndex = startIndex + pageSize;
|
|
466
|
-
const paginatedItems = items.slice(startIndex, endIndex);
|
|
467
|
-
const totalPages = Math.ceil(items.length / pageSize);
|
|
468
|
-
const hasNextPage = currentClientPageIndex < totalPages - 1;
|
|
469
|
-
const hasPreviousPage = currentClientPageIndex > 0;
|
|
470
|
-
return {
|
|
471
|
-
items: paginatedItems,
|
|
472
|
-
filterLength: items.length,
|
|
473
|
-
firstDoc: null,
|
|
474
|
-
lastDoc: null,
|
|
475
|
-
hasNextPage,
|
|
476
|
-
hasPreviousPage,
|
|
477
|
-
currentClientPageIndex,
|
|
478
|
-
totalPages,
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
shouldUseClientSideFallback(params) {
|
|
482
|
-
const hasConditions = params.conditions && params.conditions.length > 0;
|
|
483
|
-
const hasArrangeFilters = params.arrange?.filters && params.arrange.filters.length > 0;
|
|
484
|
-
const hasSortBy = params.arrange?.sortBy?.field;
|
|
485
|
-
if (params.filterFn) {
|
|
486
|
-
return false;
|
|
487
|
-
}
|
|
488
|
-
if (hasArrangeFilters) {
|
|
489
|
-
const equalsFilters = params.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
490
|
-
if (equalsFilters.length > 0) {
|
|
491
|
-
const propertiesSet = new Set(equalsFilters.map((f) => f.filter.property));
|
|
492
|
-
if (propertiesSet.size > 1) {
|
|
493
|
-
return true;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
if (hasConditions && hasArrangeFilters && hasSortBy) {
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
if (hasConditions && hasArrangeFilters) {
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
if (hasArrangeFilters && params.arrange.filters.length > 1 && hasSortBy) {
|
|
504
|
-
const equalsFilters = params.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
505
|
-
if (equalsFilters.length > 0) {
|
|
506
|
-
const propertiesSet = new Set(equalsFilters.map((f) => f.filter.property));
|
|
507
|
-
if (propertiesSet.size === 1 &&
|
|
508
|
-
equalsFilters.length === params.arrange.filters.length) {
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return true;
|
|
513
|
-
}
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
async getPaginated(params) {
|
|
517
|
-
const idFilterValue = this.getIdFilter(params);
|
|
518
|
-
if (idFilterValue) {
|
|
519
|
-
const result = await this.searchByIdPartial(params, idFilterValue);
|
|
520
|
-
return result;
|
|
521
|
-
}
|
|
522
|
-
// Detectar preventivamente se deve usar fallback
|
|
523
|
-
if (this.shouldUseClientSideFallback(params)) {
|
|
524
|
-
await this.trackMissingIndexPreventive(params.collection, params.arrange, params.conditions);
|
|
525
|
-
const result = await this.executeClientSideQuery(params);
|
|
526
|
-
console.log('📊 [TABLE] Resultados paginados via fallback client-side:', {
|
|
527
|
-
totalItems: result.filterLength,
|
|
528
|
-
returnedItems: result.items.length,
|
|
529
|
-
hasNextPage: result.hasNextPage,
|
|
530
|
-
currentPage: (result.currentClientPageIndex || 0) + 1,
|
|
531
|
-
});
|
|
532
|
-
return result;
|
|
533
|
-
}
|
|
534
|
-
try {
|
|
535
|
-
const result = await this.executeQuery(params);
|
|
536
|
-
console.log('📊 [TABLE] Resultados paginados via Firestore:', {
|
|
537
|
-
totalItems: result.filterLength || 'N/A',
|
|
538
|
-
returnedItems: result.items.length,
|
|
539
|
-
hasNextPage: result.hasNextPage,
|
|
540
|
-
});
|
|
541
|
-
return result;
|
|
542
|
-
}
|
|
543
|
-
catch (error) {
|
|
544
|
-
if (error && error.code === 'failed-precondition') {
|
|
545
|
-
await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
|
|
546
|
-
const result = await this.executeClientSideQuery(params);
|
|
547
|
-
console.log('📊 [TABLE] Resultados paginados via fallback (erro de index):', {
|
|
548
|
-
totalItems: result.filterLength,
|
|
549
|
-
returnedItems: result.items.length,
|
|
550
|
-
hasNextPage: result.hasNextPage,
|
|
551
|
-
currentPage: (result.currentClientPageIndex || 0) + 1,
|
|
552
|
-
});
|
|
553
|
-
return result;
|
|
554
|
-
}
|
|
555
|
-
else if (error && error.code === 'invalid-argument') {
|
|
556
|
-
await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
|
|
557
|
-
const result = await this.executeClientSideQuery(params);
|
|
558
|
-
console.log('📊 [TABLE] Resultados paginados via fallback (argumento inválido):', {
|
|
559
|
-
totalItems: result.filterLength,
|
|
560
|
-
returnedItems: result.items.length,
|
|
561
|
-
hasNextPage: result.hasNextPage,
|
|
562
|
-
currentPage: (result.currentClientPageIndex || 0) + 1,
|
|
563
|
-
});
|
|
564
|
-
return result;
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
throw error;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
async executeClientSideQuery(params) {
|
|
572
|
-
// Otimizar usando pelo menos uma cláusula .where() quando possível
|
|
573
|
-
let query = this.ngFire.collection(params.collection).ref;
|
|
574
|
-
let appliedCondition = null;
|
|
575
|
-
let hasAppliedWhereClause = false;
|
|
576
|
-
// Primeiro, tenta aplicar condições simples
|
|
577
|
-
if (params.conditions && params.conditions.length > 0) {
|
|
578
|
-
const simpleCondition = params.conditions.find((cond) => ['==', '>', '<', '>=', '<=', 'in', 'array-contains'].includes(cond.operator));
|
|
579
|
-
if (simpleCondition) {
|
|
580
|
-
query = query.where(simpleCondition.firestoreProperty, simpleCondition.operator, simpleCondition.dashProperty);
|
|
581
|
-
appliedCondition = simpleCondition;
|
|
582
|
-
hasAppliedWhereClause = true;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
// Se não há condições disponíveis, tenta aplicar filtros do arrange
|
|
586
|
-
let appliedFirestoreFilter = null;
|
|
587
|
-
if (!hasAppliedWhereClause && params.arrange?.filters) {
|
|
588
|
-
// Agrupar equals filters por propriedade
|
|
589
|
-
const equalsFilters = params.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
590
|
-
const equalsGroupedByProperty = equalsFilters.reduce((acc, current) => {
|
|
591
|
-
const prop = current.filter.property;
|
|
592
|
-
if (!acc[prop]) {
|
|
593
|
-
acc[prop] = [];
|
|
594
|
-
}
|
|
595
|
-
acc[prop].push(current.filter.filtering);
|
|
596
|
-
return acc;
|
|
597
|
-
}, {});
|
|
598
|
-
// Se há apenas UMA propriedade com múltiplos valores, aplicar no Firestore com 'in'
|
|
599
|
-
const properties = Object.keys(equalsGroupedByProperty);
|
|
600
|
-
if (properties.length === 1 && equalsFilters.length > 0) {
|
|
601
|
-
const prop = properties[0];
|
|
602
|
-
const values = equalsGroupedByProperty[prop];
|
|
603
|
-
query = query.where(prop, 'in', values);
|
|
604
|
-
hasAppliedWhereClause = true;
|
|
605
|
-
// Marcar TODOS os equals filters dessa propriedade como aplicados
|
|
606
|
-
appliedFirestoreFilter = 'all-equals';
|
|
607
|
-
}
|
|
608
|
-
else if (properties.length === 0) {
|
|
609
|
-
const otherFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
|
|
610
|
-
f.filter?.filtering &&
|
|
611
|
-
f.filter?.property) ||
|
|
612
|
-
(f.arrange === 'filterByDate' &&
|
|
613
|
-
f.dateFilter?.initial &&
|
|
614
|
-
f.dateFilter?.final));
|
|
615
|
-
if (otherFilter) {
|
|
616
|
-
if (otherFilter.arrange === 'filter' && otherFilter.filter) {
|
|
617
|
-
const filterValue = otherFilter.filter.filtering
|
|
618
|
-
.trim()
|
|
619
|
-
.toUpperCase();
|
|
620
|
-
query = query
|
|
621
|
-
.where(otherFilter.filter.property, '>=', filterValue)
|
|
622
|
-
.where(otherFilter.filter.property, '<=', filterValue + '\uf8ff');
|
|
623
|
-
hasAppliedWhereClause = true;
|
|
624
|
-
appliedFirestoreFilter = otherFilter;
|
|
625
|
-
}
|
|
626
|
-
else if (otherFilter.arrange === 'filterByDate' &&
|
|
627
|
-
otherFilter.dateFilter &&
|
|
628
|
-
params.arrange.sortBy?.field) {
|
|
629
|
-
query = query
|
|
630
|
-
.where(params.arrange.sortBy.field, '>=', otherFilter.dateFilter.initial)
|
|
631
|
-
.where(params.arrange.sortBy.field, '<=', otherFilter.dateFilter.final);
|
|
632
|
-
hasAppliedWhereClause = true;
|
|
633
|
-
appliedFirestoreFilter = otherFilter;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
const allDocsSnapshot = await query.get();
|
|
639
|
-
let items = allDocsSnapshot.docs.map((doc) => ({
|
|
640
|
-
id: doc.id,
|
|
641
|
-
...doc.data(),
|
|
642
|
-
}));
|
|
643
|
-
// Aplicar condições restantes
|
|
644
|
-
if (params.conditions) {
|
|
645
|
-
const remainingConditions = params.conditions.filter((cond) => cond !== appliedCondition);
|
|
646
|
-
if (remainingConditions.length > 0) {
|
|
647
|
-
const operators = this.operators;
|
|
648
|
-
items = items.filter((item) => {
|
|
649
|
-
return remainingConditions.every((cond) => {
|
|
650
|
-
const operatorFn = operators[cond.operator];
|
|
651
|
-
return operatorFn
|
|
652
|
-
? operatorFn(item[cond.firestoreProperty], cond.dashProperty)
|
|
653
|
-
: false;
|
|
654
|
-
});
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
const { filters, sortBy } = params.arrange;
|
|
659
|
-
// Track which filter was already applied in Firestore to avoid double filtering
|
|
660
|
-
if (hasAppliedWhereClause && !appliedCondition && params.arrange?.filters) {
|
|
661
|
-
const equalsFilter = params.arrange.filters.find((f) => f.arrange === 'equals' && f.filter?.filtering);
|
|
662
|
-
if (equalsFilter) {
|
|
663
|
-
appliedFirestoreFilter = equalsFilter;
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
appliedFirestoreFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
|
|
667
|
-
f.filter?.filtering &&
|
|
668
|
-
f.filter?.property) ||
|
|
669
|
-
(f.arrange === 'filterByDate' &&
|
|
670
|
-
f.dateFilter?.initial &&
|
|
671
|
-
f.dateFilter?.final));
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
const equalsFilters = filters.filter((f) => f.arrange === 'equals');
|
|
675
|
-
const otherFilters = filters.filter((f) => f.arrange !== 'equals');
|
|
676
|
-
// Aplicar equals filters no client-side apenas se não foram aplicados no Firestore
|
|
677
|
-
if (appliedFirestoreFilter !== 'all-equals' && equalsFilters.length > 0) {
|
|
678
|
-
// Agrupar por propriedade para aplicar OR dentro de cada propriedade
|
|
679
|
-
const groupedByProperty = equalsFilters.reduce((acc, f) => {
|
|
680
|
-
const prop = f.filter.property;
|
|
681
|
-
if (!acc[prop]) {
|
|
682
|
-
acc[prop] = [];
|
|
683
|
-
}
|
|
684
|
-
acc[prop].push(f.filter.filtering);
|
|
685
|
-
return acc;
|
|
686
|
-
}, {});
|
|
687
|
-
// Filtrar: item deve ter pelo menos um valor de CADA propriedade
|
|
688
|
-
// (AND entre propriedades, OR dentro de cada propriedade)
|
|
689
|
-
items = items.filter((item) => {
|
|
690
|
-
return Object.entries(groupedByProperty).every(([prop, values]) => {
|
|
691
|
-
return values.includes(item[prop]);
|
|
692
|
-
});
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
otherFilters.forEach((filterItem) => {
|
|
696
|
-
if (appliedFirestoreFilter === filterItem) {
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (filterItem.arrange === 'filter' &&
|
|
700
|
-
filterItem.filter?.filtering &&
|
|
701
|
-
filterItem.filter?.property) {
|
|
702
|
-
const filterValue = String(filterItem.filter.filtering)
|
|
703
|
-
.trim()
|
|
704
|
-
.toLowerCase();
|
|
705
|
-
items = items.filter((item) => {
|
|
706
|
-
const itemValue = String(item[filterItem.filter.property]).toLowerCase();
|
|
707
|
-
return itemValue.includes(filterValue);
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
if (filterItem.arrange === 'filterByDate' &&
|
|
711
|
-
filterItem.dateFilter?.initial &&
|
|
712
|
-
filterItem.dateFilter?.final &&
|
|
713
|
-
sortBy.field) {
|
|
714
|
-
items = items.filter((item) => {
|
|
715
|
-
try {
|
|
716
|
-
const fieldValue = item[sortBy.field];
|
|
717
|
-
if (!fieldValue) {
|
|
718
|
-
return false;
|
|
719
|
-
}
|
|
720
|
-
let itemDate;
|
|
721
|
-
if (typeof fieldValue.toDate === 'function') {
|
|
722
|
-
itemDate = fieldValue.toDate();
|
|
723
|
-
}
|
|
724
|
-
else if (fieldValue instanceof Date) {
|
|
725
|
-
itemDate = fieldValue;
|
|
726
|
-
}
|
|
727
|
-
else if (typeof fieldValue === 'string') {
|
|
728
|
-
itemDate = new Date(fieldValue);
|
|
729
|
-
if (isNaN(itemDate.getTime())) {
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
else if (typeof fieldValue === 'number') {
|
|
734
|
-
itemDate = new Date(fieldValue);
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
return false;
|
|
738
|
-
}
|
|
739
|
-
return (itemDate >= filterItem.dateFilter.initial &&
|
|
740
|
-
itemDate <= filterItem.dateFilter.final);
|
|
741
|
-
}
|
|
742
|
-
catch (error) {
|
|
743
|
-
console.warn('Erro ao processar filtro de data para o item:', item.id, error);
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
// Aplicar filterFn se existir
|
|
750
|
-
if (params.filterFn) {
|
|
751
|
-
items = items.filter(params.filterFn);
|
|
752
|
-
}
|
|
753
|
-
if (sortBy && sortBy.field && sortBy.order) {
|
|
754
|
-
items.sort((a, b) => {
|
|
755
|
-
const valA = a[sortBy.field];
|
|
756
|
-
const valB = b[sortBy.field];
|
|
757
|
-
if (valA < valB) {
|
|
758
|
-
return sortBy.order === 'asc' ? -1 : 1;
|
|
759
|
-
}
|
|
760
|
-
if (valA > valB) {
|
|
761
|
-
return sortBy.order === 'asc' ? 1 : -1;
|
|
762
|
-
}
|
|
763
|
-
return 0;
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
// Implementação adequada da paginação
|
|
767
|
-
let currentClientPageIndex = 0;
|
|
768
|
-
// Determinar a página atual baseada na navegação
|
|
769
|
-
if (params.navigation === 'reload') {
|
|
770
|
-
currentClientPageIndex = 0;
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
currentClientPageIndex = params.clientPageIndex || 0;
|
|
774
|
-
}
|
|
775
|
-
const pageSize = params.batchSize;
|
|
776
|
-
const startIndex = currentClientPageIndex * pageSize;
|
|
777
|
-
const endIndex = startIndex + pageSize;
|
|
778
|
-
const paginatedItems = items.slice(startIndex, endIndex);
|
|
779
|
-
const totalPages = Math.ceil(items.length / pageSize);
|
|
780
|
-
const hasNextPage = currentClientPageIndex < totalPages - 1;
|
|
781
|
-
const hasPreviousPage = currentClientPageIndex > 0;
|
|
782
|
-
return {
|
|
783
|
-
items: paginatedItems,
|
|
784
|
-
filterLength: items.length,
|
|
785
|
-
lastDoc: null,
|
|
786
|
-
firstDoc: null,
|
|
787
|
-
hasNextPage: hasNextPage,
|
|
788
|
-
hasPreviousPage: hasPreviousPage,
|
|
789
|
-
currentClientPageIndex: currentClientPageIndex,
|
|
790
|
-
totalPages: totalPages,
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
async getItemsData(collection, arrange, conditions = undefined) {
|
|
794
|
-
try {
|
|
795
|
-
let query = this.ngFire.collection(collection).ref;
|
|
796
|
-
query = this.applyFilters(query, arrange, conditions);
|
|
797
|
-
const snapshot = await query.get();
|
|
798
|
-
return await Promise.all(snapshot.docs.map(async (doc) => {
|
|
799
|
-
const data = doc.data();
|
|
800
|
-
const id = doc.id;
|
|
801
|
-
return {
|
|
802
|
-
id,
|
|
803
|
-
...data,
|
|
804
|
-
};
|
|
805
|
-
}));
|
|
806
|
-
}
|
|
807
|
-
catch (e) {
|
|
808
|
-
throw e;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
async deleteIndex(id, col) {
|
|
812
|
-
try {
|
|
813
|
-
const batch = this.ngFire.firestore.batch();
|
|
814
|
-
const docRef = this.ngFire.collection(col).doc(id);
|
|
815
|
-
const docSnapshot = (await firstValueFrom(docRef.get()));
|
|
816
|
-
const doc = docSnapshot.data();
|
|
817
|
-
batch.delete(docRef.ref);
|
|
818
|
-
if (doc && typeof doc.index === 'number') {
|
|
819
|
-
await this.reindex(doc.index, col, batch);
|
|
820
|
-
}
|
|
821
|
-
await batch.commit();
|
|
822
|
-
this.toastr.success('Item excluído com sucesso!');
|
|
823
|
-
return true;
|
|
824
|
-
}
|
|
825
|
-
catch (e) {
|
|
826
|
-
const error = e;
|
|
827
|
-
console.error('Erro ao deletar item:', error);
|
|
828
|
-
this.toastr.error('Erro ao deletar item.');
|
|
829
|
-
return false;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
async reindex(index, col, batch) {
|
|
833
|
-
try {
|
|
834
|
-
const snapshot = (await firstValueFrom(this.ngFire.collection(col).get()));
|
|
835
|
-
const docs = snapshot.docs;
|
|
836
|
-
for (let doc of docs) {
|
|
837
|
-
const data = doc.data();
|
|
838
|
-
if (data && typeof data.index === 'number' && data.index > index) {
|
|
839
|
-
data.index--;
|
|
840
|
-
const docRef = this.ngFire.collection(col).doc(doc.id).ref;
|
|
841
|
-
batch.update(docRef, data);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
console.error('Erro ao reindexar:', error);
|
|
847
|
-
}
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
dateFormatValidator() {
|
|
851
|
-
return (control) => {
|
|
852
|
-
if (!control.value) {
|
|
853
|
-
return null;
|
|
854
|
-
}
|
|
855
|
-
const dateStr = control.value.trim();
|
|
856
|
-
const datePattern = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
|
|
857
|
-
if (!datePattern.test(dateStr)) {
|
|
858
|
-
return { invalidFormat: true };
|
|
859
|
-
}
|
|
860
|
-
const parts = dateStr.split('/');
|
|
861
|
-
const day = parts[0].padStart(2, '0');
|
|
862
|
-
const month = parts[1].padStart(2, '0');
|
|
863
|
-
const year = parts[2];
|
|
864
|
-
const normalizedDate = `${day}/${month}/${year}`;
|
|
865
|
-
const date = moment(normalizedDate, 'DD/MM/YYYY', true);
|
|
866
|
-
if (!date.isValid()) {
|
|
867
|
-
return { invalidDate: true };
|
|
868
|
-
}
|
|
869
|
-
return null;
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
async updateIndex(index, id, col) {
|
|
873
|
-
await this.ngFire.collection(col).doc(id).update({ index });
|
|
874
|
-
}
|
|
875
|
-
/**
|
|
876
|
-
* Extrai o link de criação de índice da mensagem de erro do Firestore
|
|
877
|
-
*/
|
|
878
|
-
extractIndexLink(error) {
|
|
879
|
-
if (!error || !error.message)
|
|
880
|
-
return null;
|
|
881
|
-
const linkMatch = error.message.match(/(https:\/\/console\.firebase\.google\.com\/[^\s]+)/);
|
|
882
|
-
return linkMatch ? linkMatch[1] : null;
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Rastreia índices ausentes ao usar fallback preventivo
|
|
886
|
-
*/
|
|
887
|
-
async trackMissingIndexPreventive(collection, arrange, conditions = undefined) {
|
|
888
|
-
try {
|
|
889
|
-
const querySignature = this.generateQuerySignature(collection, arrange, conditions);
|
|
890
|
-
const docId = `${collection}_${querySignature}`;
|
|
891
|
-
const indexLink = this.generateIndexLink(collection, arrange, conditions);
|
|
892
|
-
const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
|
|
893
|
-
const trackingData = {
|
|
894
|
-
collection,
|
|
895
|
-
indexLink,
|
|
896
|
-
indexInstructions,
|
|
897
|
-
arrange: {
|
|
898
|
-
sortBy: arrange.sortBy,
|
|
899
|
-
filters: arrange.filters?.map((f) => ({
|
|
900
|
-
arrange: f.arrange,
|
|
901
|
-
property: f.filter?.property || null,
|
|
902
|
-
dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
|
|
903
|
-
})) || [],
|
|
904
|
-
},
|
|
905
|
-
conditions: conditions?.map((c) => ({
|
|
906
|
-
property: c.firestoreProperty,
|
|
907
|
-
operator: c.operator,
|
|
908
|
-
})) || [],
|
|
909
|
-
errorMessage: `Fallback preventivo usado para a collection ${collection}. A query exigiria índice composto.`,
|
|
910
|
-
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
911
|
-
};
|
|
912
|
-
console.log('📄 [INDEX LINK] Dados que serão salvos no documento:', {
|
|
913
|
-
docId,
|
|
914
|
-
collection: trackingData.collection,
|
|
915
|
-
indexLink: trackingData.indexLink,
|
|
916
|
-
arrange: trackingData.arrange,
|
|
917
|
-
conditions: trackingData.conditions,
|
|
918
|
-
errorMessage: trackingData.errorMessage,
|
|
919
|
-
});
|
|
920
|
-
const docRef = this.ngFire.collection('missingIndexes').doc(docId);
|
|
921
|
-
const doc = await docRef.get().toPromise();
|
|
922
|
-
if (doc && doc.exists) {
|
|
923
|
-
await docRef.update({
|
|
924
|
-
count: firebase.firestore.FieldValue.increment(1),
|
|
925
|
-
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
926
|
-
lastError: trackingData.errorMessage,
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
else {
|
|
930
|
-
await docRef.set({
|
|
931
|
-
...trackingData,
|
|
932
|
-
count: 1,
|
|
933
|
-
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
catch (trackingError) {
|
|
938
|
-
console.warn('Falha ao rastrear fallback preventivo:', trackingError);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
/**
|
|
942
|
-
* Gera uma assinatura única para uma query
|
|
943
|
-
*/
|
|
944
|
-
generateQuerySignature(collection, arrange, conditions = undefined) {
|
|
945
|
-
const signature = {
|
|
946
|
-
collection,
|
|
947
|
-
sortBy: arrange.sortBy,
|
|
948
|
-
filters: arrange.filters?.map((f) => ({
|
|
949
|
-
arrange: f.arrange,
|
|
950
|
-
property: f.filter?.property || null,
|
|
951
|
-
})) || [],
|
|
952
|
-
conditions: conditions?.map((c) => ({
|
|
953
|
-
property: c.firestoreProperty,
|
|
954
|
-
operator: c.operator,
|
|
955
|
-
})) || [],
|
|
956
|
-
};
|
|
957
|
-
return btoa(JSON.stringify(signature))
|
|
958
|
-
.replace(/[^a-zA-Z0-9]/g, '')
|
|
959
|
-
.substring(0, 20);
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Gera instruções claras para criar o índice manualmente
|
|
963
|
-
*/
|
|
964
|
-
generateIndexInstructions(collection, arrange, conditions = undefined) {
|
|
965
|
-
const instructions = {
|
|
966
|
-
summary: '',
|
|
967
|
-
collection: collection,
|
|
968
|
-
fields: [],
|
|
969
|
-
queryExample: '',
|
|
970
|
-
stepByStep: [],
|
|
971
|
-
notes: [],
|
|
972
|
-
};
|
|
973
|
-
const fields = [];
|
|
974
|
-
if (conditions && conditions.length > 0) {
|
|
975
|
-
conditions.forEach((condition) => {
|
|
976
|
-
if (condition.firestoreProperty) {
|
|
977
|
-
fields.push({
|
|
978
|
-
field: condition.firestoreProperty,
|
|
979
|
-
order: 'Ascending',
|
|
980
|
-
type: 'WHERE clause',
|
|
981
|
-
operator: condition.operator,
|
|
982
|
-
description: `Filtrar por ${condition.firestoreProperty} usando operador ${condition.operator}`,
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
if (arrange.filters && arrange.filters.length > 0) {
|
|
988
|
-
arrange.filters.forEach((filter) => {
|
|
989
|
-
if (filter.filter?.property) {
|
|
990
|
-
fields.push({
|
|
991
|
-
field: filter.filter.property,
|
|
992
|
-
order: 'Ascending',
|
|
993
|
-
type: 'WHERE clause (filter)',
|
|
994
|
-
operator: filter.arrange === 'filter' ? 'CONTAINS' : 'RANGE',
|
|
995
|
-
description: `Filtrar por ${filter.filter.property} usando filtro ${filter.arrange}`,
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
if (arrange.sortBy?.field) {
|
|
1001
|
-
fields.push({
|
|
1002
|
-
field: arrange.sortBy.field,
|
|
1003
|
-
order: arrange.sortBy.order === 'desc' ? 'Descending' : 'Ascending',
|
|
1004
|
-
type: 'ORDER BY clause',
|
|
1005
|
-
operator: 'N/A',
|
|
1006
|
-
description: `Ordenar resultados por ${arrange.sortBy.field} em ordem ${arrange.sortBy.order}`,
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
instructions.fields = fields;
|
|
1010
|
-
const fieldNames = fields.map((f) => f.field).join(' + ');
|
|
1011
|
-
instructions.summary = `Criar índice composto para ${collection}: ${fieldNames}`;
|
|
1012
|
-
let queryExample = `db.collection('${collection}')`;
|
|
1013
|
-
fields.forEach((field, index) => {
|
|
1014
|
-
if (field.type.includes('WHERE')) {
|
|
1015
|
-
if (field.operator === '==') {
|
|
1016
|
-
queryExample += `\n .where('${field.field}', '==', 'value')`;
|
|
1017
|
-
}
|
|
1018
|
-
else if (field.operator === 'CONTAINS') {
|
|
1019
|
-
queryExample += `\n .where('${field.field}', '>=', 'searchText')`;
|
|
1020
|
-
}
|
|
1021
|
-
else {
|
|
1022
|
-
queryExample += `\n .where('${field.field}', '${field.operator}', 'value')`;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
const orderByField = fields.find((f) => f.type.includes('ORDER BY'));
|
|
1027
|
-
if (orderByField) {
|
|
1028
|
-
queryExample += `\n .orderBy('${orderByField.field}', '${orderByField.order.toLowerCase()}')`;
|
|
1029
|
-
}
|
|
1030
|
-
instructions.queryExample = queryExample;
|
|
1031
|
-
instructions.stepByStep = [
|
|
1032
|
-
'1. Ir para Firebase Console → Firestore → Indexes',
|
|
1033
|
-
'2. Clicar em "Create Index"',
|
|
1034
|
-
`3. Definir Collection ID: ${collection}`,
|
|
1035
|
-
'4. Configurar campos nesta ORDEM EXATA:',
|
|
1036
|
-
...fields.map((field, index) => ` ${index + 1}. Campo: ${field.field}, Order: ${field.order}, Array: No`),
|
|
1037
|
-
'5. Definir Query scopes: Collection',
|
|
1038
|
-
'6. Clicar em "Create" e aguardar conclusão',
|
|
1039
|
-
];
|
|
1040
|
-
instructions.notes = [
|
|
1041
|
-
'⚠️ A ordem dos campos é CRÍTICA - deve corresponder exatamente à ordem da query',
|
|
1042
|
-
'⚠️ As cláusulas WHERE devem vir ANTES do campo ORDER BY',
|
|
1043
|
-
'⚠️ Este índice só funcionará para queries com esta combinação EXATA de campos',
|
|
1044
|
-
'⚠️ A criação do índice pode levar vários minutos',
|
|
1045
|
-
];
|
|
1046
|
-
return instructions;
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Gera um link de índice baseado na estrutura da query
|
|
1050
|
-
*/
|
|
1051
|
-
generateIndexLink(collection, arrange, conditions = undefined) {
|
|
1052
|
-
try {
|
|
1053
|
-
const indexFields = [];
|
|
1054
|
-
if (conditions && conditions.length > 0) {
|
|
1055
|
-
conditions.forEach((condition) => {
|
|
1056
|
-
if (condition.firestoreProperty) {
|
|
1057
|
-
indexFields.push(condition.firestoreProperty);
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
if (arrange.filters && arrange.filters.length > 0) {
|
|
1062
|
-
arrange.filters.forEach((filter) => {
|
|
1063
|
-
if (filter.filter?.property) {
|
|
1064
|
-
indexFields.push(filter.filter.property);
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
if (arrange.sortBy?.field) {
|
|
1069
|
-
indexFields.push(arrange.sortBy.field);
|
|
1070
|
-
}
|
|
1071
|
-
if (indexFields.length > 1) {
|
|
1072
|
-
const baseUrl = 'https://console.firebase.google.com/project/toppayy-dev/firestore/indexes';
|
|
1073
|
-
const queryParams = new URLSearchParams({
|
|
1074
|
-
create_composite: `collection=${collection}&fields=${indexFields.join(',')}`,
|
|
1075
|
-
});
|
|
1076
|
-
const finalLink = `${baseUrl}?${queryParams.toString()}`;
|
|
1077
|
-
return finalLink;
|
|
1078
|
-
}
|
|
1079
|
-
return null;
|
|
1080
|
-
}
|
|
1081
|
-
catch (error) {
|
|
1082
|
-
console.warn('Falha ao gerar link de índice:', error);
|
|
1083
|
-
return null;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
async trackMissingIndex(error, collection, arrange, conditions = undefined) {
|
|
1087
|
-
try {
|
|
1088
|
-
const indexLink = this.extractIndexLink(error);
|
|
1089
|
-
if (!indexLink)
|
|
1090
|
-
return;
|
|
1091
|
-
const linkHash = btoa(indexLink)
|
|
1092
|
-
.replace(/[^a-zA-Z0-9]/g, '')
|
|
1093
|
-
.substring(0, 20);
|
|
1094
|
-
const docId = `${collection}_${linkHash}`;
|
|
1095
|
-
const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
|
|
1096
|
-
const trackingData = {
|
|
1097
|
-
collection,
|
|
1098
|
-
indexLink,
|
|
1099
|
-
indexInstructions,
|
|
1100
|
-
arrange: {
|
|
1101
|
-
sortBy: arrange.sortBy,
|
|
1102
|
-
filters: arrange.filters?.map((f) => ({
|
|
1103
|
-
arrange: f.arrange,
|
|
1104
|
-
property: f.filter?.property || null,
|
|
1105
|
-
dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
|
|
1106
|
-
})) || [],
|
|
1107
|
-
},
|
|
1108
|
-
conditions: conditions?.map((c) => ({
|
|
1109
|
-
property: c.firestoreProperty,
|
|
1110
|
-
operator: c.operator,
|
|
1111
|
-
})) || [],
|
|
1112
|
-
errorMessage: error.message,
|
|
1113
|
-
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
1114
|
-
};
|
|
1115
|
-
const docRef = this.ngFire.collection('missingIndexes').doc(docId);
|
|
1116
|
-
const doc = await docRef.get().toPromise();
|
|
1117
|
-
if (doc && doc.exists) {
|
|
1118
|
-
await docRef.update({
|
|
1119
|
-
count: firebase.firestore.FieldValue.increment(1),
|
|
1120
|
-
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
1121
|
-
lastError: error.message,
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
else {
|
|
1125
|
-
await docRef.set({
|
|
1126
|
-
...trackingData,
|
|
1127
|
-
count: 1,
|
|
1128
|
-
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
catch (trackingError) {
|
|
1133
|
-
console.warn('Falha ao rastrear índice ausente:', trackingError);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
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 });
|
|
1138
|
-
TableService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, providedIn: 'root' });
|
|
1139
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, decorators: [{
|
|
1140
|
-
type: Injectable,
|
|
1141
|
-
args: [{
|
|
1142
|
-
providedIn: 'root',
|
|
1143
|
-
}]
|
|
1144
|
-
}], ctorParameters: function () { return [{ type: i1.AngularFirestore, decorators: [{
|
|
1145
|
-
type: Optional
|
|
1146
|
-
}] }, { type: i2.MatDialog, decorators: [{
|
|
1147
|
-
type: Optional
|
|
1148
|
-
}] }, { type: i3.ToastrService, decorators: [{
|
|
1149
|
-
type: Optional
|
|
47
|
+
class TableService {
|
|
48
|
+
constructor(ngFire, dialog, toastr) {
|
|
49
|
+
this.ngFire = ngFire;
|
|
50
|
+
this.dialog = dialog;
|
|
51
|
+
this.toastr = toastr;
|
|
52
|
+
this.operators = {
|
|
53
|
+
'==': (a, b) => a === b,
|
|
54
|
+
'!=': (a, b) => a !== b,
|
|
55
|
+
'>': (a, b) => a > b,
|
|
56
|
+
'<': (a, b) => a < b,
|
|
57
|
+
'>=': (a, b) => a >= b,
|
|
58
|
+
'<=': (a, b) => a <= b,
|
|
59
|
+
in: (a, b) => Array.isArray(b) && b.includes(a),
|
|
60
|
+
'not-in': (a, b) => Array.isArray(b) && !b.includes(a),
|
|
61
|
+
'array-contains': (a, b) => Array.isArray(a) && a.includes(b),
|
|
62
|
+
'array-contains-any': (a, b) => Array.isArray(a) &&
|
|
63
|
+
Array.isArray(b) &&
|
|
64
|
+
b.some((item) => a.includes(item)),
|
|
65
|
+
includes: (a, b) => a.includes(b), // Para strings ou arrays
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async getItems(collection) {
|
|
69
|
+
try {
|
|
70
|
+
const querySnapshot = await collection.get();
|
|
71
|
+
return querySnapshot.docs.map((doc) => {
|
|
72
|
+
return { ...doc.data(), id: doc.id };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.warn('Collection não encontrada:', error);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async executeQuery(params) {
|
|
81
|
+
if (params.filterFn) {
|
|
82
|
+
// Lógica com filtro no cliente (filterFn)
|
|
83
|
+
const BATCH_FETCH_SIZE = params.batchSize;
|
|
84
|
+
const GOAL_SIZE = params.batchSize + 1;
|
|
85
|
+
if (params.navigation === 'forward' || params.navigation === 'reload') {
|
|
86
|
+
if (params.navigation === 'reload' && params.doc) {
|
|
87
|
+
params.doc.lastDoc = null;
|
|
88
|
+
}
|
|
89
|
+
let lastDocCursor = params.doc ? params.doc.lastDoc : null;
|
|
90
|
+
let pageResults = [];
|
|
91
|
+
let allFetchedDocs = [];
|
|
92
|
+
let hasMoreDocsInDb = true;
|
|
93
|
+
while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
|
|
94
|
+
let query = this.ngFire.collection(params.collection).ref;
|
|
95
|
+
query = this.applyFilters(query, params.arrange, params.conditions);
|
|
96
|
+
if (lastDocCursor) {
|
|
97
|
+
query = query.startAfter(lastDocCursor);
|
|
98
|
+
}
|
|
99
|
+
query = query.limit(BATCH_FETCH_SIZE);
|
|
100
|
+
const snapshot = await query.get();
|
|
101
|
+
if (snapshot.empty) {
|
|
102
|
+
hasMoreDocsInDb = false;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
lastDocCursor = snapshot.docs[snapshot.docs.length - 1];
|
|
106
|
+
allFetchedDocs.push(...snapshot.docs);
|
|
107
|
+
const batchUsers = snapshot.docs
|
|
108
|
+
.map((doc) => ({
|
|
109
|
+
id: doc.id,
|
|
110
|
+
...doc.data(),
|
|
111
|
+
}))
|
|
112
|
+
.filter(params.filterFn);
|
|
113
|
+
pageResults.push(...batchUsers);
|
|
114
|
+
if (snapshot.size < BATCH_FETCH_SIZE) {
|
|
115
|
+
hasMoreDocsInDb = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const hasNextPage = pageResults.length > params.batchSize;
|
|
119
|
+
const finalItems = pageResults.slice(0, params.batchSize);
|
|
120
|
+
const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
|
|
121
|
+
const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
|
|
122
|
+
return {
|
|
123
|
+
items: finalItems,
|
|
124
|
+
filterLength: null,
|
|
125
|
+
firstDoc: firstDocOfPage,
|
|
126
|
+
lastDoc: lastDocOfPage,
|
|
127
|
+
hasNextPage: hasNextPage,
|
|
128
|
+
hasPreviousPage: !!(params.doc && params.doc.lastDoc) &&
|
|
129
|
+
params.navigation !== 'reload',
|
|
130
|
+
currentClientPageIndex: undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Lógica para trás (backward)
|
|
134
|
+
else if (params.navigation === 'backward') {
|
|
135
|
+
if (!params.doc || !params.doc.firstDoc) {
|
|
136
|
+
return {
|
|
137
|
+
items: [],
|
|
138
|
+
filterLength: null,
|
|
139
|
+
firstDoc: null,
|
|
140
|
+
lastDoc: null,
|
|
141
|
+
hasNextPage: true,
|
|
142
|
+
hasPreviousPage: false,
|
|
143
|
+
currentClientPageIndex: undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
let pageResults = [];
|
|
147
|
+
let allFetchedDocs = [];
|
|
148
|
+
let hasMoreDocsInDb = true;
|
|
149
|
+
let boundaryDoc = params.doc.firstDoc;
|
|
150
|
+
while (pageResults.length < GOAL_SIZE && hasMoreDocsInDb) {
|
|
151
|
+
let query = this.ngFire.collection(params.collection).ref;
|
|
152
|
+
query = this.applyFilters(query, params.arrange, params.conditions);
|
|
153
|
+
query = query.endBefore(boundaryDoc);
|
|
154
|
+
query = query.limitToLast(BATCH_FETCH_SIZE);
|
|
155
|
+
const snapshot = await query.get();
|
|
156
|
+
if (snapshot.empty) {
|
|
157
|
+
hasMoreDocsInDb = false;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
boundaryDoc = snapshot.docs[0];
|
|
161
|
+
allFetchedDocs = [...snapshot.docs, ...allFetchedDocs];
|
|
162
|
+
const batchUsers = snapshot.docs
|
|
163
|
+
.map((doc) => ({
|
|
164
|
+
id: doc.id,
|
|
165
|
+
...doc.data(),
|
|
166
|
+
}))
|
|
167
|
+
.filter(params.filterFn);
|
|
168
|
+
pageResults = [...batchUsers, ...pageResults];
|
|
169
|
+
if (snapshot.size < BATCH_FETCH_SIZE) {
|
|
170
|
+
hasMoreDocsInDb = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const finalItems = pageResults.slice(0, params.batchSize);
|
|
174
|
+
const firstDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[0]?.id) || null;
|
|
175
|
+
const lastDocOfPage = allFetchedDocs.find((doc) => doc.id === finalItems[finalItems.length - 1]?.id) || null;
|
|
176
|
+
return {
|
|
177
|
+
items: finalItems,
|
|
178
|
+
filterLength: null,
|
|
179
|
+
firstDoc: firstDocOfPage,
|
|
180
|
+
lastDoc: lastDocOfPage,
|
|
181
|
+
hasNextPage: true,
|
|
182
|
+
currentClientPageIndex: undefined,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
let items = [];
|
|
188
|
+
let docs = [];
|
|
189
|
+
let hasNextPage = false;
|
|
190
|
+
let filterLength = null;
|
|
191
|
+
let query = this.ngFire.collection(params.collection).ref;
|
|
192
|
+
if (params.conditions) {
|
|
193
|
+
params.conditions.forEach((c) => {
|
|
194
|
+
if (c.operator === '!=') {
|
|
195
|
+
query = query.orderBy(c.firestoreProperty);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
query = this.applyFilters(query, params.arrange, params.conditions);
|
|
200
|
+
if (params.navigation === 'reload') {
|
|
201
|
+
query = query.limit(params.batchSize + 1);
|
|
202
|
+
if (params.doc && params.doc.firstDoc) {
|
|
203
|
+
query = query.startAt(params.doc.firstDoc);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (params.navigation === 'forward') {
|
|
207
|
+
query = query.limit(params.batchSize + 1);
|
|
208
|
+
if (params.doc && params.doc.lastDoc) {
|
|
209
|
+
query = query.startAfter(params.doc.lastDoc);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// backward
|
|
214
|
+
query = query.limitToLast(params.batchSize + 1);
|
|
215
|
+
if (params.doc && params.doc.firstDoc) {
|
|
216
|
+
query = query.endBefore(params.doc.firstDoc);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const itemCol = await query.get();
|
|
220
|
+
itemCol.docs.forEach((doc) => docs.push(doc));
|
|
221
|
+
const itemPromises = docs.map(async (item) => {
|
|
222
|
+
const itemData = item.data();
|
|
223
|
+
items.push({ id: item.id, ...itemData });
|
|
224
|
+
});
|
|
225
|
+
let lastDoc = docs[docs.length - 1] || null;
|
|
226
|
+
let firstDoc = docs[0];
|
|
227
|
+
if ((items.length > params.batchSize && params.navigation === 'forward') ||
|
|
228
|
+
(params.navigation === 'reload' && items.length > params.batchSize)) {
|
|
229
|
+
lastDoc = docs[docs.length - 2] || null;
|
|
230
|
+
items.pop();
|
|
231
|
+
hasNextPage = true;
|
|
232
|
+
}
|
|
233
|
+
if (items.length > params.batchSize && params.navigation === 'backward') {
|
|
234
|
+
firstDoc = docs[1];
|
|
235
|
+
items.shift();
|
|
236
|
+
hasNextPage = true;
|
|
237
|
+
}
|
|
238
|
+
await Promise.all(itemPromises);
|
|
239
|
+
return {
|
|
240
|
+
items,
|
|
241
|
+
filterLength,
|
|
242
|
+
lastDoc,
|
|
243
|
+
firstDoc,
|
|
244
|
+
hasNextPage,
|
|
245
|
+
currentClientPageIndex: undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// Fallback para garantir que sempre retornamos algo
|
|
249
|
+
return {
|
|
250
|
+
items: [],
|
|
251
|
+
filterLength: null,
|
|
252
|
+
firstDoc: null,
|
|
253
|
+
lastDoc: null,
|
|
254
|
+
hasNextPage: false,
|
|
255
|
+
currentClientPageIndex: undefined,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
applyFilters(query, arrange, conditions) {
|
|
259
|
+
if (conditions) {
|
|
260
|
+
conditions.map((cond) => {
|
|
261
|
+
query = query.where(cond.firestoreProperty, cond.operator, cond.dashProperty);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
let hasFilterSpecificOrderBy = false;
|
|
265
|
+
let appliedOrderByField = null;
|
|
266
|
+
const equalsFilters = arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
267
|
+
const otherFilters = arrange.filters.filter((f) => f.arrange !== 'equals');
|
|
268
|
+
const equalsGroupedByProperty = equalsFilters.reduce((acc, current) => {
|
|
269
|
+
const prop = current.filter.property;
|
|
270
|
+
if (!acc[prop]) {
|
|
271
|
+
acc[prop] = [];
|
|
272
|
+
}
|
|
273
|
+
acc[prop].push(current.filter.filtering);
|
|
274
|
+
return acc;
|
|
275
|
+
}, {});
|
|
276
|
+
for (const prop in equalsGroupedByProperty) {
|
|
277
|
+
const values = equalsGroupedByProperty[prop];
|
|
278
|
+
if (values.length > 0) {
|
|
279
|
+
query = query.where(prop, 'in', values);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
otherFilters.forEach((filterItem) => {
|
|
283
|
+
// Aplicar filtragem por busca
|
|
284
|
+
if (filterItem.filter?.filtering &&
|
|
285
|
+
filterItem.filter?.property !== '' &&
|
|
286
|
+
filterItem.arrange === 'filter') {
|
|
287
|
+
query = query
|
|
288
|
+
.where(filterItem.filter.property, '>=', filterItem.filter.filtering.trim().toUpperCase())
|
|
289
|
+
.where(filterItem.filter.property, '<=', filterItem.filter.filtering.trim().toUpperCase() + '\uf8ff');
|
|
290
|
+
if (!hasFilterSpecificOrderBy) {
|
|
291
|
+
query = query.orderBy(filterItem.filter.property);
|
|
292
|
+
hasFilterSpecificOrderBy = true;
|
|
293
|
+
appliedOrderByField = filterItem.filter.property;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Aplicar filtro do tipo "filterByDate"
|
|
297
|
+
if (filterItem.dateFilter && filterItem.arrange === 'filterByDate') {
|
|
298
|
+
query = query
|
|
299
|
+
.where(arrange.sortBy.field, '>=', filterItem.dateFilter.initial)
|
|
300
|
+
.where(arrange.sortBy.field, '<=', filterItem.dateFilter.final);
|
|
301
|
+
if (!hasFilterSpecificOrderBy) {
|
|
302
|
+
query = query.orderBy(arrange.sortBy.field);
|
|
303
|
+
hasFilterSpecificOrderBy = true;
|
|
304
|
+
appliedOrderByField = arrange.sortBy.field;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
// Aplicar sortBy
|
|
309
|
+
if (arrange.sortBy && arrange.sortBy.field && arrange.sortBy.order) {
|
|
310
|
+
if (appliedOrderByField !== arrange.sortBy.field) {
|
|
311
|
+
query = query.orderBy(arrange.sortBy.field, arrange.sortBy.order);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return query;
|
|
315
|
+
}
|
|
316
|
+
getIdFilter(params) {
|
|
317
|
+
if (!params.arrange?.filters)
|
|
318
|
+
return null;
|
|
319
|
+
const idFilter = params.arrange.filters.find((f) => f.arrange === 'filter' &&
|
|
320
|
+
f.filter?.property === 'id' &&
|
|
321
|
+
f.filter?.filtering);
|
|
322
|
+
return idFilter?.filter?.filtering?.trim() || null;
|
|
323
|
+
}
|
|
324
|
+
async getDocumentById(collection, docId) {
|
|
325
|
+
try {
|
|
326
|
+
const docRef = this.ngFire.collection(collection).doc(docId);
|
|
327
|
+
const docSnapshot = await docRef.get().toPromise();
|
|
328
|
+
if (docSnapshot && docSnapshot.exists) {
|
|
329
|
+
return {
|
|
330
|
+
id: docSnapshot.id,
|
|
331
|
+
...docSnapshot.data(),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
console.warn('Erro ao buscar documento por ID:', error);
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async searchByIdPartial(params, searchTerm) {
|
|
342
|
+
const exactMatch = await this.getDocumentById(params.collection, searchTerm);
|
|
343
|
+
if (exactMatch) {
|
|
344
|
+
if (params.conditions) {
|
|
345
|
+
const operators = this.operators;
|
|
346
|
+
const passesConditions = params.conditions.every((cond) => {
|
|
347
|
+
const operatorFn = operators[cond.operator];
|
|
348
|
+
return operatorFn
|
|
349
|
+
? operatorFn(exactMatch[cond.firestoreProperty], cond.dashProperty)
|
|
350
|
+
: false;
|
|
351
|
+
});
|
|
352
|
+
if (!passesConditions) {
|
|
353
|
+
return {
|
|
354
|
+
items: [],
|
|
355
|
+
filterLength: 0,
|
|
356
|
+
firstDoc: null,
|
|
357
|
+
lastDoc: null,
|
|
358
|
+
hasNextPage: false,
|
|
359
|
+
hasPreviousPage: false,
|
|
360
|
+
currentClientPageIndex: 0,
|
|
361
|
+
totalPages: 0,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (params.filterFn && !params.filterFn(exactMatch)) {
|
|
366
|
+
return {
|
|
367
|
+
items: [],
|
|
368
|
+
filterLength: 0,
|
|
369
|
+
firstDoc: null,
|
|
370
|
+
lastDoc: null,
|
|
371
|
+
hasNextPage: false,
|
|
372
|
+
hasPreviousPage: false,
|
|
373
|
+
currentClientPageIndex: 0,
|
|
374
|
+
totalPages: 0,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
items: [exactMatch],
|
|
379
|
+
filterLength: 1,
|
|
380
|
+
firstDoc: null,
|
|
381
|
+
lastDoc: null,
|
|
382
|
+
hasNextPage: false,
|
|
383
|
+
hasPreviousPage: false,
|
|
384
|
+
currentClientPageIndex: 0,
|
|
385
|
+
totalPages: 1,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const searchTermLower = searchTerm.toLowerCase();
|
|
389
|
+
const paramsWithoutIdFilter = {
|
|
390
|
+
...params,
|
|
391
|
+
arrange: {
|
|
392
|
+
...params.arrange,
|
|
393
|
+
filters: params.arrange.filters.filter((f) => !(f.arrange === 'filter' && f.filter?.property === 'id')),
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
let query = this.ngFire.collection(params.collection).ref;
|
|
397
|
+
// Aplicar conditions
|
|
398
|
+
if (params.conditions) {
|
|
399
|
+
params.conditions.forEach((cond) => {
|
|
400
|
+
query = query.where(cond.firestoreProperty, cond.operator, cond.dashProperty);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// Aplicar sortBy
|
|
404
|
+
if (params.arrange?.sortBy?.field && params.arrange?.sortBy?.order) {
|
|
405
|
+
query = query.orderBy(params.arrange.sortBy.field, params.arrange.sortBy.order);
|
|
406
|
+
}
|
|
407
|
+
const snapshot = await query.get();
|
|
408
|
+
let items = snapshot.docs
|
|
409
|
+
.map((doc) => ({
|
|
410
|
+
id: doc.id,
|
|
411
|
+
...doc.data(),
|
|
412
|
+
}))
|
|
413
|
+
.filter((item) => item.id.toLowerCase().includes(searchTermLower));
|
|
414
|
+
// Separar equals filters e outros filtros
|
|
415
|
+
const equalsFilters = paramsWithoutIdFilter.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
416
|
+
const otherFilters = paramsWithoutIdFilter.arrange.filters.filter((f) => f.arrange !== 'equals' &&
|
|
417
|
+
(f.arrange !== 'filter' || f.filter?.property !== 'id'));
|
|
418
|
+
// Aplicar equals filters com lógica OR dentro de cada propriedade
|
|
419
|
+
if (equalsFilters.length > 0) {
|
|
420
|
+
// Agrupar por propriedade para aplicar OR
|
|
421
|
+
const groupedByProperty = equalsFilters.reduce((acc, f) => {
|
|
422
|
+
const prop = f.filter.property;
|
|
423
|
+
if (!acc[prop]) {
|
|
424
|
+
acc[prop] = [];
|
|
425
|
+
}
|
|
426
|
+
acc[prop].push(f.filter.filtering);
|
|
427
|
+
return acc;
|
|
428
|
+
}, {});
|
|
429
|
+
// Filtrar: item deve ter pelo menos um valor de CADA propriedade
|
|
430
|
+
items = items.filter((item) => {
|
|
431
|
+
return Object.entries(groupedByProperty).every(([prop, values]) => {
|
|
432
|
+
return values.includes(item[prop]);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// Aplicar outros filtros
|
|
437
|
+
otherFilters.forEach((filterItem) => {
|
|
438
|
+
if (filterItem.arrange === 'filter' &&
|
|
439
|
+
filterItem.filter?.filtering &&
|
|
440
|
+
filterItem.filter?.property) {
|
|
441
|
+
const filterValue = String(filterItem.filter.filtering)
|
|
442
|
+
.trim()
|
|
443
|
+
.toLowerCase();
|
|
444
|
+
items = items.filter((item) => {
|
|
445
|
+
const itemValue = String(item[filterItem.filter.property]).toLowerCase();
|
|
446
|
+
return itemValue.includes(filterValue);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
// Aplicar filterFn se existir
|
|
451
|
+
if (params.filterFn) {
|
|
452
|
+
items = items.filter(params.filterFn);
|
|
453
|
+
}
|
|
454
|
+
// Paginação
|
|
455
|
+
const pageSize = params.batchSize;
|
|
456
|
+
let currentClientPageIndex = 0;
|
|
457
|
+
if (params.navigation === 'reload') {
|
|
458
|
+
currentClientPageIndex = 0;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// Usar o índice passado pelo componente sem incrementar/decrementar
|
|
462
|
+
currentClientPageIndex = params.clientPageIndex || 0;
|
|
463
|
+
}
|
|
464
|
+
const startIndex = currentClientPageIndex * pageSize;
|
|
465
|
+
const endIndex = startIndex + pageSize;
|
|
466
|
+
const paginatedItems = items.slice(startIndex, endIndex);
|
|
467
|
+
const totalPages = Math.ceil(items.length / pageSize);
|
|
468
|
+
const hasNextPage = currentClientPageIndex < totalPages - 1;
|
|
469
|
+
const hasPreviousPage = currentClientPageIndex > 0;
|
|
470
|
+
return {
|
|
471
|
+
items: paginatedItems,
|
|
472
|
+
filterLength: items.length,
|
|
473
|
+
firstDoc: null,
|
|
474
|
+
lastDoc: null,
|
|
475
|
+
hasNextPage,
|
|
476
|
+
hasPreviousPage,
|
|
477
|
+
currentClientPageIndex,
|
|
478
|
+
totalPages,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
shouldUseClientSideFallback(params) {
|
|
482
|
+
const hasConditions = params.conditions && params.conditions.length > 0;
|
|
483
|
+
const hasArrangeFilters = params.arrange?.filters && params.arrange.filters.length > 0;
|
|
484
|
+
const hasSortBy = params.arrange?.sortBy?.field;
|
|
485
|
+
if (params.filterFn) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
if (hasArrangeFilters) {
|
|
489
|
+
const equalsFilters = params.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
490
|
+
if (equalsFilters.length > 0) {
|
|
491
|
+
const propertiesSet = new Set(equalsFilters.map((f) => f.filter.property));
|
|
492
|
+
if (propertiesSet.size > 1) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (hasConditions && hasArrangeFilters && hasSortBy) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
if (hasConditions && hasArrangeFilters) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (hasArrangeFilters && params.arrange.filters.length > 1 && hasSortBy) {
|
|
504
|
+
const equalsFilters = params.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
505
|
+
if (equalsFilters.length > 0) {
|
|
506
|
+
const propertiesSet = new Set(equalsFilters.map((f) => f.filter.property));
|
|
507
|
+
if (propertiesSet.size === 1 &&
|
|
508
|
+
equalsFilters.length === params.arrange.filters.length) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
async getPaginated(params) {
|
|
517
|
+
const idFilterValue = this.getIdFilter(params);
|
|
518
|
+
if (idFilterValue) {
|
|
519
|
+
const result = await this.searchByIdPartial(params, idFilterValue);
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
// Detectar preventivamente se deve usar fallback
|
|
523
|
+
if (this.shouldUseClientSideFallback(params)) {
|
|
524
|
+
await this.trackMissingIndexPreventive(params.collection, params.arrange, params.conditions);
|
|
525
|
+
const result = await this.executeClientSideQuery(params);
|
|
526
|
+
console.log('📊 [TABLE] Resultados paginados via fallback client-side:', {
|
|
527
|
+
totalItems: result.filterLength,
|
|
528
|
+
returnedItems: result.items.length,
|
|
529
|
+
hasNextPage: result.hasNextPage,
|
|
530
|
+
currentPage: (result.currentClientPageIndex || 0) + 1,
|
|
531
|
+
});
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const result = await this.executeQuery(params);
|
|
536
|
+
console.log('📊 [TABLE] Resultados paginados via Firestore:', {
|
|
537
|
+
totalItems: result.filterLength || 'N/A',
|
|
538
|
+
returnedItems: result.items.length,
|
|
539
|
+
hasNextPage: result.hasNextPage,
|
|
540
|
+
});
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
if (error && error.code === 'failed-precondition') {
|
|
545
|
+
await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
|
|
546
|
+
const result = await this.executeClientSideQuery(params);
|
|
547
|
+
console.log('📊 [TABLE] Resultados paginados via fallback (erro de index):', {
|
|
548
|
+
totalItems: result.filterLength,
|
|
549
|
+
returnedItems: result.items.length,
|
|
550
|
+
hasNextPage: result.hasNextPage,
|
|
551
|
+
currentPage: (result.currentClientPageIndex || 0) + 1,
|
|
552
|
+
});
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
else if (error && error.code === 'invalid-argument') {
|
|
556
|
+
await this.trackMissingIndex(error, params.collection, params.arrange, params.conditions);
|
|
557
|
+
const result = await this.executeClientSideQuery(params);
|
|
558
|
+
console.log('📊 [TABLE] Resultados paginados via fallback (argumento inválido):', {
|
|
559
|
+
totalItems: result.filterLength,
|
|
560
|
+
returnedItems: result.items.length,
|
|
561
|
+
hasNextPage: result.hasNextPage,
|
|
562
|
+
currentPage: (result.currentClientPageIndex || 0) + 1,
|
|
563
|
+
});
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async executeClientSideQuery(params) {
|
|
572
|
+
// Otimizar usando pelo menos uma cláusula .where() quando possível
|
|
573
|
+
let query = this.ngFire.collection(params.collection).ref;
|
|
574
|
+
let appliedCondition = null;
|
|
575
|
+
let hasAppliedWhereClause = false;
|
|
576
|
+
// Primeiro, tenta aplicar condições simples
|
|
577
|
+
if (params.conditions && params.conditions.length > 0) {
|
|
578
|
+
const simpleCondition = params.conditions.find((cond) => ['==', '>', '<', '>=', '<=', 'in', 'array-contains'].includes(cond.operator));
|
|
579
|
+
if (simpleCondition) {
|
|
580
|
+
query = query.where(simpleCondition.firestoreProperty, simpleCondition.operator, simpleCondition.dashProperty);
|
|
581
|
+
appliedCondition = simpleCondition;
|
|
582
|
+
hasAppliedWhereClause = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Se não há condições disponíveis, tenta aplicar filtros do arrange
|
|
586
|
+
let appliedFirestoreFilter = null;
|
|
587
|
+
if (!hasAppliedWhereClause && params.arrange?.filters) {
|
|
588
|
+
// Agrupar equals filters por propriedade
|
|
589
|
+
const equalsFilters = params.arrange.filters.filter((f) => f.arrange === 'equals' && f.filter);
|
|
590
|
+
const equalsGroupedByProperty = equalsFilters.reduce((acc, current) => {
|
|
591
|
+
const prop = current.filter.property;
|
|
592
|
+
if (!acc[prop]) {
|
|
593
|
+
acc[prop] = [];
|
|
594
|
+
}
|
|
595
|
+
acc[prop].push(current.filter.filtering);
|
|
596
|
+
return acc;
|
|
597
|
+
}, {});
|
|
598
|
+
// Se há apenas UMA propriedade com múltiplos valores, aplicar no Firestore com 'in'
|
|
599
|
+
const properties = Object.keys(equalsGroupedByProperty);
|
|
600
|
+
if (properties.length === 1 && equalsFilters.length > 0) {
|
|
601
|
+
const prop = properties[0];
|
|
602
|
+
const values = equalsGroupedByProperty[prop];
|
|
603
|
+
query = query.where(prop, 'in', values);
|
|
604
|
+
hasAppliedWhereClause = true;
|
|
605
|
+
// Marcar TODOS os equals filters dessa propriedade como aplicados
|
|
606
|
+
appliedFirestoreFilter = 'all-equals';
|
|
607
|
+
}
|
|
608
|
+
else if (properties.length === 0) {
|
|
609
|
+
const otherFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
|
|
610
|
+
f.filter?.filtering &&
|
|
611
|
+
f.filter?.property) ||
|
|
612
|
+
(f.arrange === 'filterByDate' &&
|
|
613
|
+
f.dateFilter?.initial &&
|
|
614
|
+
f.dateFilter?.final));
|
|
615
|
+
if (otherFilter) {
|
|
616
|
+
if (otherFilter.arrange === 'filter' && otherFilter.filter) {
|
|
617
|
+
const filterValue = otherFilter.filter.filtering
|
|
618
|
+
.trim()
|
|
619
|
+
.toUpperCase();
|
|
620
|
+
query = query
|
|
621
|
+
.where(otherFilter.filter.property, '>=', filterValue)
|
|
622
|
+
.where(otherFilter.filter.property, '<=', filterValue + '\uf8ff');
|
|
623
|
+
hasAppliedWhereClause = true;
|
|
624
|
+
appliedFirestoreFilter = otherFilter;
|
|
625
|
+
}
|
|
626
|
+
else if (otherFilter.arrange === 'filterByDate' &&
|
|
627
|
+
otherFilter.dateFilter &&
|
|
628
|
+
params.arrange.sortBy?.field) {
|
|
629
|
+
query = query
|
|
630
|
+
.where(params.arrange.sortBy.field, '>=', otherFilter.dateFilter.initial)
|
|
631
|
+
.where(params.arrange.sortBy.field, '<=', otherFilter.dateFilter.final);
|
|
632
|
+
hasAppliedWhereClause = true;
|
|
633
|
+
appliedFirestoreFilter = otherFilter;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const allDocsSnapshot = await query.get();
|
|
639
|
+
let items = allDocsSnapshot.docs.map((doc) => ({
|
|
640
|
+
id: doc.id,
|
|
641
|
+
...doc.data(),
|
|
642
|
+
}));
|
|
643
|
+
// Aplicar condições restantes
|
|
644
|
+
if (params.conditions) {
|
|
645
|
+
const remainingConditions = params.conditions.filter((cond) => cond !== appliedCondition);
|
|
646
|
+
if (remainingConditions.length > 0) {
|
|
647
|
+
const operators = this.operators;
|
|
648
|
+
items = items.filter((item) => {
|
|
649
|
+
return remainingConditions.every((cond) => {
|
|
650
|
+
const operatorFn = operators[cond.operator];
|
|
651
|
+
return operatorFn
|
|
652
|
+
? operatorFn(item[cond.firestoreProperty], cond.dashProperty)
|
|
653
|
+
: false;
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const { filters, sortBy } = params.arrange;
|
|
659
|
+
// Track which filter was already applied in Firestore to avoid double filtering
|
|
660
|
+
if (hasAppliedWhereClause && !appliedCondition && params.arrange?.filters) {
|
|
661
|
+
const equalsFilter = params.arrange.filters.find((f) => f.arrange === 'equals' && f.filter?.filtering);
|
|
662
|
+
if (equalsFilter) {
|
|
663
|
+
appliedFirestoreFilter = equalsFilter;
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
appliedFirestoreFilter = params.arrange.filters.find((f) => (f.arrange === 'filter' &&
|
|
667
|
+
f.filter?.filtering &&
|
|
668
|
+
f.filter?.property) ||
|
|
669
|
+
(f.arrange === 'filterByDate' &&
|
|
670
|
+
f.dateFilter?.initial &&
|
|
671
|
+
f.dateFilter?.final));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const equalsFilters = filters.filter((f) => f.arrange === 'equals');
|
|
675
|
+
const otherFilters = filters.filter((f) => f.arrange !== 'equals');
|
|
676
|
+
// Aplicar equals filters no client-side apenas se não foram aplicados no Firestore
|
|
677
|
+
if (appliedFirestoreFilter !== 'all-equals' && equalsFilters.length > 0) {
|
|
678
|
+
// Agrupar por propriedade para aplicar OR dentro de cada propriedade
|
|
679
|
+
const groupedByProperty = equalsFilters.reduce((acc, f) => {
|
|
680
|
+
const prop = f.filter.property;
|
|
681
|
+
if (!acc[prop]) {
|
|
682
|
+
acc[prop] = [];
|
|
683
|
+
}
|
|
684
|
+
acc[prop].push(f.filter.filtering);
|
|
685
|
+
return acc;
|
|
686
|
+
}, {});
|
|
687
|
+
// Filtrar: item deve ter pelo menos um valor de CADA propriedade
|
|
688
|
+
// (AND entre propriedades, OR dentro de cada propriedade)
|
|
689
|
+
items = items.filter((item) => {
|
|
690
|
+
return Object.entries(groupedByProperty).every(([prop, values]) => {
|
|
691
|
+
return values.includes(item[prop]);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
otherFilters.forEach((filterItem) => {
|
|
696
|
+
if (appliedFirestoreFilter === filterItem) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (filterItem.arrange === 'filter' &&
|
|
700
|
+
filterItem.filter?.filtering &&
|
|
701
|
+
filterItem.filter?.property) {
|
|
702
|
+
const filterValue = String(filterItem.filter.filtering)
|
|
703
|
+
.trim()
|
|
704
|
+
.toLowerCase();
|
|
705
|
+
items = items.filter((item) => {
|
|
706
|
+
const itemValue = String(item[filterItem.filter.property]).toLowerCase();
|
|
707
|
+
return itemValue.includes(filterValue);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (filterItem.arrange === 'filterByDate' &&
|
|
711
|
+
filterItem.dateFilter?.initial &&
|
|
712
|
+
filterItem.dateFilter?.final &&
|
|
713
|
+
sortBy.field) {
|
|
714
|
+
items = items.filter((item) => {
|
|
715
|
+
try {
|
|
716
|
+
const fieldValue = item[sortBy.field];
|
|
717
|
+
if (!fieldValue) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
let itemDate;
|
|
721
|
+
if (typeof fieldValue.toDate === 'function') {
|
|
722
|
+
itemDate = fieldValue.toDate();
|
|
723
|
+
}
|
|
724
|
+
else if (fieldValue instanceof Date) {
|
|
725
|
+
itemDate = fieldValue;
|
|
726
|
+
}
|
|
727
|
+
else if (typeof fieldValue === 'string') {
|
|
728
|
+
itemDate = new Date(fieldValue);
|
|
729
|
+
if (isNaN(itemDate.getTime())) {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else if (typeof fieldValue === 'number') {
|
|
734
|
+
itemDate = new Date(fieldValue);
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
return (itemDate >= filterItem.dateFilter.initial &&
|
|
740
|
+
itemDate <= filterItem.dateFilter.final);
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
console.warn('Erro ao processar filtro de data para o item:', item.id, error);
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
// Aplicar filterFn se existir
|
|
750
|
+
if (params.filterFn) {
|
|
751
|
+
items = items.filter(params.filterFn);
|
|
752
|
+
}
|
|
753
|
+
if (sortBy && sortBy.field && sortBy.order) {
|
|
754
|
+
items.sort((a, b) => {
|
|
755
|
+
const valA = a[sortBy.field];
|
|
756
|
+
const valB = b[sortBy.field];
|
|
757
|
+
if (valA < valB) {
|
|
758
|
+
return sortBy.order === 'asc' ? -1 : 1;
|
|
759
|
+
}
|
|
760
|
+
if (valA > valB) {
|
|
761
|
+
return sortBy.order === 'asc' ? 1 : -1;
|
|
762
|
+
}
|
|
763
|
+
return 0;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
// Implementação adequada da paginação
|
|
767
|
+
let currentClientPageIndex = 0;
|
|
768
|
+
// Determinar a página atual baseada na navegação
|
|
769
|
+
if (params.navigation === 'reload') {
|
|
770
|
+
currentClientPageIndex = 0;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
currentClientPageIndex = params.clientPageIndex || 0;
|
|
774
|
+
}
|
|
775
|
+
const pageSize = params.batchSize;
|
|
776
|
+
const startIndex = currentClientPageIndex * pageSize;
|
|
777
|
+
const endIndex = startIndex + pageSize;
|
|
778
|
+
const paginatedItems = items.slice(startIndex, endIndex);
|
|
779
|
+
const totalPages = Math.ceil(items.length / pageSize);
|
|
780
|
+
const hasNextPage = currentClientPageIndex < totalPages - 1;
|
|
781
|
+
const hasPreviousPage = currentClientPageIndex > 0;
|
|
782
|
+
return {
|
|
783
|
+
items: paginatedItems,
|
|
784
|
+
filterLength: items.length,
|
|
785
|
+
lastDoc: null,
|
|
786
|
+
firstDoc: null,
|
|
787
|
+
hasNextPage: hasNextPage,
|
|
788
|
+
hasPreviousPage: hasPreviousPage,
|
|
789
|
+
currentClientPageIndex: currentClientPageIndex,
|
|
790
|
+
totalPages: totalPages,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
async getItemsData(collection, arrange, conditions = undefined) {
|
|
794
|
+
try {
|
|
795
|
+
let query = this.ngFire.collection(collection).ref;
|
|
796
|
+
query = this.applyFilters(query, arrange, conditions);
|
|
797
|
+
const snapshot = await query.get();
|
|
798
|
+
return await Promise.all(snapshot.docs.map(async (doc) => {
|
|
799
|
+
const data = doc.data();
|
|
800
|
+
const id = doc.id;
|
|
801
|
+
return {
|
|
802
|
+
id,
|
|
803
|
+
...data,
|
|
804
|
+
};
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
throw e;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async deleteIndex(id, col) {
|
|
812
|
+
try {
|
|
813
|
+
const batch = this.ngFire.firestore.batch();
|
|
814
|
+
const docRef = this.ngFire.collection(col).doc(id);
|
|
815
|
+
const docSnapshot = (await firstValueFrom(docRef.get()));
|
|
816
|
+
const doc = docSnapshot.data();
|
|
817
|
+
batch.delete(docRef.ref);
|
|
818
|
+
if (doc && typeof doc.index === 'number') {
|
|
819
|
+
await this.reindex(doc.index, col, batch);
|
|
820
|
+
}
|
|
821
|
+
await batch.commit();
|
|
822
|
+
this.toastr.success('Item excluído com sucesso!');
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
catch (e) {
|
|
826
|
+
const error = e;
|
|
827
|
+
console.error('Erro ao deletar item:', error);
|
|
828
|
+
this.toastr.error('Erro ao deletar item.');
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async reindex(index, col, batch) {
|
|
833
|
+
try {
|
|
834
|
+
const snapshot = (await firstValueFrom(this.ngFire.collection(col).get()));
|
|
835
|
+
const docs = snapshot.docs;
|
|
836
|
+
for (let doc of docs) {
|
|
837
|
+
const data = doc.data();
|
|
838
|
+
if (data && typeof data.index === 'number' && data.index > index) {
|
|
839
|
+
data.index--;
|
|
840
|
+
const docRef = this.ngFire.collection(col).doc(doc.id).ref;
|
|
841
|
+
batch.update(docRef, data);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
console.error('Erro ao reindexar:', error);
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
dateFormatValidator() {
|
|
851
|
+
return (control) => {
|
|
852
|
+
if (!control.value) {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
const dateStr = control.value.trim();
|
|
856
|
+
const datePattern = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
|
|
857
|
+
if (!datePattern.test(dateStr)) {
|
|
858
|
+
return { invalidFormat: true };
|
|
859
|
+
}
|
|
860
|
+
const parts = dateStr.split('/');
|
|
861
|
+
const day = parts[0].padStart(2, '0');
|
|
862
|
+
const month = parts[1].padStart(2, '0');
|
|
863
|
+
const year = parts[2];
|
|
864
|
+
const normalizedDate = `${day}/${month}/${year}`;
|
|
865
|
+
const date = moment(normalizedDate, 'DD/MM/YYYY', true);
|
|
866
|
+
if (!date.isValid()) {
|
|
867
|
+
return { invalidDate: true };
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
async updateIndex(index, id, col) {
|
|
873
|
+
await this.ngFire.collection(col).doc(id).update({ index });
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Extrai o link de criação de índice da mensagem de erro do Firestore
|
|
877
|
+
*/
|
|
878
|
+
extractIndexLink(error) {
|
|
879
|
+
if (!error || !error.message)
|
|
880
|
+
return null;
|
|
881
|
+
const linkMatch = error.message.match(/(https:\/\/console\.firebase\.google\.com\/[^\s]+)/);
|
|
882
|
+
return linkMatch ? linkMatch[1] : null;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Rastreia índices ausentes ao usar fallback preventivo
|
|
886
|
+
*/
|
|
887
|
+
async trackMissingIndexPreventive(collection, arrange, conditions = undefined) {
|
|
888
|
+
try {
|
|
889
|
+
const querySignature = this.generateQuerySignature(collection, arrange, conditions);
|
|
890
|
+
const docId = `${collection}_${querySignature}`;
|
|
891
|
+
const indexLink = this.generateIndexLink(collection, arrange, conditions);
|
|
892
|
+
const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
|
|
893
|
+
const trackingData = {
|
|
894
|
+
collection,
|
|
895
|
+
indexLink,
|
|
896
|
+
indexInstructions,
|
|
897
|
+
arrange: {
|
|
898
|
+
sortBy: arrange.sortBy,
|
|
899
|
+
filters: arrange.filters?.map((f) => ({
|
|
900
|
+
arrange: f.arrange,
|
|
901
|
+
property: f.filter?.property || null,
|
|
902
|
+
dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
|
|
903
|
+
})) || [],
|
|
904
|
+
},
|
|
905
|
+
conditions: conditions?.map((c) => ({
|
|
906
|
+
property: c.firestoreProperty,
|
|
907
|
+
operator: c.operator,
|
|
908
|
+
})) || [],
|
|
909
|
+
errorMessage: `Fallback preventivo usado para a collection ${collection}. A query exigiria índice composto.`,
|
|
910
|
+
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
911
|
+
};
|
|
912
|
+
console.log('📄 [INDEX LINK] Dados que serão salvos no documento:', {
|
|
913
|
+
docId,
|
|
914
|
+
collection: trackingData.collection,
|
|
915
|
+
indexLink: trackingData.indexLink,
|
|
916
|
+
arrange: trackingData.arrange,
|
|
917
|
+
conditions: trackingData.conditions,
|
|
918
|
+
errorMessage: trackingData.errorMessage,
|
|
919
|
+
});
|
|
920
|
+
const docRef = this.ngFire.collection('missingIndexes').doc(docId);
|
|
921
|
+
const doc = await docRef.get().toPromise();
|
|
922
|
+
if (doc && doc.exists) {
|
|
923
|
+
await docRef.update({
|
|
924
|
+
count: firebase.firestore.FieldValue.increment(1),
|
|
925
|
+
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
926
|
+
lastError: trackingData.errorMessage,
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
await docRef.set({
|
|
931
|
+
...trackingData,
|
|
932
|
+
count: 1,
|
|
933
|
+
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
catch (trackingError) {
|
|
938
|
+
console.warn('Falha ao rastrear fallback preventivo:', trackingError);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Gera uma assinatura única para uma query
|
|
943
|
+
*/
|
|
944
|
+
generateQuerySignature(collection, arrange, conditions = undefined) {
|
|
945
|
+
const signature = {
|
|
946
|
+
collection,
|
|
947
|
+
sortBy: arrange.sortBy,
|
|
948
|
+
filters: arrange.filters?.map((f) => ({
|
|
949
|
+
arrange: f.arrange,
|
|
950
|
+
property: f.filter?.property || null,
|
|
951
|
+
})) || [],
|
|
952
|
+
conditions: conditions?.map((c) => ({
|
|
953
|
+
property: c.firestoreProperty,
|
|
954
|
+
operator: c.operator,
|
|
955
|
+
})) || [],
|
|
956
|
+
};
|
|
957
|
+
return btoa(JSON.stringify(signature))
|
|
958
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
959
|
+
.substring(0, 20);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Gera instruções claras para criar o índice manualmente
|
|
963
|
+
*/
|
|
964
|
+
generateIndexInstructions(collection, arrange, conditions = undefined) {
|
|
965
|
+
const instructions = {
|
|
966
|
+
summary: '',
|
|
967
|
+
collection: collection,
|
|
968
|
+
fields: [],
|
|
969
|
+
queryExample: '',
|
|
970
|
+
stepByStep: [],
|
|
971
|
+
notes: [],
|
|
972
|
+
};
|
|
973
|
+
const fields = [];
|
|
974
|
+
if (conditions && conditions.length > 0) {
|
|
975
|
+
conditions.forEach((condition) => {
|
|
976
|
+
if (condition.firestoreProperty) {
|
|
977
|
+
fields.push({
|
|
978
|
+
field: condition.firestoreProperty,
|
|
979
|
+
order: 'Ascending',
|
|
980
|
+
type: 'WHERE clause',
|
|
981
|
+
operator: condition.operator,
|
|
982
|
+
description: `Filtrar por ${condition.firestoreProperty} usando operador ${condition.operator}`,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
if (arrange.filters && arrange.filters.length > 0) {
|
|
988
|
+
arrange.filters.forEach((filter) => {
|
|
989
|
+
if (filter.filter?.property) {
|
|
990
|
+
fields.push({
|
|
991
|
+
field: filter.filter.property,
|
|
992
|
+
order: 'Ascending',
|
|
993
|
+
type: 'WHERE clause (filter)',
|
|
994
|
+
operator: filter.arrange === 'filter' ? 'CONTAINS' : 'RANGE',
|
|
995
|
+
description: `Filtrar por ${filter.filter.property} usando filtro ${filter.arrange}`,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (arrange.sortBy?.field) {
|
|
1001
|
+
fields.push({
|
|
1002
|
+
field: arrange.sortBy.field,
|
|
1003
|
+
order: arrange.sortBy.order === 'desc' ? 'Descending' : 'Ascending',
|
|
1004
|
+
type: 'ORDER BY clause',
|
|
1005
|
+
operator: 'N/A',
|
|
1006
|
+
description: `Ordenar resultados por ${arrange.sortBy.field} em ordem ${arrange.sortBy.order}`,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
instructions.fields = fields;
|
|
1010
|
+
const fieldNames = fields.map((f) => f.field).join(' + ');
|
|
1011
|
+
instructions.summary = `Criar índice composto para ${collection}: ${fieldNames}`;
|
|
1012
|
+
let queryExample = `db.collection('${collection}')`;
|
|
1013
|
+
fields.forEach((field, index) => {
|
|
1014
|
+
if (field.type.includes('WHERE')) {
|
|
1015
|
+
if (field.operator === '==') {
|
|
1016
|
+
queryExample += `\n .where('${field.field}', '==', 'value')`;
|
|
1017
|
+
}
|
|
1018
|
+
else if (field.operator === 'CONTAINS') {
|
|
1019
|
+
queryExample += `\n .where('${field.field}', '>=', 'searchText')`;
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
queryExample += `\n .where('${field.field}', '${field.operator}', 'value')`;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
const orderByField = fields.find((f) => f.type.includes('ORDER BY'));
|
|
1027
|
+
if (orderByField) {
|
|
1028
|
+
queryExample += `\n .orderBy('${orderByField.field}', '${orderByField.order.toLowerCase()}')`;
|
|
1029
|
+
}
|
|
1030
|
+
instructions.queryExample = queryExample;
|
|
1031
|
+
instructions.stepByStep = [
|
|
1032
|
+
'1. Ir para Firebase Console → Firestore → Indexes',
|
|
1033
|
+
'2. Clicar em "Create Index"',
|
|
1034
|
+
`3. Definir Collection ID: ${collection}`,
|
|
1035
|
+
'4. Configurar campos nesta ORDEM EXATA:',
|
|
1036
|
+
...fields.map((field, index) => ` ${index + 1}. Campo: ${field.field}, Order: ${field.order}, Array: No`),
|
|
1037
|
+
'5. Definir Query scopes: Collection',
|
|
1038
|
+
'6. Clicar em "Create" e aguardar conclusão',
|
|
1039
|
+
];
|
|
1040
|
+
instructions.notes = [
|
|
1041
|
+
'⚠️ A ordem dos campos é CRÍTICA - deve corresponder exatamente à ordem da query',
|
|
1042
|
+
'⚠️ As cláusulas WHERE devem vir ANTES do campo ORDER BY',
|
|
1043
|
+
'⚠️ Este índice só funcionará para queries com esta combinação EXATA de campos',
|
|
1044
|
+
'⚠️ A criação do índice pode levar vários minutos',
|
|
1045
|
+
];
|
|
1046
|
+
return instructions;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Gera um link de índice baseado na estrutura da query
|
|
1050
|
+
*/
|
|
1051
|
+
generateIndexLink(collection, arrange, conditions = undefined) {
|
|
1052
|
+
try {
|
|
1053
|
+
const indexFields = [];
|
|
1054
|
+
if (conditions && conditions.length > 0) {
|
|
1055
|
+
conditions.forEach((condition) => {
|
|
1056
|
+
if (condition.firestoreProperty) {
|
|
1057
|
+
indexFields.push(condition.firestoreProperty);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
if (arrange.filters && arrange.filters.length > 0) {
|
|
1062
|
+
arrange.filters.forEach((filter) => {
|
|
1063
|
+
if (filter.filter?.property) {
|
|
1064
|
+
indexFields.push(filter.filter.property);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
if (arrange.sortBy?.field) {
|
|
1069
|
+
indexFields.push(arrange.sortBy.field);
|
|
1070
|
+
}
|
|
1071
|
+
if (indexFields.length > 1) {
|
|
1072
|
+
const baseUrl = 'https://console.firebase.google.com/project/toppayy-dev/firestore/indexes';
|
|
1073
|
+
const queryParams = new URLSearchParams({
|
|
1074
|
+
create_composite: `collection=${collection}&fields=${indexFields.join(',')}`,
|
|
1075
|
+
});
|
|
1076
|
+
const finalLink = `${baseUrl}?${queryParams.toString()}`;
|
|
1077
|
+
return finalLink;
|
|
1078
|
+
}
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
console.warn('Falha ao gerar link de índice:', error);
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async trackMissingIndex(error, collection, arrange, conditions = undefined) {
|
|
1087
|
+
try {
|
|
1088
|
+
const indexLink = this.extractIndexLink(error);
|
|
1089
|
+
if (!indexLink)
|
|
1090
|
+
return;
|
|
1091
|
+
const linkHash = btoa(indexLink)
|
|
1092
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
1093
|
+
.substring(0, 20);
|
|
1094
|
+
const docId = `${collection}_${linkHash}`;
|
|
1095
|
+
const indexInstructions = this.generateIndexInstructions(collection, arrange, conditions);
|
|
1096
|
+
const trackingData = {
|
|
1097
|
+
collection,
|
|
1098
|
+
indexLink,
|
|
1099
|
+
indexInstructions,
|
|
1100
|
+
arrange: {
|
|
1101
|
+
sortBy: arrange.sortBy,
|
|
1102
|
+
filters: arrange.filters?.map((f) => ({
|
|
1103
|
+
arrange: f.arrange,
|
|
1104
|
+
property: f.filter?.property || null,
|
|
1105
|
+
dateField: f.arrange === 'filterByDate' ? arrange.sortBy?.field : null,
|
|
1106
|
+
})) || [],
|
|
1107
|
+
},
|
|
1108
|
+
conditions: conditions?.map((c) => ({
|
|
1109
|
+
property: c.firestoreProperty,
|
|
1110
|
+
operator: c.operator,
|
|
1111
|
+
})) || [],
|
|
1112
|
+
errorMessage: error.message,
|
|
1113
|
+
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
1114
|
+
};
|
|
1115
|
+
const docRef = this.ngFire.collection('missingIndexes').doc(docId);
|
|
1116
|
+
const doc = await docRef.get().toPromise();
|
|
1117
|
+
if (doc && doc.exists) {
|
|
1118
|
+
await docRef.update({
|
|
1119
|
+
count: firebase.firestore.FieldValue.increment(1),
|
|
1120
|
+
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
1121
|
+
lastError: error.message,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
await docRef.set({
|
|
1126
|
+
...trackingData,
|
|
1127
|
+
count: 1,
|
|
1128
|
+
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
catch (trackingError) {
|
|
1133
|
+
console.warn('Falha ao rastrear índice ausente:', trackingError);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
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 });
|
|
1138
|
+
TableService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, providedIn: 'root' });
|
|
1139
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableService, decorators: [{
|
|
1140
|
+
type: Injectable,
|
|
1141
|
+
args: [{
|
|
1142
|
+
providedIn: 'root',
|
|
1143
|
+
}]
|
|
1144
|
+
}], ctorParameters: function () { return [{ type: i1.AngularFirestore, decorators: [{
|
|
1145
|
+
type: Optional
|
|
1146
|
+
}] }, { type: i2.MatDialog, decorators: [{
|
|
1147
|
+
type: Optional
|
|
1148
|
+
}] }, { type: i3.ToastrService, decorators: [{
|
|
1149
|
+
type: Optional
|
|
1150
1150
|
}] }]; } });
|
|
1151
1151
|
|
|
1152
|
-
class TableComponent {
|
|
1153
|
-
// CONSTRUCTOR
|
|
1154
|
-
constructor(router, tableService, firestore) {
|
|
1155
|
-
this.router = router;
|
|
1156
|
-
this.tableService = tableService;
|
|
1157
|
-
this.firestore = firestore;
|
|
1158
|
-
this.arrange = null;
|
|
1159
|
-
this.currentPageNumber = 1;
|
|
1160
|
-
this.currentClientPageIndex = 0;
|
|
1161
|
-
this.items = [];
|
|
1162
|
-
this.filteredItems = []; // Dados filtrados para modo não paginado (público para acesso externo)
|
|
1163
|
-
this.isLoading = false;
|
|
1164
|
-
this.lastDoc = null;
|
|
1165
|
-
this.firstDoc = null;
|
|
1166
|
-
this.sortBy = {
|
|
1167
|
-
field: 'createdAt',
|
|
1168
|
-
order: 'desc',
|
|
1169
|
-
};
|
|
1170
|
-
this.columnProperties = [];
|
|
1171
|
-
this.selectSort = new FormControl('');
|
|
1172
|
-
this.currentArrange = '';
|
|
1173
|
-
this.hasNextPage = false;
|
|
1174
|
-
this.dropdownItems = [];
|
|
1175
|
-
this.sortableDropdownItems = [];
|
|
1176
|
-
this.pageSize = 25;
|
|
1177
|
-
this.totalItems = 0;
|
|
1178
|
-
this.filterValue = null;
|
|
1179
|
-
this.hasFilterableColumn = false;
|
|
1180
|
-
this.hasSortableColumn = false;
|
|
1181
|
-
this.filterSubject = new Subject();
|
|
1182
|
-
this.debounceTimeMs = 500;
|
|
1183
|
-
this.selectedTab = 0;
|
|
1184
|
-
// Propriedades para controle do tooltip
|
|
1185
|
-
this.hoveredCell = null;
|
|
1186
|
-
this.showTooltip = false;
|
|
1187
|
-
this.tooltipContent = '';
|
|
1188
|
-
this.tooltipPosition = { x: 0, y: 0 };
|
|
1189
|
-
this.filtersForm = new FormArray([this.createFilterGroup()]);
|
|
1190
|
-
}
|
|
1191
|
-
createFilterGroup() {
|
|
1192
|
-
return new FormGroup({
|
|
1193
|
-
selectFilter: new FormControl(''),
|
|
1194
|
-
typeFilter: new FormControl(''),
|
|
1195
|
-
selectItem: new FormControl(''),
|
|
1196
|
-
initialDate: new FormControl('',
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
this.
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
this.
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
this.
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
this.
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
this.
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
this.
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
this.
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
1767
|
-
if (
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
this.
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
this.
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
this.
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
if (this.
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
this.
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
if (
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
if (
|
|
2089
|
-
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
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 [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 && hasFilterableColumn === true\"\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 <!-- FILTERS PANEL (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 && dropdownItems.length > 0\"\r\n >\r\n <!-- FILTERS CONTENT -->\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 (blur)=\"onDateFilterChange()\"\r\n formControlName=\"initialDate\"\r\n [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 (blur)=\"onDateFilterChange()\"\r\n matInput\r\n formControlName=\"finalDate\"\r\n [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 & Actions -->\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 <!-- Action Buttons -->\r\n <div class=\"flex flex-wrap gap-3\">\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 Aplicar\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)=\"handleDownload()\"\r\n >\r\n <i class=\"fa fa-download\"></i>\r\n Exportar\r\n </button>\r\n </div>\r\n </div>\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"] }, { kind: "directive", type: i16.NgxMaskDirective, selector: "input[mask], textarea[mask]", inputs: ["mask", "specialCharacters", "patterns", "prefix", "suffix", "thousandSeparator", "decimalMarker", "dropSpecialCharacters", "hiddenInput", "showMaskTyped", "placeHolderCharacter", "shownMaskExpression", "showTemplate", "clearIfNotMatch", "validation", "separatorLimit", "allowNegativeNumbers", "leadZeroDateTime", "leadZero", "triggerOnMaskChange", "apm", "inputTransformFn", "outputTransformFn", "keepCharacterPositions"], outputs: ["maskFilled"], exportAs: ["mask", "ngxMask"] }] });
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
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 [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 && hasFilterableColumn === true\"\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 <!-- FILTERS PANEL (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 && dropdownItems.length > 0\"\r\n >\r\n <!-- FILTERS CONTENT -->\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 (blur)=\"onDateFilterChange()\"\r\n formControlName=\"initialDate\"\r\n [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 (blur)=\"onDateFilterChange()\"\r\n matInput\r\n formControlName=\"finalDate\"\r\n [dropSpecialCharacters]=\"false\"\r\n mask=\"d0/M0/0000\"\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 & Actions -->\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 <!-- Action Buttons -->\r\n <div class=\"flex flex-wrap gap-3\">\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 Aplicar\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)=\"handleDownload()\"\r\n >\r\n <i class=\"fa fa-download\"></i>\r\n Exportar\r\n </button>\r\n </div>\r\n </div>\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"] }]
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
1152
|
+
class TableComponent {
|
|
1153
|
+
// CONSTRUCTOR
|
|
1154
|
+
constructor(router, tableService, firestore) {
|
|
1155
|
+
this.router = router;
|
|
1156
|
+
this.tableService = tableService;
|
|
1157
|
+
this.firestore = firestore;
|
|
1158
|
+
this.arrange = null;
|
|
1159
|
+
this.currentPageNumber = 1;
|
|
1160
|
+
this.currentClientPageIndex = 0;
|
|
1161
|
+
this.items = [];
|
|
1162
|
+
this.filteredItems = []; // Dados filtrados para modo não paginado (público para acesso externo)
|
|
1163
|
+
this.isLoading = false;
|
|
1164
|
+
this.lastDoc = null;
|
|
1165
|
+
this.firstDoc = null;
|
|
1166
|
+
this.sortBy = {
|
|
1167
|
+
field: 'createdAt',
|
|
1168
|
+
order: 'desc',
|
|
1169
|
+
};
|
|
1170
|
+
this.columnProperties = [];
|
|
1171
|
+
this.selectSort = new FormControl('');
|
|
1172
|
+
this.currentArrange = '';
|
|
1173
|
+
this.hasNextPage = false;
|
|
1174
|
+
this.dropdownItems = [];
|
|
1175
|
+
this.sortableDropdownItems = [];
|
|
1176
|
+
this.pageSize = 25;
|
|
1177
|
+
this.totalItems = 0;
|
|
1178
|
+
this.filterValue = null;
|
|
1179
|
+
this.hasFilterableColumn = false;
|
|
1180
|
+
this.hasSortableColumn = false;
|
|
1181
|
+
this.filterSubject = new Subject();
|
|
1182
|
+
this.debounceTimeMs = 500;
|
|
1183
|
+
this.selectedTab = 0;
|
|
1184
|
+
// Propriedades para controle do tooltip
|
|
1185
|
+
this.hoveredCell = null;
|
|
1186
|
+
this.showTooltip = false;
|
|
1187
|
+
this.tooltipContent = '';
|
|
1188
|
+
this.tooltipPosition = { x: 0, y: 0 };
|
|
1189
|
+
this.filtersForm = new FormArray([this.createFilterGroup()]);
|
|
1190
|
+
}
|
|
1191
|
+
createFilterGroup() {
|
|
1192
|
+
return new FormGroup({
|
|
1193
|
+
selectFilter: new FormControl(''),
|
|
1194
|
+
typeFilter: new FormControl(''),
|
|
1195
|
+
selectItem: new FormControl(''),
|
|
1196
|
+
initialDate: new FormControl('', [
|
|
1197
|
+
this.tableService.dateFormatValidator(),
|
|
1198
|
+
]),
|
|
1199
|
+
finalDate: new FormControl('', [this.tableService.dateFormatValidator()]),
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
addFilter(filterData) {
|
|
1203
|
+
if (this.data.pagination === true &&
|
|
1204
|
+
this.hasActiveDateFilter() &&
|
|
1205
|
+
filterData?.selectFilter?.arrange === 'filterByDate') {
|
|
1206
|
+
// Não permitir adicionar novo filtro de data quando já existe um ativo
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const newFilterGroup = this.createFilterGroup();
|
|
1210
|
+
if (filterData) {
|
|
1211
|
+
if (filterData.selectFilter) {
|
|
1212
|
+
newFilterGroup.get('selectFilter')?.setValue(filterData.selectFilter);
|
|
1213
|
+
}
|
|
1214
|
+
if (filterData.typeFilter) {
|
|
1215
|
+
newFilterGroup.get('typeFilter')?.setValue(filterData.typeFilter);
|
|
1216
|
+
}
|
|
1217
|
+
if (filterData.selectItem) {
|
|
1218
|
+
newFilterGroup.get('selectItem')?.setValue(filterData.selectItem);
|
|
1219
|
+
}
|
|
1220
|
+
if (filterData.initialDate) {
|
|
1221
|
+
newFilterGroup.get('initialDate')?.setValue(filterData.initialDate);
|
|
1222
|
+
}
|
|
1223
|
+
if (filterData.finalDate) {
|
|
1224
|
+
newFilterGroup.get('finalDate')?.setValue(filterData.finalDate);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
this.filtersForm.push(newFilterGroup);
|
|
1228
|
+
}
|
|
1229
|
+
hasActiveDateFilter() {
|
|
1230
|
+
return this.filtersForm.controls.some((control) => {
|
|
1231
|
+
const group = control;
|
|
1232
|
+
const selectedFilter = group.get('selectFilter')?.value;
|
|
1233
|
+
return selectedFilter?.arrange === 'filterByDate';
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
getAvailableFilterOptions() {
|
|
1237
|
+
if (this.data.pagination === true && this.hasActiveDateFilter()) {
|
|
1238
|
+
return this.dropdownItems.filter((item) => item.arrange !== 'filterByDate');
|
|
1239
|
+
}
|
|
1240
|
+
return this.dropdownItems;
|
|
1241
|
+
}
|
|
1242
|
+
onSelectFilterChange() {
|
|
1243
|
+
const lastIndex = this.filtersForm.length - 1;
|
|
1244
|
+
const lastFilter = this.filtersForm.at(lastIndex);
|
|
1245
|
+
const selectedFilter = lastFilter.get('selectFilter')?.value;
|
|
1246
|
+
if (selectedFilter) {
|
|
1247
|
+
if (selectedFilter.arrange === 'filterByDate' &&
|
|
1248
|
+
this.data.pagination === true) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
this.addFilter();
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
removeFilter(index) {
|
|
1255
|
+
this.filtersForm.removeAt(index);
|
|
1256
|
+
if (this.filtersForm.length === 0) {
|
|
1257
|
+
this.addFilter();
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
removeAllFilters() {
|
|
1261
|
+
this.filtersForm.clear();
|
|
1262
|
+
this.addFilter();
|
|
1263
|
+
this.resetFilter();
|
|
1264
|
+
}
|
|
1265
|
+
// METHODS
|
|
1266
|
+
async ngOnInit() {
|
|
1267
|
+
if (!this.data.color)
|
|
1268
|
+
this.data.color = { bg: 'bg-primary', text: 'text-black' };
|
|
1269
|
+
this.columnProperties = this.data.displayedColumns.map((column) => {
|
|
1270
|
+
return column.property;
|
|
1271
|
+
});
|
|
1272
|
+
if (this.data.actionButton && !this.data.actionButton.condition) {
|
|
1273
|
+
this.data.actionButton.condition = (_row) => true;
|
|
1274
|
+
}
|
|
1275
|
+
this.data.displayedColumns.forEach((col) => {
|
|
1276
|
+
if (col.isFilterable) {
|
|
1277
|
+
if (this.hasFilterableColumn === false)
|
|
1278
|
+
this.hasFilterableColumn = true;
|
|
1279
|
+
this.dropdownItems.push({
|
|
1280
|
+
...col,
|
|
1281
|
+
arrange: 'filter',
|
|
1282
|
+
title: col.title,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (col.isSortable) {
|
|
1286
|
+
if (this.hasSortableColumn === false)
|
|
1287
|
+
this.hasSortableColumn = true;
|
|
1288
|
+
this.sortableDropdownItems.push({
|
|
1289
|
+
...col,
|
|
1290
|
+
arrange: 'ascending',
|
|
1291
|
+
title: col.title + ': crescente',
|
|
1292
|
+
});
|
|
1293
|
+
this.sortableDropdownItems.push({
|
|
1294
|
+
...col,
|
|
1295
|
+
arrange: 'descending',
|
|
1296
|
+
title: col.title + ': decrescente',
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
if (col.isFilterableByDate) {
|
|
1300
|
+
this.dropdownItems.push({
|
|
1301
|
+
...col,
|
|
1302
|
+
arrange: 'filterByDate',
|
|
1303
|
+
title: col.title + ': filtro por data',
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
if (this.data.filterableOptions &&
|
|
1308
|
+
Array.isArray(this.data.filterableOptions) &&
|
|
1309
|
+
this.data.filterableOptions.length > 0) {
|
|
1310
|
+
this.data.filterableOptions.forEach((option) => this.dropdownItems.push({ ...option, arrange: 'equals' }));
|
|
1311
|
+
}
|
|
1312
|
+
// Sem paginação
|
|
1313
|
+
if (this.data.pagination === false) {
|
|
1314
|
+
await this.loadItems();
|
|
1315
|
+
}
|
|
1316
|
+
// Com paginação
|
|
1317
|
+
if (this.data.pagination === true) {
|
|
1318
|
+
if (this.data.sortBy)
|
|
1319
|
+
this.sortBy = {
|
|
1320
|
+
field: this.data.sortBy.field,
|
|
1321
|
+
order: this.data.sortBy.order,
|
|
1322
|
+
};
|
|
1323
|
+
this.filterSubject
|
|
1324
|
+
.pipe(debounceTime(this.debounceTimeMs))
|
|
1325
|
+
.subscribe(() => {
|
|
1326
|
+
this.loadItemsPaginated('reload', true);
|
|
1327
|
+
});
|
|
1328
|
+
this.isLoading = true;
|
|
1329
|
+
await this.loadItemsPaginated('reload', true);
|
|
1330
|
+
this.sort.active = 'createdAt';
|
|
1331
|
+
this.sort.direction = 'desc';
|
|
1332
|
+
this.dataSource.paginator = this.paginator;
|
|
1333
|
+
this.dataSource.sort = this.sort;
|
|
1334
|
+
this.totalItems = 0;
|
|
1335
|
+
if (this.data.totalRef) {
|
|
1336
|
+
for (const totalRef of this.data.totalRef) {
|
|
1337
|
+
const totalRefDoc = await totalRef.ref.get();
|
|
1338
|
+
const docData = totalRefDoc.data();
|
|
1339
|
+
if (docData && docData[totalRef.field])
|
|
1340
|
+
this.totalItems = (this.totalItems +
|
|
1341
|
+
docData[totalRef.field]);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
this.isLoading = false;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
getDisplayValue(col, row, withinLimit = false) {
|
|
1348
|
+
let value;
|
|
1349
|
+
if (col.calculateValue) {
|
|
1350
|
+
value = col.calculateValue(row);
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
value = this.getNestedValue(row, col.property);
|
|
1354
|
+
}
|
|
1355
|
+
if (Array.isArray(value) && col.arrayField) {
|
|
1356
|
+
value = this.formatArrayValue(value, col.arrayField);
|
|
1357
|
+
}
|
|
1358
|
+
if (col.queryLength && row[col.property]) {
|
|
1359
|
+
value = row[col.property];
|
|
1360
|
+
}
|
|
1361
|
+
value = col.pipe ? col.pipe.transform(value) : value;
|
|
1362
|
+
if (value === null || value === undefined) {
|
|
1363
|
+
value = '';
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
value = String(value);
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof value !== 'string') {
|
|
1369
|
+
value = '';
|
|
1370
|
+
}
|
|
1371
|
+
if (withinLimit || !col.charLimit) {
|
|
1372
|
+
return value;
|
|
1373
|
+
}
|
|
1374
|
+
return value.substring(0, col.charLimit) + '...';
|
|
1375
|
+
}
|
|
1376
|
+
getNestedValue(obj, path) {
|
|
1377
|
+
if (!path)
|
|
1378
|
+
return undefined;
|
|
1379
|
+
const properties = path.split('.');
|
|
1380
|
+
return properties.reduce((acc, currentPart) => acc && acc[currentPart], obj);
|
|
1381
|
+
}
|
|
1382
|
+
formatArrayValue(array, field) {
|
|
1383
|
+
if (!Array.isArray(array) || array.length === 0) {
|
|
1384
|
+
return '';
|
|
1385
|
+
}
|
|
1386
|
+
const values = array
|
|
1387
|
+
.map((item) => {
|
|
1388
|
+
if (typeof item === 'object' && item !== null) {
|
|
1389
|
+
return item[field] || '';
|
|
1390
|
+
}
|
|
1391
|
+
return String(item);
|
|
1392
|
+
})
|
|
1393
|
+
.filter((value) => value !== '' && value !== null && value !== undefined);
|
|
1394
|
+
return values.join(', ');
|
|
1395
|
+
}
|
|
1396
|
+
// Métodos sem paginação
|
|
1397
|
+
async loadItems() {
|
|
1398
|
+
this.items = await this.tableService.getItems(this.data.collectionRef);
|
|
1399
|
+
if (this.data.conditions) {
|
|
1400
|
+
this.filterItems();
|
|
1401
|
+
}
|
|
1402
|
+
await this.loadRelations();
|
|
1403
|
+
await this.loadQueryLengths();
|
|
1404
|
+
this.totalItems = this.items.length;
|
|
1405
|
+
// Inicializar arrange para tabelas não paginadas
|
|
1406
|
+
this.arrange = {
|
|
1407
|
+
filters: [],
|
|
1408
|
+
sortBy: this.data.sortBy || {
|
|
1409
|
+
field: 'createdAt',
|
|
1410
|
+
order: 'desc',
|
|
1411
|
+
},
|
|
1412
|
+
};
|
|
1413
|
+
this.sortBy = this.data.sortBy || {
|
|
1414
|
+
field: 'createdAt',
|
|
1415
|
+
order: 'desc',
|
|
1416
|
+
};
|
|
1417
|
+
// Aplicar filtros client-side se existirem
|
|
1418
|
+
let itemsToDisplay = [...this.items];
|
|
1419
|
+
itemsToDisplay = this.applyClientSideFilters(itemsToDisplay);
|
|
1420
|
+
this.filteredItems = itemsToDisplay; // Armazenar dados filtrados
|
|
1421
|
+
this.dataSource = new MatTableDataSource(itemsToDisplay);
|
|
1422
|
+
this.dataSource.paginator = this.paginator;
|
|
1423
|
+
this.dataSource.sort = this.sort;
|
|
1424
|
+
if (this.data.sortBy) {
|
|
1425
|
+
this.dataSource.sort.active = this.data.sortBy.field;
|
|
1426
|
+
this.dataSource.sort.direction = this.data.sortBy.order;
|
|
1427
|
+
this.dataSource.sort.sortChange.emit();
|
|
1428
|
+
}
|
|
1429
|
+
this.dataSource.filterPredicate = (data, filter) => {
|
|
1430
|
+
return this.data.displayedColumns.some((col) => {
|
|
1431
|
+
if (col.filterPredicates) {
|
|
1432
|
+
return col.filterPredicates.some((predicate) => {
|
|
1433
|
+
const propertyValue = data[col.property];
|
|
1434
|
+
if (!propertyValue || typeof propertyValue !== 'object') {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
const predicateValue = propertyValue[predicate];
|
|
1438
|
+
if (predicateValue === null || predicateValue === undefined) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
return String(predicateValue)
|
|
1442
|
+
.trim()
|
|
1443
|
+
.toLocaleLowerCase()
|
|
1444
|
+
.includes(filter);
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
if (col.property && col.isFilterable) {
|
|
1448
|
+
const propertyValue = data[col.property];
|
|
1449
|
+
if (propertyValue === null || propertyValue === undefined) {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
return String(propertyValue)
|
|
1453
|
+
.trim()
|
|
1454
|
+
.toLocaleLowerCase()
|
|
1455
|
+
.includes(filter);
|
|
1456
|
+
}
|
|
1457
|
+
return false;
|
|
1458
|
+
});
|
|
1459
|
+
};
|
|
1460
|
+
this.filterPredicate = this.dataSource.filterPredicate;
|
|
1461
|
+
}
|
|
1462
|
+
// Aplicar filtros client-side (filtros por data)
|
|
1463
|
+
applyClientSideFilters(items) {
|
|
1464
|
+
let filteredItems = [...items];
|
|
1465
|
+
// Agrupar filtros de data por propriedade
|
|
1466
|
+
const dateFiltersByProperty = {};
|
|
1467
|
+
const otherFilters = [];
|
|
1468
|
+
this.filtersForm.controls.forEach((control) => {
|
|
1469
|
+
const group = control;
|
|
1470
|
+
const selectedFilter = group.get('selectFilter')?.value;
|
|
1471
|
+
if (!selectedFilter)
|
|
1472
|
+
return;
|
|
1473
|
+
const arrange = selectedFilter.arrange;
|
|
1474
|
+
if (arrange === 'filterByDate') {
|
|
1475
|
+
const initial = group.get('initialDate')?.value;
|
|
1476
|
+
const final = group.get('finalDate')?.value;
|
|
1477
|
+
// Só aplicar filtro se ambas as datas estiverem preenchidas
|
|
1478
|
+
if (initial && final && initial.trim() && final.trim()) {
|
|
1479
|
+
try {
|
|
1480
|
+
// Validar formato da data (DD/MM/AAAA)
|
|
1481
|
+
const datePattern = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
|
|
1482
|
+
if (!datePattern.test(initial) || !datePattern.test(final)) {
|
|
1483
|
+
return; // Ignorar se formato inválido
|
|
1484
|
+
}
|
|
1485
|
+
const [dayI, monthI, yearI] = initial.split('/');
|
|
1486
|
+
const initialDate = new Date(`${monthI}/${dayI}/${yearI}`);
|
|
1487
|
+
const [dayF, monthF, yearF] = final.split('/');
|
|
1488
|
+
const finalDate = new Date(`${monthF}/${dayF}/${yearF}`);
|
|
1489
|
+
finalDate.setHours(23, 59, 59);
|
|
1490
|
+
// Validar se as datas são válidas
|
|
1491
|
+
if (isNaN(initialDate.getTime()) || isNaN(finalDate.getTime())) {
|
|
1492
|
+
return; // Ignorar se datas inválidas
|
|
1493
|
+
}
|
|
1494
|
+
// Usar o campo da coluna ou o sortBy.field como padrão
|
|
1495
|
+
const dateField = selectedFilter.property || this.sortBy.field;
|
|
1496
|
+
if (!dateFiltersByProperty[dateField]) {
|
|
1497
|
+
dateFiltersByProperty[dateField] = [];
|
|
1498
|
+
}
|
|
1499
|
+
dateFiltersByProperty[dateField].push({
|
|
1500
|
+
initial: initialDate,
|
|
1501
|
+
final: finalDate,
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
console.warn('Erro ao processar datas do filtro:', error);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
else {
|
|
1510
|
+
otherFilters.push({ group, selectedFilter, arrange });
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
Object.keys(dateFiltersByProperty).forEach((dateField) => {
|
|
1514
|
+
const intervals = dateFiltersByProperty[dateField];
|
|
1515
|
+
filteredItems = filteredItems.filter((item) => {
|
|
1516
|
+
try {
|
|
1517
|
+
const fieldValue = item[dateField];
|
|
1518
|
+
if (!fieldValue) {
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
let itemDate;
|
|
1522
|
+
if (typeof fieldValue.toDate === 'function') {
|
|
1523
|
+
itemDate = fieldValue.toDate();
|
|
1524
|
+
}
|
|
1525
|
+
else if (fieldValue instanceof Date) {
|
|
1526
|
+
itemDate = fieldValue;
|
|
1527
|
+
}
|
|
1528
|
+
else if (typeof fieldValue === 'string') {
|
|
1529
|
+
itemDate = new Date(fieldValue);
|
|
1530
|
+
if (isNaN(itemDate.getTime())) {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
else if (typeof fieldValue === 'number') {
|
|
1535
|
+
itemDate = new Date(fieldValue);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
return intervals.some((interval) => itemDate >= interval.initial && itemDate <= interval.final);
|
|
1541
|
+
}
|
|
1542
|
+
catch (error) {
|
|
1543
|
+
console.warn('Erro ao processar filtro de data para o item:', item.id, error);
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
});
|
|
1548
|
+
otherFilters.forEach(({ group, selectedFilter, arrange }) => {
|
|
1549
|
+
if (arrange === 'filter') {
|
|
1550
|
+
const filterValue = group.get('typeFilter')?.value;
|
|
1551
|
+
if (filterValue && filterValue.trim()) {
|
|
1552
|
+
const property = selectedFilter.property;
|
|
1553
|
+
filteredItems = filteredItems.filter((item) => {
|
|
1554
|
+
const value = String(item[property] || '')
|
|
1555
|
+
.trim()
|
|
1556
|
+
.toLocaleLowerCase();
|
|
1557
|
+
return value.includes(filterValue.trim().toLocaleLowerCase());
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
else if (selectedFilter.hasOwnProperty('items') &&
|
|
1562
|
+
arrange === 'equals') {
|
|
1563
|
+
const selectedItems = group.get('selectItem')?.value;
|
|
1564
|
+
if (Array.isArray(selectedItems) && selectedItems.length > 0) {
|
|
1565
|
+
const values = selectedItems.map((item) => item.value);
|
|
1566
|
+
filteredItems = filteredItems.filter((item) => {
|
|
1567
|
+
return selectedItems.some((selectedItem) => {
|
|
1568
|
+
const itemValue = item[selectedItem.property];
|
|
1569
|
+
return itemValue === selectedItem.value;
|
|
1570
|
+
});
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
return filteredItems;
|
|
1576
|
+
}
|
|
1577
|
+
// Handler para mudanças de data no filtro
|
|
1578
|
+
onDateFilterChange() {
|
|
1579
|
+
if (this.data.pagination === false) {
|
|
1580
|
+
// Atualizar arrange com os filtros ativos
|
|
1581
|
+
this.arrange = this.buildArrangeFromFilters();
|
|
1582
|
+
// Aplicar filtros (ou remover se campos estiverem vazios)
|
|
1583
|
+
this.applyFiltersToDataSource();
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
// Aplicar filtros ao dataSource quando não há paginação
|
|
1587
|
+
applyFiltersToDataSource() {
|
|
1588
|
+
if (!this.dataSource)
|
|
1589
|
+
return;
|
|
1590
|
+
let filteredItems = this.applyClientSideFilters([...this.items]);
|
|
1591
|
+
this.dataSource.data = filteredItems;
|
|
1592
|
+
// Atualizar filteredItems com os dados filtrados (será atualizado novamente no handleDownload com filteredData)
|
|
1593
|
+
this.filteredItems = filteredItems;
|
|
1594
|
+
this.totalItems = filteredItems.length;
|
|
1595
|
+
}
|
|
1596
|
+
// Métodos com paginação
|
|
1597
|
+
async loadItemsPaginated(navigation = 'reload', reset = false) {
|
|
1598
|
+
if (reset && ['forward', 'reload'].includes(navigation)) {
|
|
1599
|
+
this.lastDoc = null;
|
|
1600
|
+
this.currentClientPageIndex = 0;
|
|
1601
|
+
}
|
|
1602
|
+
if (reset && ['backward', 'reload'].includes(navigation)) {
|
|
1603
|
+
this.firstDoc = null;
|
|
1604
|
+
this.currentClientPageIndex = 0;
|
|
1605
|
+
}
|
|
1606
|
+
const activeFilters = this.filtersForm.controls
|
|
1607
|
+
.flatMap((control) => {
|
|
1608
|
+
const group = control;
|
|
1609
|
+
const selectedFilter = group.get('selectFilter')?.value;
|
|
1610
|
+
if (!selectedFilter)
|
|
1611
|
+
return [];
|
|
1612
|
+
const arrange = selectedFilter.arrange;
|
|
1613
|
+
if (arrange === 'filter') {
|
|
1614
|
+
const filterValue = group.get('typeFilter')?.value;
|
|
1615
|
+
if (!filterValue)
|
|
1616
|
+
return [];
|
|
1617
|
+
return {
|
|
1618
|
+
arrange,
|
|
1619
|
+
filter: {
|
|
1620
|
+
property: selectedFilter.property,
|
|
1621
|
+
filtering: filterValue,
|
|
1622
|
+
},
|
|
1623
|
+
dateFilter: undefined,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
if (arrange === 'filterByDate') {
|
|
1627
|
+
const initial = group.get('initialDate')?.value;
|
|
1628
|
+
const final = group.get('finalDate')?.value;
|
|
1629
|
+
if (initial && final) {
|
|
1630
|
+
const [dayI, monthI, yearI] = initial.split('/');
|
|
1631
|
+
const initialDate = new Date(`${monthI}/${dayI}/${yearI}`);
|
|
1632
|
+
const [dayF, monthF, yearF] = final.split('/');
|
|
1633
|
+
const finalDate = new Date(`${monthF}/${dayF}/${yearF}`);
|
|
1634
|
+
finalDate.setHours(23, 59, 59);
|
|
1635
|
+
return {
|
|
1636
|
+
arrange,
|
|
1637
|
+
filter: undefined,
|
|
1638
|
+
dateFilter: {
|
|
1639
|
+
initial: initialDate,
|
|
1640
|
+
final: finalDate,
|
|
1641
|
+
},
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
return [];
|
|
1645
|
+
}
|
|
1646
|
+
if (selectedFilter.hasOwnProperty('items') && arrange === 'equals') {
|
|
1647
|
+
const selectedItems = group.get('selectItem')?.value;
|
|
1648
|
+
if (Array.isArray(selectedItems) && selectedItems.length > 0) {
|
|
1649
|
+
return selectedItems.map((item) => ({
|
|
1650
|
+
arrange,
|
|
1651
|
+
filter: {
|
|
1652
|
+
property: item.property,
|
|
1653
|
+
filtering: item.value,
|
|
1654
|
+
},
|
|
1655
|
+
dateFilter: undefined,
|
|
1656
|
+
}));
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return [];
|
|
1660
|
+
})
|
|
1661
|
+
.filter((f) => f && (f.filter?.filtering !== undefined || f.dateFilter));
|
|
1662
|
+
this.arrange = {
|
|
1663
|
+
filters: activeFilters,
|
|
1664
|
+
sortBy: this.sortBy,
|
|
1665
|
+
};
|
|
1666
|
+
const paginated = {
|
|
1667
|
+
batchSize: this.pageSize,
|
|
1668
|
+
collection: this.data.collection,
|
|
1669
|
+
doc: { lastDoc: this.lastDoc, firstDoc: this.firstDoc },
|
|
1670
|
+
navigation,
|
|
1671
|
+
arrange: this.arrange,
|
|
1672
|
+
conditions: this.data.conditions,
|
|
1673
|
+
size: this.totalItems,
|
|
1674
|
+
filterFn: this.data.filterFn,
|
|
1675
|
+
clientPageIndex: this.currentClientPageIndex,
|
|
1676
|
+
};
|
|
1677
|
+
const result = await this.tableService.getPaginated(paginated);
|
|
1678
|
+
this.items = result.items;
|
|
1679
|
+
await this.loadRelations();
|
|
1680
|
+
await this.loadQueryLengths();
|
|
1681
|
+
this.lastDoc = result.lastDoc;
|
|
1682
|
+
this.firstDoc = result.firstDoc;
|
|
1683
|
+
// Atualizar currentClientPageIndex se retornado pelo fallback
|
|
1684
|
+
if (result.currentClientPageIndex !== undefined) {
|
|
1685
|
+
this.currentClientPageIndex = result.currentClientPageIndex;
|
|
1686
|
+
}
|
|
1687
|
+
let sum = 0;
|
|
1688
|
+
if (this.data.totalRef) {
|
|
1689
|
+
for (const totalRef of this.data.totalRef) {
|
|
1690
|
+
const totalRefDoc = await totalRef.ref.get();
|
|
1691
|
+
const docData = totalRefDoc.data();
|
|
1692
|
+
if (docData || result.filterLength) {
|
|
1693
|
+
sum =
|
|
1694
|
+
result.filterLength ??
|
|
1695
|
+
(sum + (docData ? docData[totalRef.field] : 0));
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
this.totalItems = sum;
|
|
1699
|
+
}
|
|
1700
|
+
this.hasNextPage = result.hasNextPage;
|
|
1701
|
+
this.dataSource = new MatTableDataSource(this.items);
|
|
1702
|
+
this.filterPredicate = this.dataSource.filterPredicate;
|
|
1703
|
+
}
|
|
1704
|
+
async onPageChange(event) {
|
|
1705
|
+
if (this.data.pagination === true && event) {
|
|
1706
|
+
this.isLoading = true;
|
|
1707
|
+
const previousPageIndex = event.previousPageIndex ?? 0;
|
|
1708
|
+
const pageIndex = event.pageIndex;
|
|
1709
|
+
const currentComponentPageSize = this.pageSize;
|
|
1710
|
+
const eventPageSize = event.pageSize;
|
|
1711
|
+
const totalItems = this.totalItems;
|
|
1712
|
+
let navigationDirection;
|
|
1713
|
+
let resetDocs = false;
|
|
1714
|
+
let originalPageSize = null;
|
|
1715
|
+
const lastPageIndex = Math.max(0, Math.ceil(totalItems / eventPageSize) - 1);
|
|
1716
|
+
// Atualizar currentClientPageIndex sempre para o fallback
|
|
1717
|
+
this.currentClientPageIndex = pageIndex;
|
|
1718
|
+
if (previousPageIndex !== undefined && pageIndex > previousPageIndex) {
|
|
1719
|
+
this.currentPageNumber++;
|
|
1720
|
+
}
|
|
1721
|
+
else if (previousPageIndex !== undefined &&
|
|
1722
|
+
pageIndex < previousPageIndex) {
|
|
1723
|
+
this.currentPageNumber = Math.max(1, this.currentPageNumber - 1);
|
|
1724
|
+
}
|
|
1725
|
+
if (eventPageSize !== currentComponentPageSize) {
|
|
1726
|
+
console.log('Alterou a quantidade de elementos exibidos por página');
|
|
1727
|
+
this.pageSize = eventPageSize;
|
|
1728
|
+
navigationDirection = 'forward';
|
|
1729
|
+
resetDocs = true;
|
|
1730
|
+
this.currentClientPageIndex = 0;
|
|
1731
|
+
}
|
|
1732
|
+
else if (pageIndex === 0 &&
|
|
1733
|
+
previousPageIndex !== undefined &&
|
|
1734
|
+
pageIndex < previousPageIndex) {
|
|
1735
|
+
console.log('Pulou para a primeira página');
|
|
1736
|
+
navigationDirection = 'forward';
|
|
1737
|
+
this.currentPageNumber = 1;
|
|
1738
|
+
this.currentClientPageIndex = 0;
|
|
1739
|
+
resetDocs = true;
|
|
1740
|
+
}
|
|
1741
|
+
else if (pageIndex === lastPageIndex &&
|
|
1742
|
+
previousPageIndex !== undefined &&
|
|
1743
|
+
pageIndex > previousPageIndex &&
|
|
1744
|
+
pageIndex - previousPageIndex > 1) {
|
|
1745
|
+
console.log('Pulou para a ultima página');
|
|
1746
|
+
navigationDirection = 'backward';
|
|
1747
|
+
resetDocs = true;
|
|
1748
|
+
const itemsExpectedInLastPage = totalItems - lastPageIndex * eventPageSize;
|
|
1749
|
+
if (itemsExpectedInLastPage > 0 &&
|
|
1750
|
+
itemsExpectedInLastPage < eventPageSize) {
|
|
1751
|
+
originalPageSize = this.pageSize;
|
|
1752
|
+
this.pageSize = itemsExpectedInLastPage;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
else if (previousPageIndex !== undefined &&
|
|
1756
|
+
pageIndex > previousPageIndex) {
|
|
1757
|
+
console.log('Procedendo');
|
|
1758
|
+
navigationDirection = 'forward';
|
|
1759
|
+
resetDocs = false;
|
|
1760
|
+
}
|
|
1761
|
+
else if (previousPageIndex !== undefined &&
|
|
1762
|
+
pageIndex < previousPageIndex) {
|
|
1763
|
+
console.log('Retrocedendo.');
|
|
1764
|
+
navigationDirection = 'backward';
|
|
1765
|
+
resetDocs = false;
|
|
1766
|
+
}
|
|
1767
|
+
else if (previousPageIndex !== undefined &&
|
|
1768
|
+
pageIndex === previousPageIndex) {
|
|
1769
|
+
console.log('Recarregando.');
|
|
1770
|
+
navigationDirection = 'reload';
|
|
1771
|
+
resetDocs = false;
|
|
1772
|
+
}
|
|
1773
|
+
else if (previousPageIndex === undefined && pageIndex === 0) {
|
|
1774
|
+
console.log('Evento inicial do paginador para pág 0. ngOnInit carregou.');
|
|
1775
|
+
this.isLoading = false;
|
|
1776
|
+
if (event)
|
|
1777
|
+
this.pageEvent = event;
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
else {
|
|
1781
|
+
console.warn('INESPERADO! Condição de navegação não tratada:', event);
|
|
1782
|
+
this.isLoading = false;
|
|
1783
|
+
if (event)
|
|
1784
|
+
this.pageEvent = event;
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (navigationDirection) {
|
|
1788
|
+
try {
|
|
1789
|
+
await this.loadItemsPaginated(navigationDirection, resetDocs);
|
|
1790
|
+
}
|
|
1791
|
+
catch (error) {
|
|
1792
|
+
console.error('Erro ao carregar itens paginados:', error);
|
|
1793
|
+
}
|
|
1794
|
+
finally {
|
|
1795
|
+
if (originalPageSize !== null) {
|
|
1796
|
+
this.pageSize = originalPageSize;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
if (event)
|
|
1801
|
+
this.pageEvent = event;
|
|
1802
|
+
this.isLoading = false;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Outros métodos
|
|
1806
|
+
applyFilter(value) {
|
|
1807
|
+
// Sem paginação
|
|
1808
|
+
if (this.data.pagination === false) {
|
|
1809
|
+
this.dataSource.filter = String(value).trim().toLowerCase();
|
|
1810
|
+
}
|
|
1811
|
+
// Com paginação
|
|
1812
|
+
if (this.data.pagination === true) {
|
|
1813
|
+
this.filterValue = value;
|
|
1814
|
+
this.filterSubject.next(this.filterValue);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
goToDetails(row) {
|
|
1818
|
+
if (this.data.isNotClickable) {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const urlPath = this.data.url || this.data.name;
|
|
1822
|
+
const url = this.router.serializeUrl(this.router.createUrlTree([`/${urlPath}`, row.id]));
|
|
1823
|
+
window.open(url, '_blank');
|
|
1824
|
+
}
|
|
1825
|
+
async getRelation(params) {
|
|
1826
|
+
try {
|
|
1827
|
+
let snapshot;
|
|
1828
|
+
if (params.id !== '' &&
|
|
1829
|
+
params.id !== undefined &&
|
|
1830
|
+
params.collection !== undefined &&
|
|
1831
|
+
params.collection !== '') {
|
|
1832
|
+
snapshot = await firstValueFrom(this.firestore.collection(params.collection).doc(params.id).get());
|
|
1833
|
+
}
|
|
1834
|
+
if (snapshot && snapshot.exists) {
|
|
1835
|
+
const data = snapshot.data();
|
|
1836
|
+
return data?.[params.newProperty] ?? '';
|
|
1837
|
+
}
|
|
1838
|
+
return '';
|
|
1839
|
+
}
|
|
1840
|
+
catch (e) {
|
|
1841
|
+
console.log(e);
|
|
1842
|
+
return '';
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
async loadRelations() {
|
|
1846
|
+
const relationPromises = this.data.displayedColumns
|
|
1847
|
+
.filter((col) => col.relation)
|
|
1848
|
+
.flatMap((col) => this.items.map(async (item) => {
|
|
1849
|
+
if (col.relation) {
|
|
1850
|
+
item[col.property] = await this.getRelation({
|
|
1851
|
+
id: item[col.relation.property],
|
|
1852
|
+
collection: col.relation.collection,
|
|
1853
|
+
newProperty: col.relation.newProperty,
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}));
|
|
1857
|
+
await Promise.all(relationPromises);
|
|
1858
|
+
}
|
|
1859
|
+
async getQueryLength(params) {
|
|
1860
|
+
const snapshot = await this.firestore
|
|
1861
|
+
.collection(params.relation.collection)
|
|
1862
|
+
.ref.where(params.relation.property, params.relation.operator, params.item[params.relation.value])
|
|
1863
|
+
.get();
|
|
1864
|
+
return snapshot.size;
|
|
1865
|
+
}
|
|
1866
|
+
async loadQueryLengths() {
|
|
1867
|
+
const lengthPromises = this.data.displayedColumns
|
|
1868
|
+
.filter((col) => col.queryLength)
|
|
1869
|
+
.flatMap((col) => this.items.map(async (item) => {
|
|
1870
|
+
if (col.queryLength) {
|
|
1871
|
+
item[col.property] = await this.getQueryLength({
|
|
1872
|
+
item: item,
|
|
1873
|
+
relation: col.queryLength,
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}));
|
|
1877
|
+
await Promise.all(lengthPromises);
|
|
1878
|
+
}
|
|
1879
|
+
filterItems() {
|
|
1880
|
+
if (this.data.conditions) {
|
|
1881
|
+
this.data.conditions.forEach((cond) => {
|
|
1882
|
+
this.items = this.items.filter((item) => {
|
|
1883
|
+
const operatorFunction = this.tableService.operators[cond.operator];
|
|
1884
|
+
if (operatorFunction) {
|
|
1885
|
+
return operatorFunction(item[cond.firestoreProperty], cond.dashProperty);
|
|
1886
|
+
}
|
|
1887
|
+
return false;
|
|
1888
|
+
});
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
buildArrangeFromFilters() {
|
|
1893
|
+
const activeFilters = this.filtersForm.controls
|
|
1894
|
+
.flatMap((control) => {
|
|
1895
|
+
const group = control;
|
|
1896
|
+
const selectedFilter = group.get('selectFilter')?.value;
|
|
1897
|
+
if (!selectedFilter)
|
|
1898
|
+
return [];
|
|
1899
|
+
const arrange = selectedFilter.arrange;
|
|
1900
|
+
if (arrange === 'filter') {
|
|
1901
|
+
const filterValue = group.get('typeFilter')?.value;
|
|
1902
|
+
if (!filterValue)
|
|
1903
|
+
return [];
|
|
1904
|
+
return {
|
|
1905
|
+
arrange,
|
|
1906
|
+
filter: {
|
|
1907
|
+
property: selectedFilter.property,
|
|
1908
|
+
filtering: filterValue,
|
|
1909
|
+
},
|
|
1910
|
+
dateFilter: undefined,
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
if (arrange === 'filterByDate') {
|
|
1914
|
+
const initial = group.get('initialDate')?.value;
|
|
1915
|
+
const final = group.get('finalDate')?.value;
|
|
1916
|
+
if (initial && final) {
|
|
1917
|
+
try {
|
|
1918
|
+
const [dayI, monthI, yearI] = initial.split('/');
|
|
1919
|
+
const initialDate = new Date(`${monthI}/${dayI}/${yearI}`);
|
|
1920
|
+
const [dayF, monthF, yearF] = final.split('/');
|
|
1921
|
+
const finalDate = new Date(`${monthF}/${dayF}/${yearF}`);
|
|
1922
|
+
finalDate.setHours(23, 59, 59);
|
|
1923
|
+
return {
|
|
1924
|
+
arrange,
|
|
1925
|
+
filter: undefined,
|
|
1926
|
+
dateFilter: {
|
|
1927
|
+
initial: initialDate,
|
|
1928
|
+
final: finalDate,
|
|
1929
|
+
},
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
catch (error) {
|
|
1933
|
+
return [];
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return [];
|
|
1937
|
+
}
|
|
1938
|
+
if (selectedFilter.hasOwnProperty('items') && arrange === 'equals') {
|
|
1939
|
+
const selectedItems = group.get('selectItem')?.value;
|
|
1940
|
+
if (Array.isArray(selectedItems) && selectedItems.length > 0) {
|
|
1941
|
+
return selectedItems.map((item) => ({
|
|
1942
|
+
arrange,
|
|
1943
|
+
filter: {
|
|
1944
|
+
property: item.property,
|
|
1945
|
+
filtering: item.value,
|
|
1946
|
+
},
|
|
1947
|
+
dateFilter: undefined,
|
|
1948
|
+
}));
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
return [];
|
|
1952
|
+
})
|
|
1953
|
+
.filter((f) => f && (f.filter?.filtering !== undefined || f.dateFilter));
|
|
1954
|
+
return {
|
|
1955
|
+
filters: activeFilters,
|
|
1956
|
+
sortBy: this.sortBy,
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
// Filtro de data
|
|
1960
|
+
async search(event) {
|
|
1961
|
+
// Prevenir comportamento padrão do formulário ao pressionar Enter
|
|
1962
|
+
if (event) {
|
|
1963
|
+
event.preventDefault();
|
|
1964
|
+
event.stopPropagation();
|
|
1965
|
+
}
|
|
1966
|
+
if (this.selectSort.value) {
|
|
1967
|
+
if (this.selectSort.value.arrange === 'ascending') {
|
|
1968
|
+
this.sortBy = {
|
|
1969
|
+
field: this.selectSort.value.property,
|
|
1970
|
+
order: 'asc',
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
if (this.selectSort.value.arrange === 'descending') {
|
|
1974
|
+
this.sortBy = {
|
|
1975
|
+
field: this.selectSort.value.property,
|
|
1976
|
+
order: 'desc',
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
// Sem paginação: aplicar filtros client-side
|
|
1981
|
+
if (this.data.pagination === false) {
|
|
1982
|
+
// Atualizar arrange com os filtros ativos
|
|
1983
|
+
this.arrange = this.buildArrangeFromFilters();
|
|
1984
|
+
this.applyFiltersToDataSource();
|
|
1985
|
+
this.currentArrange =
|
|
1986
|
+
this.filtersForm.length > 0
|
|
1987
|
+
? this.filtersForm.at(0).get('selectFilter')?.value?.arrange ?? ''
|
|
1988
|
+
: '';
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
// Com paginação: comportamento original
|
|
1992
|
+
await this.loadItemsPaginated('reload', true);
|
|
1993
|
+
this.currentArrange =
|
|
1994
|
+
this.filtersForm.length > 0
|
|
1995
|
+
? this.filtersForm.at(0).get('selectFilter')?.value?.arrange ?? ''
|
|
1996
|
+
: '';
|
|
1997
|
+
this.paginator.firstPage();
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
async resetFilter() {
|
|
2001
|
+
this.dataSource.filter = '';
|
|
2002
|
+
if (this.filterPredicate) {
|
|
2003
|
+
this.dataSource.filterPredicate = this.filterPredicate;
|
|
2004
|
+
}
|
|
2005
|
+
this.filtersForm.clear();
|
|
2006
|
+
this.addFilter();
|
|
2007
|
+
this.selectSort.patchValue('');
|
|
2008
|
+
this.sortBy = {
|
|
2009
|
+
order: this.data.sortBy ? this.data.sortBy.order : 'desc',
|
|
2010
|
+
field: this.data.sortBy ? this.data.sortBy.field : 'createdAt',
|
|
2011
|
+
};
|
|
2012
|
+
// Sem paginação: recarregar itens originais
|
|
2013
|
+
if (this.data.pagination === false) {
|
|
2014
|
+
// Resetar arrange para valores padrão
|
|
2015
|
+
this.arrange = {
|
|
2016
|
+
filters: [],
|
|
2017
|
+
sortBy: this.sortBy,
|
|
2018
|
+
};
|
|
2019
|
+
this.dataSource.data = [...this.items];
|
|
2020
|
+
this.totalItems = this.items.length;
|
|
2021
|
+
this.currentArrange = '';
|
|
2022
|
+
}
|
|
2023
|
+
else {
|
|
2024
|
+
// Com paginação: comportamento original
|
|
2025
|
+
await this.loadItemsPaginated('reload', true);
|
|
2026
|
+
this.currentArrange = '';
|
|
2027
|
+
this.paginator.firstPage();
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
// Método público para recarregar a tabela
|
|
2031
|
+
async reloadTable() {
|
|
2032
|
+
if (this.data.pagination) {
|
|
2033
|
+
await this.loadItemsPaginated('reload', true);
|
|
2034
|
+
this.paginator.firstPage();
|
|
2035
|
+
}
|
|
2036
|
+
else {
|
|
2037
|
+
await this.loadItems();
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
updateDisplayedColumns() {
|
|
2041
|
+
if (this.dataSource) {
|
|
2042
|
+
this.dataSource = new MatTableDataSource([]);
|
|
2043
|
+
}
|
|
2044
|
+
this.columnProperties = this.data.displayedColumns.map((column) => {
|
|
2045
|
+
return column.property;
|
|
2046
|
+
});
|
|
2047
|
+
this.dropdownItems = [];
|
|
2048
|
+
this.sortableDropdownItems = [];
|
|
2049
|
+
this.data.displayedColumns.forEach((col) => {
|
|
2050
|
+
if (col.isFilterable) {
|
|
2051
|
+
this.dropdownItems.push({
|
|
2052
|
+
...col,
|
|
2053
|
+
arrange: 'filter',
|
|
2054
|
+
title: col.title,
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
if (col.isSortable) {
|
|
2058
|
+
this.sortableDropdownItems.push({
|
|
2059
|
+
...col,
|
|
2060
|
+
arrange: 'ascending',
|
|
2061
|
+
title: col.title + ': crescente',
|
|
2062
|
+
});
|
|
2063
|
+
this.sortableDropdownItems.push({
|
|
2064
|
+
...col,
|
|
2065
|
+
arrange: 'descending',
|
|
2066
|
+
title: col.title + ': decrescente',
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
if (col.isFilterableByDate) {
|
|
2070
|
+
this.dropdownItems.push({
|
|
2071
|
+
...col,
|
|
2072
|
+
arrange: 'filterByDate',
|
|
2073
|
+
title: col.title + ': filtro por data',
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
if (this.data.filterableOptions &&
|
|
2078
|
+
Array.isArray(this.data.filterableOptions)) {
|
|
2079
|
+
this.data.filterableOptions.forEach((option) => this.dropdownItems.push({ ...option, arrange: 'equals' }));
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
isString(value) {
|
|
2083
|
+
return typeof value === 'string';
|
|
2084
|
+
}
|
|
2085
|
+
// Métodos para controle do tooltip
|
|
2086
|
+
onCellMouseEnter(event, row, col) {
|
|
2087
|
+
// Só mostrar tooltip se a coluna tiver charLimit definido
|
|
2088
|
+
if (!col.charLimit) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const fullValue = this.getDisplayValue(col, row, true);
|
|
2092
|
+
// Só mostrar tooltip se o valor completo for maior que o limite
|
|
2093
|
+
if (fullValue.length <= col.charLimit) {
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
this.hoveredCell = { row, col };
|
|
2097
|
+
this.tooltipContent = fullValue;
|
|
2098
|
+
// Definir posição do tooltip
|
|
2099
|
+
this.tooltipPosition = {
|
|
2100
|
+
x: event.clientX + 10,
|
|
2101
|
+
y: event.clientY - 10,
|
|
2102
|
+
};
|
|
2103
|
+
// Timeout para mostrar o tooltip
|
|
2104
|
+
this.tooltipTimeout = setTimeout(() => {
|
|
2105
|
+
if (this.hoveredCell &&
|
|
2106
|
+
this.hoveredCell.row === row &&
|
|
2107
|
+
this.hoveredCell.col === col) {
|
|
2108
|
+
this.showTooltip = true;
|
|
2109
|
+
}
|
|
2110
|
+
}, 500);
|
|
2111
|
+
}
|
|
2112
|
+
onCellMouseLeave() {
|
|
2113
|
+
if (this.tooltipTimeout) {
|
|
2114
|
+
clearTimeout(this.tooltipTimeout);
|
|
2115
|
+
this.tooltipTimeout = null;
|
|
2116
|
+
}
|
|
2117
|
+
this.showTooltip = false;
|
|
2118
|
+
this.hoveredCell = null;
|
|
2119
|
+
this.tooltipContent = '';
|
|
2120
|
+
}
|
|
2121
|
+
onCellMouseMove(event) {
|
|
2122
|
+
if (this.showTooltip) {
|
|
2123
|
+
this.tooltipPosition = {
|
|
2124
|
+
x: event.clientX + 10,
|
|
2125
|
+
y: event.clientY - 10,
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
// Métodos para inversão vertical dos tabs
|
|
2130
|
+
getTabGroups(tabs) {
|
|
2131
|
+
if (!tabs || tabs.length === 0)
|
|
2132
|
+
return [];
|
|
2133
|
+
const totalGroups = Math.ceil(tabs.length / 6);
|
|
2134
|
+
const groups = [];
|
|
2135
|
+
// Criar array de índices invertidos (último grupo primeiro)
|
|
2136
|
+
for (let i = totalGroups - 1; i >= 0; i--) {
|
|
2137
|
+
groups.push(i);
|
|
2138
|
+
}
|
|
2139
|
+
return groups;
|
|
2140
|
+
}
|
|
2141
|
+
getTabGroup(tabs, groupIndex) {
|
|
2142
|
+
if (!tabs || tabs.length === 0)
|
|
2143
|
+
return [];
|
|
2144
|
+
const startIndex = groupIndex * 6;
|
|
2145
|
+
const endIndex = Math.min(startIndex + 6, tabs.length);
|
|
2146
|
+
return tabs.slice(startIndex, endIndex);
|
|
2147
|
+
}
|
|
2148
|
+
getRealTabIndex(groupIndex, tabIndexInGroup) {
|
|
2149
|
+
if (!this.data.tabs?.tabsData)
|
|
2150
|
+
return 0;
|
|
2151
|
+
const totalGroups = Math.ceil(this.data.tabs.tabsData.length / 6);
|
|
2152
|
+
const realGroupIndex = totalGroups - 1 - groupIndex;
|
|
2153
|
+
return realGroupIndex * 6 + tabIndexInGroup;
|
|
2154
|
+
}
|
|
2155
|
+
onTableSelected(i, j) {
|
|
2156
|
+
if (!this.data.tabs?.tabsData || !this.data.tabs.method)
|
|
2157
|
+
return;
|
|
2158
|
+
this.selectedTab = this.getRealTabIndex(i, j);
|
|
2159
|
+
const tab = this.data.tabs.tabsData[this.selectedTab];
|
|
2160
|
+
if (tab) {
|
|
2161
|
+
this.data.tabs.method(tab, this.selectedTab);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
isTabSelected(originalIndex) {
|
|
2165
|
+
return this.selectedTab === originalIndex;
|
|
2166
|
+
}
|
|
2167
|
+
shouldShowActionButton() {
|
|
2168
|
+
if (!this.data?.actionButton) {
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
if (!this.data.actionButton.condition) {
|
|
2172
|
+
return true;
|
|
2173
|
+
}
|
|
2174
|
+
try {
|
|
2175
|
+
return this.data.actionButton.condition(null) ?? true;
|
|
2176
|
+
}
|
|
2177
|
+
catch {
|
|
2178
|
+
return true;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
// Método para lidar com download (diferente para paginado e não paginado)
|
|
2182
|
+
handleDownload() {
|
|
2183
|
+
if (!this.downloadTable)
|
|
2184
|
+
return;
|
|
2185
|
+
// Se não há paginação, usar dados filtrados do dataSource
|
|
2186
|
+
if (this.data.pagination === false) {
|
|
2187
|
+
// Atualizar filteredItems com os dados filtrados do dataSource
|
|
2188
|
+
// (que já inclui filtro de texto aplicado via filterPredicate)
|
|
2189
|
+
if (this.dataSource && this.dataSource.filteredData) {
|
|
2190
|
+
this.filteredItems = [...this.dataSource.filteredData];
|
|
2191
|
+
}
|
|
2192
|
+
// Construir arrange com os filtros ativos (se houver)
|
|
2193
|
+
const arrange = this.buildArrangeFromFilters();
|
|
2194
|
+
this.downloadTable(arrange, this.data.conditions || []);
|
|
2195
|
+
}
|
|
2196
|
+
else {
|
|
2197
|
+
// Modo paginado: usar arrange existente (comportamento original)
|
|
2198
|
+
if (this.arrange) {
|
|
2199
|
+
this.downloadTable(this.arrange, this.data.conditions || []);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
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 });
|
|
2205
|
+
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 getAvailableFilterOptions()\" [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($event)\"\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($event)\"\n formControlName=\"initialDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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($event)\"\n matInput\n formControlName=\"finalDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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 && hasFilterableColumn === true\"\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($event)\"\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 <!-- FILTERS PANEL (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 && dropdownItems.length > 0\"\n >\n <!-- FILTERS CONTENT -->\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 getAvailableFilterOptions()\" [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($event)\"\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($event)\"\n (blur)=\"onDateFilterChange()\"\n formControlName=\"initialDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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($event)\"\n (blur)=\"onDateFilterChange()\"\n matInput\n formControlName=\"finalDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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 & Actions -->\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 <!-- Action Buttons -->\n <div class=\"flex flex-wrap gap-3\">\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 Aplicar\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)=\"handleDownload()\"\n >\n <i class=\"fa fa-download\"></i>\n Exportar\n </button>\n </div>\n </div>\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"] }, { kind: "directive", type: i16.NgxMaskDirective, selector: "input[mask], textarea[mask]", inputs: ["mask", "specialCharacters", "patterns", "prefix", "suffix", "thousandSeparator", "decimalMarker", "dropSpecialCharacters", "hiddenInput", "showMaskTyped", "placeHolderCharacter", "shownMaskExpression", "showTemplate", "clearIfNotMatch", "validation", "separatorLimit", "allowNegativeNumbers", "leadZeroDateTime", "leadZero", "triggerOnMaskChange", "apm", "inputTransformFn", "outputTransformFn", "keepCharacterPositions"], outputs: ["maskFilled"], exportAs: ["mask", "ngxMask"] }] });
|
|
2206
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TableComponent, decorators: [{
|
|
2207
|
+
type: Component,
|
|
2208
|
+
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 getAvailableFilterOptions()\" [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($event)\"\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($event)\"\n formControlName=\"initialDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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($event)\"\n matInput\n formControlName=\"finalDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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 && hasFilterableColumn === true\"\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($event)\"\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 <!-- FILTERS PANEL (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 && dropdownItems.length > 0\"\n >\n <!-- FILTERS CONTENT -->\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 getAvailableFilterOptions()\" [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($event)\"\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($event)\"\n (blur)=\"onDateFilterChange()\"\n formControlName=\"initialDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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($event)\"\n (blur)=\"onDateFilterChange()\"\n matInput\n formControlName=\"finalDate\"\n [dropSpecialCharacters]=\"false\"\n mask=\"d0/M0/0000\"\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 & Actions -->\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 <!-- Action Buttons -->\n <div class=\"flex flex-wrap gap-3\">\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 Aplicar\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)=\"handleDownload()\"\n >\n <i class=\"fa fa-download\"></i>\n Exportar\n </button>\n </div>\n </div>\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"] }]
|
|
2209
|
+
}], ctorParameters: function () { return [{ type: i1$1.Router }, { type: TableService }, { type: i1.AngularFirestore }]; }, propDecorators: { data: [{
|
|
2210
|
+
type: Input
|
|
2211
|
+
}], downloadTable: [{
|
|
2212
|
+
type: Input
|
|
2213
|
+
}], paginator: [{
|
|
2214
|
+
type: ViewChild,
|
|
2215
|
+
args: [MatPaginator]
|
|
2216
|
+
}], sort: [{
|
|
2217
|
+
type: ViewChild,
|
|
2218
|
+
args: [MatSort]
|
|
2147
2219
|
}] } });
|
|
2148
2220
|
|
|
2149
|
-
class NgFirebaseTableKxpModule {
|
|
2150
|
-
}
|
|
2151
|
-
NgFirebaseTableKxpModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
2152
|
-
NgFirebaseTableKxpModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, declarations: [NgFirebaseTableKxpComponent, TableComponent], imports: [CommonModule,
|
|
2153
|
-
ReactiveFormsModule,
|
|
2154
|
-
FormsModule,
|
|
2155
|
-
RouterModule,
|
|
2156
|
-
MatTableModule,
|
|
2157
|
-
MatPaginatorModule,
|
|
2158
|
-
MatSortModule,
|
|
2159
|
-
MatFormFieldModule,
|
|
2160
|
-
MatInputModule,
|
|
2161
|
-
MatSelectModule,
|
|
2162
|
-
MatTooltipModule,
|
|
2163
|
-
MatProgressSpinnerModule,
|
|
2164
|
-
MatIconModule,
|
|
2165
|
-
MatDialogModule,
|
|
2166
|
-
NgxMaskDirective,
|
|
2167
|
-
NgxMaskPipe], exports: [NgFirebaseTableKxpComponent, TableComponent] });
|
|
2168
|
-
NgFirebaseTableKxpModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, providers: [provideNgxMask()], imports: [CommonModule,
|
|
2169
|
-
ReactiveFormsModule,
|
|
2170
|
-
FormsModule,
|
|
2171
|
-
RouterModule,
|
|
2172
|
-
MatTableModule,
|
|
2173
|
-
MatPaginatorModule,
|
|
2174
|
-
MatSortModule,
|
|
2175
|
-
MatFormFieldModule,
|
|
2176
|
-
MatInputModule,
|
|
2177
|
-
MatSelectModule,
|
|
2178
|
-
MatTooltipModule,
|
|
2179
|
-
MatProgressSpinnerModule,
|
|
2180
|
-
MatIconModule,
|
|
2181
|
-
MatDialogModule] });
|
|
2182
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, decorators: [{
|
|
2183
|
-
type: NgModule,
|
|
2184
|
-
args: [{
|
|
2185
|
-
declarations: [NgFirebaseTableKxpComponent, TableComponent],
|
|
2186
|
-
imports: [
|
|
2187
|
-
CommonModule,
|
|
2188
|
-
ReactiveFormsModule,
|
|
2189
|
-
FormsModule,
|
|
2190
|
-
RouterModule,
|
|
2191
|
-
MatTableModule,
|
|
2192
|
-
MatPaginatorModule,
|
|
2193
|
-
MatSortModule,
|
|
2194
|
-
MatFormFieldModule,
|
|
2195
|
-
MatInputModule,
|
|
2196
|
-
MatSelectModule,
|
|
2197
|
-
MatTooltipModule,
|
|
2198
|
-
MatProgressSpinnerModule,
|
|
2199
|
-
MatIconModule,
|
|
2200
|
-
MatDialogModule,
|
|
2201
|
-
NgxMaskDirective,
|
|
2202
|
-
NgxMaskPipe,
|
|
2203
|
-
],
|
|
2204
|
-
exports: [NgFirebaseTableKxpComponent, TableComponent],
|
|
2205
|
-
providers: [provideNgxMask()],
|
|
2206
|
-
}]
|
|
2221
|
+
class NgFirebaseTableKxpModule {
|
|
2222
|
+
}
|
|
2223
|
+
NgFirebaseTableKxpModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
2224
|
+
NgFirebaseTableKxpModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, declarations: [NgFirebaseTableKxpComponent, TableComponent], imports: [CommonModule,
|
|
2225
|
+
ReactiveFormsModule,
|
|
2226
|
+
FormsModule,
|
|
2227
|
+
RouterModule,
|
|
2228
|
+
MatTableModule,
|
|
2229
|
+
MatPaginatorModule,
|
|
2230
|
+
MatSortModule,
|
|
2231
|
+
MatFormFieldModule,
|
|
2232
|
+
MatInputModule,
|
|
2233
|
+
MatSelectModule,
|
|
2234
|
+
MatTooltipModule,
|
|
2235
|
+
MatProgressSpinnerModule,
|
|
2236
|
+
MatIconModule,
|
|
2237
|
+
MatDialogModule,
|
|
2238
|
+
NgxMaskDirective,
|
|
2239
|
+
NgxMaskPipe], exports: [NgFirebaseTableKxpComponent, TableComponent] });
|
|
2240
|
+
NgFirebaseTableKxpModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, providers: [provideNgxMask()], imports: [CommonModule,
|
|
2241
|
+
ReactiveFormsModule,
|
|
2242
|
+
FormsModule,
|
|
2243
|
+
RouterModule,
|
|
2244
|
+
MatTableModule,
|
|
2245
|
+
MatPaginatorModule,
|
|
2246
|
+
MatSortModule,
|
|
2247
|
+
MatFormFieldModule,
|
|
2248
|
+
MatInputModule,
|
|
2249
|
+
MatSelectModule,
|
|
2250
|
+
MatTooltipModule,
|
|
2251
|
+
MatProgressSpinnerModule,
|
|
2252
|
+
MatIconModule,
|
|
2253
|
+
MatDialogModule] });
|
|
2254
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpModule, decorators: [{
|
|
2255
|
+
type: NgModule,
|
|
2256
|
+
args: [{
|
|
2257
|
+
declarations: [NgFirebaseTableKxpComponent, TableComponent],
|
|
2258
|
+
imports: [
|
|
2259
|
+
CommonModule,
|
|
2260
|
+
ReactiveFormsModule,
|
|
2261
|
+
FormsModule,
|
|
2262
|
+
RouterModule,
|
|
2263
|
+
MatTableModule,
|
|
2264
|
+
MatPaginatorModule,
|
|
2265
|
+
MatSortModule,
|
|
2266
|
+
MatFormFieldModule,
|
|
2267
|
+
MatInputModule,
|
|
2268
|
+
MatSelectModule,
|
|
2269
|
+
MatTooltipModule,
|
|
2270
|
+
MatProgressSpinnerModule,
|
|
2271
|
+
MatIconModule,
|
|
2272
|
+
MatDialogModule,
|
|
2273
|
+
NgxMaskDirective,
|
|
2274
|
+
NgxMaskPipe,
|
|
2275
|
+
],
|
|
2276
|
+
exports: [NgFirebaseTableKxpComponent, TableComponent],
|
|
2277
|
+
providers: [provideNgxMask()],
|
|
2278
|
+
}]
|
|
2207
2279
|
}] });
|
|
2208
2280
|
|
|
2209
|
-
class NgFirebaseTableKxpService {
|
|
2210
|
-
constructor() { }
|
|
2211
|
-
}
|
|
2212
|
-
NgFirebaseTableKxpService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2213
|
-
NgFirebaseTableKxpService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, providedIn: 'root' });
|
|
2214
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, decorators: [{
|
|
2215
|
-
type: Injectable,
|
|
2216
|
-
args: [{
|
|
2217
|
-
providedIn: 'root',
|
|
2218
|
-
}]
|
|
2281
|
+
class NgFirebaseTableKxpService {
|
|
2282
|
+
constructor() { }
|
|
2283
|
+
}
|
|
2284
|
+
NgFirebaseTableKxpService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2285
|
+
NgFirebaseTableKxpService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, providedIn: 'root' });
|
|
2286
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgFirebaseTableKxpService, decorators: [{
|
|
2287
|
+
type: Injectable,
|
|
2288
|
+
args: [{
|
|
2289
|
+
providedIn: 'root',
|
|
2290
|
+
}]
|
|
2219
2291
|
}], ctorParameters: function () { return []; } });
|
|
2220
2292
|
|
|
2221
|
-
/*
|
|
2222
|
-
* Public API Surface of ng-firebase-table-kxp
|
|
2223
|
-
*/
|
|
2293
|
+
/*
|
|
2294
|
+
* Public API Surface of ng-firebase-table-kxp
|
|
2295
|
+
*/
|
|
2224
2296
|
// Main module
|
|
2225
2297
|
|
|
2226
|
-
/**
|
|
2227
|
-
* Generated bundle index. Do not edit.
|
|
2298
|
+
/**
|
|
2299
|
+
* Generated bundle index. Do not edit.
|
|
2228
2300
|
*/
|
|
2229
2301
|
|
|
2230
2302
|
export { NgFirebaseTableKxpComponent, NgFirebaseTableKxpModule, NgFirebaseTableKxpService, TableComponent, TableService };
|