Branch Produção
This commit is contained in:
parent
751d965e8f
commit
8729ffddbb
|
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="notifications-body custom-scroll">
|
<div class="notifications-body custom-scroll">
|
||||||
<div class="notifications-state" *ngIf="notificationsLoading">
|
<div class="notifications-state loading" *ngIf="notificationsLoading">
|
||||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||||
<span>Carregando...</span>
|
<span>Carregando...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -57,20 +57,27 @@
|
||||||
<p>Tudo limpo por aqui!</p>
|
<p>Tudo limpo por aqui!</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="notifications-state info" *ngIf="!notificationsLoading && !notificationsError && hasNotificationsTruncated">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<span class="notifications-truncate-copy">
|
||||||
|
Mostrando <strong>{{ notificationsPreviewLimit }}</strong> de <strong>{{ notifications.length }}</strong> notificações
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="notification-item"
|
class="notification-item"
|
||||||
*ngFor="let n of notifications"
|
*ngFor="let n of notificationsPreview; trackBy: trackByNotificationId"
|
||||||
[class.unread]="!n.lida"
|
[class.unread]="!n.lida"
|
||||||
(click)="markNotificationRead(n)"
|
(click)="markNotificationRead(n)"
|
||||||
>
|
>
|
||||||
<div class="notif-icon-area">
|
<div class="notif-icon-area">
|
||||||
<div class="icon-circle"
|
<div class="icon-circle"
|
||||||
[class.danger]="n.tipo === 'Vencido'"
|
[class.danger]="getNotificationTipo(n) === 'Vencido'"
|
||||||
[class.warn]="n.tipo === 'AVencer'">
|
[class.warn]="getNotificationTipo(n) === 'AVencer'">
|
||||||
<i class="bi"
|
<i class="bi"
|
||||||
[class.bi-x-lg]="n.tipo === 'Vencido'"
|
[class.bi-x-lg]="getNotificationTipo(n) === 'Vencido'"
|
||||||
[class.bi-clock-history]="n.tipo === 'AVencer'"
|
[class.bi-clock-history]="getNotificationTipo(n) === 'AVencer'"
|
||||||
[class.bi-info-circle]="n.tipo !== 'Vencido' && n.tipo !== 'AVencer'"></i>
|
[class.bi-info-circle]="getNotificationTipo(n) !== 'Vencido' && getNotificationTipo(n) !== 'AVencer'"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -84,7 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="notif-desc">
|
<p class="notif-desc">
|
||||||
<span class="notif-verb">{{ getVigenciaLabel(n) }}:</span>
|
<span class="notif-verb">{{ getVigenciaLabel(n) }}:</span>
|
||||||
<strong class="notif-date-strong" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
|
<strong class="notif-date-strong" [class.danger]="getNotificationTipo(n) === 'Vencido'" [class.warn]="getNotificationTipo(n) === 'AVencer'">
|
||||||
{{ getVigenciaDate(n) }}
|
{{ getVigenciaDate(n) }}
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -399,7 +406,7 @@
|
||||||
<div class="toast-header">
|
<div class="toast-header">
|
||||||
<i class="bi bi-exclamation-circle-fill text-warning me-2"></i>
|
<i class="bi bi-exclamation-circle-fill text-warning me-2"></i>
|
||||||
<strong class="me-auto">Atenção à Vigência</strong>
|
<strong class="me-auto">Atenção à Vigência</strong>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
<button type="button" class="btn-close" (click)="acknowledgeCurrentToast()" data-bs-dismiss="toast"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-body" *ngIf="toastNotification as toastItem">
|
<div class="toast-body" *ngIf="toastNotification as toastItem">
|
||||||
A linha <strong>{{ toastItem.linha }}</strong> vence em breve.
|
A linha <strong>{{ toastItem.linha }}</strong> vence em breve.
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,72 @@ $border-color: #e5e7eb;
|
||||||
.notifications-head { padding: 16px; border-bottom: 1px solid $border-color; display: flex; justify-content: space-between; align-items: center; .head-title { font-weight: 700; font-size: 14px; display: flex; align-items: center; gap: 6px; } .see-all { font-size: 12px; color: $primary; text-decoration: none; font-weight: 600; } }
|
.notifications-head { padding: 16px; border-bottom: 1px solid $border-color; display: flex; justify-content: space-between; align-items: center; .head-title { font-weight: 700; font-size: 14px; display: flex; align-items: center; gap: 6px; } .see-all { font-size: 12px; color: $primary; text-decoration: none; font-weight: 600; } }
|
||||||
.notifications-body { max-height: 360px; overflow-y: auto; }
|
.notifications-body { max-height: 360px; overflow-y: auto; }
|
||||||
.notifications-empty { padding: 32px; text-align: center; color: $text-muted; .empty-icon { font-size: 24px; margin-bottom: 8px; } }
|
.notifications-empty { padding: 32px; text-align: center; color: $text-muted; .empty-icon { font-size: 24px; margin-bottom: 8px; } }
|
||||||
|
.notifications-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px 16px 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(28, 56, 201, 0.14);
|
||||||
|
background: rgba(28, 56, 201, 0.06);
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
background: #eff6ff;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
color: #1e3a8a;
|
||||||
|
|
||||||
|
.notifications-truncate-copy {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e3a8a;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #93c5fd;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warn {
|
||||||
|
background: #fff7ed;
|
||||||
|
border-color: #fed7aa;
|
||||||
|
color: #9a3412;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.notification-item {
|
.notification-item {
|
||||||
display: flex; gap: 12px; padding: 12px 16px; border-bottom: 1px solid $border-color; cursor: pointer;
|
display: flex; gap: 12px; padding: 12px 16px; border-bottom: 1px solid $border-color; cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, HostListener, Inject, ElementRef, ViewChild } from '@angular/core';
|
import { Component, HostListener, Inject, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
|
||||||
import { RouterLink, Router, NavigationEnd } from '@angular/router';
|
import { RouterLink, Router, NavigationEnd } from '@angular/router';
|
||||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||||
import { PLATFORM_ID } from '@angular/core';
|
import { PLATFORM_ID } from '@angular/core';
|
||||||
|
|
@ -17,7 +17,7 @@ import { CustomSelectComponent } from '../custom-select/custom-select';
|
||||||
templateUrl: './header.html',
|
templateUrl: './header.html',
|
||||||
styleUrls: ['./header.scss'],
|
styleUrls: ['./header.scss'],
|
||||||
})
|
})
|
||||||
export class Header {
|
export class Header implements AfterViewInit {
|
||||||
isScrolled = false;
|
isScrolled = false;
|
||||||
|
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
|
|
@ -32,6 +32,10 @@ export class Header {
|
||||||
notificationsLoading = false;
|
notificationsLoading = false;
|
||||||
notificationsError = false;
|
notificationsError = false;
|
||||||
private notificationsLoaded = false;
|
private notificationsLoaded = false;
|
||||||
|
private pendingToastCheck = false;
|
||||||
|
private lastNotificationsLoadAt = 0;
|
||||||
|
private readonly notificationsRefreshMs = 60_000;
|
||||||
|
readonly notificationsPreviewLimit = 40;
|
||||||
@ViewChild('notifToast') notifToast?: ElementRef;
|
@ViewChild('notifToast') notifToast?: ElementRef;
|
||||||
|
|
||||||
createUserForm: FormGroup;
|
createUserForm: FormGroup;
|
||||||
|
|
@ -195,7 +199,15 @@ export class Header {
|
||||||
this.notificationsOpen = !this.notificationsOpen;
|
this.notificationsOpen = !this.notificationsOpen;
|
||||||
if (this.notificationsOpen) {
|
if (this.notificationsOpen) {
|
||||||
this.optionsOpen = false;
|
this.optionsOpen = false;
|
||||||
this.loadNotifications();
|
if (!this.notificationsLoaded) {
|
||||||
|
this.loadNotifications(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - this.lastNotificationsLoadAt > this.notificationsRefreshMs) {
|
||||||
|
// Atualiza em background para não bloquear a abertura visual do dropdown.
|
||||||
|
setTimeout(() => this.loadNotifications(false), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,7 +226,7 @@ export class Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
getVigenciaLabel(notification: NotificationDto): string {
|
getVigenciaLabel(notification: NotificationDto): string {
|
||||||
return notification.tipo === 'Vencido' ? 'Venceu em' : 'Vence em';
|
return this.getNotificationTipo(notification) === 'Vencido' ? 'Venceu em' : 'Vence em';
|
||||||
}
|
}
|
||||||
|
|
||||||
getVigenciaDate(notification: NotificationDto): string {
|
getVigenciaDate(notification: NotificationDto): string {
|
||||||
|
|
@ -223,7 +235,30 @@ export class Header {
|
||||||
notification.referenciaData ??
|
notification.referenciaData ??
|
||||||
notification.data;
|
notification.data;
|
||||||
if (!raw) return '-';
|
if (!raw) return '-';
|
||||||
return new Date(raw).toLocaleDateString('pt-BR');
|
const parsed = this.parseDateOnly(raw);
|
||||||
|
if (!parsed) return '-';
|
||||||
|
return parsed.toLocaleDateString('pt-BR');
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotificationTipo(notification: NotificationDto): 'Vencido' | 'AVencer' {
|
||||||
|
const reference = notification.dtTerminoFidelizacao ?? notification.referenciaData;
|
||||||
|
const parsed = this.parseDateOnly(reference);
|
||||||
|
if (!parsed) return notification.tipo;
|
||||||
|
|
||||||
|
const today = this.startOfDay(new Date());
|
||||||
|
return parsed < today ? 'Vencido' : 'AVencer';
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotificationDaysToExpire(notification: NotificationDto): number | null {
|
||||||
|
const reference = notification.dtTerminoFidelizacao ?? notification.referenciaData;
|
||||||
|
const parsed = this.parseDateOnly(reference);
|
||||||
|
if (!parsed) {
|
||||||
|
return notification.diasParaVencer ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = this.startOfDay(new Date());
|
||||||
|
const msPerDay = 24 * 60 * 60 * 1000;
|
||||||
|
return Math.round((parsed.getTime() - today.getTime()) / msPerDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
abbreviateName(value?: string | null): string {
|
abbreviateName(value?: string | null): string {
|
||||||
|
|
@ -251,6 +286,18 @@ export class Header {
|
||||||
return this.notifications.filter(n => !n.lida).length;
|
return this.notifications.filter(n => !n.lida).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notificationsPreview() {
|
||||||
|
return this.notifications.slice(0, this.notificationsPreviewLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasNotificationsTruncated() {
|
||||||
|
return this.notifications.length > this.notificationsPreviewLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByNotificationId(_: number, notification: NotificationDto) {
|
||||||
|
return notification.id;
|
||||||
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.authService.logout();
|
this.authService.logout();
|
||||||
this.optionsOpen = false;
|
this.optionsOpen = false;
|
||||||
|
|
@ -288,13 +335,27 @@ export class Header {
|
||||||
localStorage.setItem('vigenciaAcknowledgedIds', JSON.stringify(Array.from(acknowledged)));
|
localStorage.setItem('vigenciaAcknowledgedIds', JSON.stringify(Array.from(acknowledged)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureNotificationsLoaded() {
|
acknowledgeCurrentToast() {
|
||||||
if (this.notificationsLoaded || this.notificationsLoading) return;
|
const current = this.toastNotification;
|
||||||
this.loadNotifications();
|
if (!current) return;
|
||||||
|
this.acknowledgeNotification(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadNotifications() {
|
ngAfterViewInit() {
|
||||||
|
if (this.pendingToastCheck) {
|
||||||
|
this.pendingToastCheck = false;
|
||||||
|
void this.maybeShowVigenciaToast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureNotificationsLoaded() {
|
||||||
|
if (this.notificationsLoaded || this.notificationsLoading) return;
|
||||||
|
this.loadNotifications(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadNotifications(showToast = true) {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
if (this.notificationsLoading) return;
|
||||||
this.notificationsLoading = true;
|
this.notificationsLoading = true;
|
||||||
this.notificationsError = false;
|
this.notificationsError = false;
|
||||||
this.notificationsService.list().subscribe({
|
this.notificationsService.list().subscribe({
|
||||||
|
|
@ -302,7 +363,10 @@ export class Header {
|
||||||
this.notifications = data || [];
|
this.notifications = data || [];
|
||||||
this.notificationsLoaded = true;
|
this.notificationsLoaded = true;
|
||||||
this.notificationsLoading = false;
|
this.notificationsLoading = false;
|
||||||
this.maybeShowVigenciaToast();
|
this.lastNotificationsLoadAt = Date.now();
|
||||||
|
if (showToast) {
|
||||||
|
void this.maybeShowVigenciaToast();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.notificationsError = true;
|
this.notificationsError = true;
|
||||||
|
|
@ -312,12 +376,17 @@ export class Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async maybeShowVigenciaToast() {
|
private async maybeShowVigenciaToast() {
|
||||||
if (!this.notifToast || !isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
if (!this.notifToast) {
|
||||||
|
this.pendingToastCheck = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pending = this.getPendingVigenciaToast();
|
const pending = this.getPendingVigenciaToast();
|
||||||
if (!pending) return;
|
if (!pending) return;
|
||||||
|
|
||||||
const bs = await import('bootstrap');
|
const bs = await import('bootstrap');
|
||||||
const toast = new bs.Toast(this.notifToast.nativeElement, { autohide: false });
|
const toast = bs.Toast.getOrCreateInstance(this.notifToast.nativeElement, { autohide: false });
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,10 +397,35 @@ export class Header {
|
||||||
private getPendingVigenciaToast() {
|
private getPendingVigenciaToast() {
|
||||||
const acknowledged = this.getAcknowledgedIds();
|
const acknowledged = this.getAcknowledgedIds();
|
||||||
return this.notifications.find(
|
return this.notifications.find(
|
||||||
n => n.tipo === 'AVencer' && n.diasParaVencer === 5 && !acknowledged.has(n.id)
|
n =>
|
||||||
|
this.getNotificationTipo(n) === 'AVencer' &&
|
||||||
|
this.getNotificationDaysToExpire(n) === 5 &&
|
||||||
|
!acknowledged.has(n.id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseDateOnly(raw?: string | null): Date | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
const datePart = raw.split('T')[0];
|
||||||
|
const parts = datePart.split('-');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const year = Number(parts[0]);
|
||||||
|
const month = Number(parts[1]);
|
||||||
|
const day = Number(parts[2]);
|
||||||
|
if (Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day)) {
|
||||||
|
return new Date(year, month - 1, day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = new Date(raw);
|
||||||
|
if (Number.isNaN(fallback.getTime())) return null;
|
||||||
|
return this.startOfDay(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startOfDay(date: Date): Date {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
private getAcknowledgedIds() {
|
private getAcknowledgedIds() {
|
||||||
if (!isPlatformBrowser(this.platformId)) return new Set<string>();
|
if (!isPlatformBrowser(this.platformId)) return new Set<string>();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,13 @@ type InsightsKpisAdicionais = {
|
||||||
totalLinesWithNoPaidAdditional?: number | null;
|
totalLinesWithNoPaidAdditional?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type InsightsLineTotal = {
|
||||||
|
tipo?: string | null;
|
||||||
|
qtdLinhas?: number | null;
|
||||||
|
valorTotalLine?: number | null;
|
||||||
|
lucroTotalLine?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
type DashboardGeralInsightsDto = {
|
type DashboardGeralInsightsDto = {
|
||||||
kpis?: {
|
kpis?: {
|
||||||
totalLinhas?: number | null;
|
totalLinhas?: number | null;
|
||||||
|
|
@ -136,6 +143,7 @@ type DashboardGeralInsightsDto = {
|
||||||
vivo?: InsightsKpisVivo | null;
|
vivo?: InsightsKpisVivo | null;
|
||||||
travelMundo?: InsightsKpisTravel | null;
|
travelMundo?: InsightsKpisTravel | null;
|
||||||
adicionais?: InsightsKpisAdicionais | null;
|
adicionais?: InsightsKpisAdicionais | null;
|
||||||
|
totaisLine?: InsightsLineTotal[] | null;
|
||||||
} | null;
|
} | null;
|
||||||
charts?: {
|
charts?: {
|
||||||
linhasPorFranquia?: InsightsChartSeries | null;
|
linhasPorFranquia?: InsightsChartSeries | null;
|
||||||
|
|
@ -507,6 +515,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const vivoRaw = this.readNode(kpisRaw, 'vivo', 'Vivo') ?? {};
|
const vivoRaw = this.readNode(kpisRaw, 'vivo', 'Vivo') ?? {};
|
||||||
const travelRaw = this.readNode(kpisRaw, 'travelMundo', 'TravelMundo') ?? {};
|
const travelRaw = this.readNode(kpisRaw, 'travelMundo', 'TravelMundo') ?? {};
|
||||||
const adicionaisRaw = this.readNode(kpisRaw, 'adicionais', 'Adicionais') ?? {};
|
const adicionaisRaw = this.readNode(kpisRaw, 'adicionais', 'Adicionais') ?? {};
|
||||||
|
const totaisLineRaw = this.readNode(kpisRaw, 'totaisLine', 'TotaisLine');
|
||||||
const chartsRaw = this.readNode(raw, 'charts', 'Charts') ?? {};
|
const chartsRaw = this.readNode(raw, 'charts', 'Charts') ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -532,6 +541,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
totalLinesWithAnyPaidAdditional: this.toNumberOrNull(this.readNode(adicionaisRaw, 'totalLinesWithAnyPaidAdditional', 'TotalLinesWithAnyPaidAdditional')),
|
totalLinesWithAnyPaidAdditional: this.toNumberOrNull(this.readNode(adicionaisRaw, 'totalLinesWithAnyPaidAdditional', 'TotalLinesWithAnyPaidAdditional')),
|
||||||
totalLinesWithNoPaidAdditional: this.toNumberOrNull(this.readNode(adicionaisRaw, 'totalLinesWithNoPaidAdditional', 'TotalLinesWithNoPaidAdditional')),
|
totalLinesWithNoPaidAdditional: this.toNumberOrNull(this.readNode(adicionaisRaw, 'totalLinesWithNoPaidAdditional', 'TotalLinesWithNoPaidAdditional')),
|
||||||
},
|
},
|
||||||
|
totaisLine: this.normalizeLineTotals(totaisLineRaw),
|
||||||
},
|
},
|
||||||
charts: {
|
charts: {
|
||||||
linhasPorFranquia: this.normalizeChartSeries(this.readNode(chartsRaw, 'linhasPorFranquia', 'LinhasPorFranquia')),
|
linhasPorFranquia: this.normalizeChartSeries(this.readNode(chartsRaw, 'linhasPorFranquia', 'LinhasPorFranquia')),
|
||||||
|
|
@ -559,6 +569,19 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeLineTotals(rawTotals: any): InsightsLineTotal[] {
|
||||||
|
if (!Array.isArray(rawTotals)) return [];
|
||||||
|
|
||||||
|
return rawTotals
|
||||||
|
.map((row: any) => ({
|
||||||
|
tipo: String(this.readNode(row, 'tipo', 'Tipo') ?? '').trim() || null,
|
||||||
|
qtdLinhas: this.toNumberOrNull(this.readNode(row, 'qtdLinhas', 'QtdLinhas')),
|
||||||
|
valorTotalLine: this.toNumberOrNull(this.readNode(row, 'valorTotalLine', 'ValorTotalLine')),
|
||||||
|
lucroTotalLine: this.toNumberOrNull(this.readNode(row, 'lucroTotalLine', 'LucroTotalLine')),
|
||||||
|
}))
|
||||||
|
.filter((row) => !!row.tipo);
|
||||||
|
}
|
||||||
|
|
||||||
private readNode(source: any, ...keys: string[]): any {
|
private readNode(source: any, ...keys: string[]): any {
|
||||||
if (!source || typeof source !== 'object') return undefined;
|
if (!source || typeof source !== 'object') return undefined;
|
||||||
|
|
||||||
|
|
@ -585,7 +608,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineTotals = Array.isArray(this.resumo.lineTotais) ? this.resumo.lineTotais : [];
|
const lineTotals = this.getEffectiveLineTotals();
|
||||||
const pf = this.findLineTotal(lineTotals, ['PF', 'PESSOA FISICA']);
|
const pf = this.findLineTotal(lineTotals, ['PF', 'PESSOA FISICA']);
|
||||||
const pj = this.findLineTotal(lineTotals, ['PJ', 'PESSOA JURIDICA']);
|
const pj = this.findLineTotal(lineTotals, ['PJ', 'PESSOA JURIDICA']);
|
||||||
const diferenca = this.findLineTotal(lineTotals, ['DIFERENCA PJ X PF', 'DIFERENÇA PJ X PF', 'DIFERENCA']);
|
const diferenca = this.findLineTotal(lineTotals, ['DIFERENCA PJ X PF', 'DIFERENÇA PJ X PF', 'DIFERENCA']);
|
||||||
|
|
@ -686,6 +709,20 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getEffectiveLineTotals(): LineTotal[] {
|
||||||
|
const fromInsights = (this.insights?.kpis?.totaisLine ?? [])
|
||||||
|
.map((row) => ({
|
||||||
|
tipo: row.tipo ?? null,
|
||||||
|
qtdLinhas: row.qtdLinhas ?? null,
|
||||||
|
valorTotalLine: row.valorTotalLine ?? null,
|
||||||
|
lucroTotalLine: row.lucroTotalLine ?? null,
|
||||||
|
}))
|
||||||
|
.filter((row) => !!(row.tipo ?? '').toString().trim());
|
||||||
|
|
||||||
|
if (fromInsights.length) return fromInsights;
|
||||||
|
return Array.isArray(this.resumo?.lineTotais) ? (this.resumo?.lineTotais ?? []) : [];
|
||||||
|
}
|
||||||
|
|
||||||
private clearInsightsData() {
|
private clearInsightsData() {
|
||||||
this.insights = null;
|
this.insights = null;
|
||||||
this.franquiaLabels = [];
|
this.franquiaLabels = [];
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@
|
||||||
class="list-item"
|
class="list-item"
|
||||||
*ngFor="let n of filteredNotifications"
|
*ngFor="let n of filteredNotifications"
|
||||||
[class.is-read]="n.lida"
|
[class.is-read]="n.lida"
|
||||||
[class.is-danger]="n.tipo === 'Vencido'"
|
[class.is-danger]="getNotificationTipo(n) === 'Vencido'"
|
||||||
[class.is-warning]="n.tipo === 'AVencer'"
|
[class.is-warning]="getNotificationTipo(n) === 'AVencer'"
|
||||||
>
|
>
|
||||||
<div class="status-strip"></div>
|
<div class="status-strip"></div>
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="item-icon">
|
<div class="item-icon">
|
||||||
<i class="bi" [class.bi-x-circle-fill]="n.tipo === 'Vencido'" [class.bi-clock-fill]="n.tipo === 'AVencer'"></i>
|
<i class="bi" [class.bi-x-circle-fill]="getNotificationTipo(n) === 'Vencido'" [class.bi-clock-fill]="getNotificationTipo(n) === 'AVencer'"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
|
|
@ -124,8 +124,8 @@
|
||||||
<span class="meta-value">{{ n.planoContrato || '-' }}</span>
|
<span class="meta-value">{{ n.planoContrato || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<span class="badge-tag" [class.danger]="n.tipo === 'Vencido'" [class.warn]="n.tipo === 'AVencer'">
|
<span class="badge-tag" [class.danger]="getNotificationTipo(n) === 'Vencido'" [class.warn]="getNotificationTipo(n) === 'AVencer'">
|
||||||
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }}
|
{{ getNotificationTipo(n) === 'Vencido' ? 'Vencido' : 'A vencer' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@ export class Notificacoes implements OnInit {
|
||||||
|
|
||||||
get filteredNotifications() {
|
get filteredNotifications() {
|
||||||
if (this.filter === 'vencidas') {
|
if (this.filter === 'vencidas') {
|
||||||
return this.notifications.filter(n => n.tipo === 'Vencido');
|
return this.notifications.filter(n => this.getNotificationTipo(n) === 'Vencido');
|
||||||
}
|
}
|
||||||
if (this.filter === 'aVencer') {
|
if (this.filter === 'aVencer') {
|
||||||
return this.notifications.filter(n => n.tipo === 'AVencer');
|
return this.notifications.filter(n => this.getNotificationTipo(n) === 'AVencer');
|
||||||
}
|
}
|
||||||
if (this.filter === 'lidas') {
|
if (this.filter === 'lidas') {
|
||||||
return this.notifications.filter(n => n.lida);
|
return this.notifications.filter(n => n.lida);
|
||||||
|
|
@ -55,7 +55,18 @@ export class Notificacoes implements OnInit {
|
||||||
|
|
||||||
formatDateLabel(date?: string | null): string {
|
formatDateLabel(date?: string | null): string {
|
||||||
if (!date) return '-';
|
if (!date) return '-';
|
||||||
return new Date(date).toLocaleDateString('pt-BR');
|
const parsed = this.parseDateOnly(date);
|
||||||
|
if (!parsed) return '-';
|
||||||
|
return parsed.toLocaleDateString('pt-BR');
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotificationTipo(notification: NotificationDto): 'Vencido' | 'AVencer' {
|
||||||
|
const reference = notification.dtTerminoFidelizacao ?? notification.referenciaData;
|
||||||
|
const parsed = this.parseDateOnly(reference);
|
||||||
|
if (!parsed) return notification.tipo;
|
||||||
|
|
||||||
|
const today = this.startOfDay(new Date());
|
||||||
|
return parsed < today ? 'Vencido' : 'AVencer';
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadNotifications() {
|
private loadNotifications() {
|
||||||
|
|
@ -74,20 +85,23 @@ export class Notificacoes implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
countByType(tipo: 'Vencido' | 'AVencer'): number {
|
countByType(tipo: 'Vencido' | 'AVencer'): number {
|
||||||
return this.notifications.filter(n => n.tipo === tipo && !n.lida).length;
|
return this.notifications.filter(n => this.getNotificationTipo(n) === tipo && !n.lida).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
markAllAsRead() {
|
markAllAsRead() {
|
||||||
if (this.filter === 'lidas' || this.bulkLoading) return;
|
if (this.filter === 'lidas' || this.bulkLoading) return;
|
||||||
this.bulkLoading = true;
|
this.bulkLoading = true;
|
||||||
|
|
||||||
const filterParam = this.getFilterParam();
|
const selectedIds = Array.from(this.selectedIds);
|
||||||
const ids = Array.from(this.selectedIds);
|
const scopedIds = selectedIds.length
|
||||||
this.notificationsService.markAllAsRead(filterParam, ids.length ? ids : undefined).subscribe({
|
? selectedIds
|
||||||
|
: (this.filter !== 'todas' ? this.filteredNotifications.map(n => n.id) : []);
|
||||||
|
const filterParam = scopedIds.length ? undefined : this.getFilterParam();
|
||||||
|
this.notificationsService.markAllAsRead(filterParam, scopedIds.length ? scopedIds : undefined).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
this.notifications = this.notifications.map((n) => {
|
this.notifications = this.notifications.map((n) => {
|
||||||
if (ids.length ? ids.includes(n.id) : this.shouldMarkRead(n)) {
|
if (scopedIds.length ? scopedIds.includes(n.id) : this.shouldMarkRead(n)) {
|
||||||
return { ...n, lida: true, lidaEm: now };
|
return { ...n, lida: true, lidaEm: now };
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
|
|
@ -105,9 +119,12 @@ export class Notificacoes implements OnInit {
|
||||||
if (this.filter === 'lidas' || this.exportLoading) return;
|
if (this.filter === 'lidas' || this.exportLoading) return;
|
||||||
this.exportLoading = true;
|
this.exportLoading = true;
|
||||||
|
|
||||||
const filterParam = this.getFilterParam();
|
const selectedIds = Array.from(this.selectedIds);
|
||||||
const ids = Array.from(this.selectedIds);
|
const scopedIds = selectedIds.length
|
||||||
this.notificationsService.export(filterParam, ids.length ? ids : undefined).subscribe({
|
? selectedIds
|
||||||
|
: (this.filter !== 'todas' ? this.filteredNotifications.map(n => n.id) : []);
|
||||||
|
const filterParam = scopedIds.length ? undefined : this.getFilterParam();
|
||||||
|
this.notificationsService.export(filterParam, scopedIds.length ? scopedIds : undefined).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
const blob = res.body;
|
const blob = res.body;
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
|
|
@ -172,11 +189,33 @@ export class Notificacoes implements OnInit {
|
||||||
|
|
||||||
private shouldMarkRead(n: NotificationDto): boolean {
|
private shouldMarkRead(n: NotificationDto): boolean {
|
||||||
if (this.filter === 'todas') return true;
|
if (this.filter === 'todas') return true;
|
||||||
if (this.filter === 'aVencer') return n.tipo === 'AVencer';
|
if (this.filter === 'aVencer') return this.getNotificationTipo(n) === 'AVencer';
|
||||||
if (this.filter === 'vencidas') return n.tipo === 'Vencido';
|
if (this.filter === 'vencidas') return this.getNotificationTipo(n) === 'Vencido';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseDateOnly(raw?: string | null): Date | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
const datePart = raw.split('T')[0];
|
||||||
|
const parts = datePart.split('-');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const year = Number(parts[0]);
|
||||||
|
const month = Number(parts[1]);
|
||||||
|
const day = Number(parts[2]);
|
||||||
|
if (Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day)) {
|
||||||
|
return new Date(year, month - 1, day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = new Date(raw);
|
||||||
|
if (Number.isNaN(fallback.getTime())) return null;
|
||||||
|
return this.startOfDay(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startOfDay(date: Date): Date {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
private extractFilename(contentDisposition: string | null): string | null {
|
private extractFilename(contentDisposition: string | null): string | null {
|
||||||
if (!contentDisposition) return null;
|
if (!contentDisposition) return null;
|
||||||
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="kpi-card">
|
<div class="kpi-card">
|
||||||
<span class="kpi-lbl">Lucro Consolidado</span>
|
<span class="kpi-lbl">Lucro Consolidado</span>
|
||||||
<strong class="kpi-val text-success">{{ formatMoney(clientesTotals?.lucro) }}</strong>
|
<strong class="kpi-val text-success">{{ formatMoney(totaisLineLucroConsolidado) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
HostBinding
|
HostBinding
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import Chart from 'chart.js/auto';
|
import Chart from 'chart.js/auto';
|
||||||
import type { ChartConfiguration, ChartData, ScriptableContext, TooltipItem } from 'chart.js';
|
import type { ChartConfiguration, ChartData, ScriptableContext, TooltipItem } from 'chart.js';
|
||||||
|
|
@ -30,6 +31,7 @@ import {
|
||||||
ReservaPorDdd,
|
ReservaPorDdd,
|
||||||
ReservaTotal
|
ReservaTotal
|
||||||
} from '../../services/resumo.service';
|
} from '../../services/resumo.service';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva';
|
type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva';
|
||||||
|
|
||||||
|
|
@ -66,6 +68,7 @@ type MacrophonyGroup = { key: string; plano: string; gbLabel: string; totalLinha
|
||||||
type GroupMetric = { label: string; value: string; tone?: string };
|
type GroupMetric = { label: string; value: string; tone?: string };
|
||||||
type GroupItem<T> = { key: string; title: string; subtitle?: string; rows: T[]; metrics: GroupMetric[]; };
|
type GroupItem<T> = { key: string; title: string; subtitle?: string; rows: T[]; metrics: GroupMetric[]; };
|
||||||
type GroupedTableState<T> = { key: string; label: string; table: TableState<T>; groupBy: (row: T) => string; groupTitle: (rows: T[]) => string; groupSubtitle?: (rows: T[]) => string | undefined; groupMetrics: (rows: T[]) => GroupMetric[]; groupSort?: (a: GroupItem<T>, b: GroupItem<T>) => number; search: string; page: number; pageSize: number; pageSizeOptions: number[]; compact: boolean; open: Set<string>; groups: GroupItem<T>[]; filtered: GroupItem<T>[]; view: GroupItem<T>[]; detailOpen: boolean; detailGroup: GroupItem<T> | null; };
|
type GroupedTableState<T> = { key: string; label: string; table: TableState<T>; groupBy: (row: T) => string; groupTitle: (rows: T[]) => string; groupSubtitle?: (rows: T[]) => string | undefined; groupMetrics: (rows: T[]) => GroupMetric[]; groupSort?: (a: GroupItem<T>, b: GroupItem<T>) => number; search: string; page: number; pageSize: number; pageSizeOptions: number[]; compact: boolean; open: Set<string>; groups: GroupItem<T>[]; filtered: GroupItem<T>[]; view: GroupItem<T>[]; detailOpen: boolean; detailGroup: GroupItem<T> | null; };
|
||||||
|
type GeralLineTotalPayload = { tipo?: unknown; qtdLinhas?: unknown; valorTotalLine?: unknown; lucroTotalLine?: unknown; };
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
|
@ -97,6 +100,8 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private charts: { [key: string]: Chart | undefined } = {};
|
private charts: { [key: string]: Chart | undefined } = {};
|
||||||
private viewReady = false;
|
private viewReady = false;
|
||||||
private dataReady = false;
|
private dataReady = false;
|
||||||
|
private readonly baseApi: string;
|
||||||
|
private totaisLineFromGeral: LineTotal[] = [];
|
||||||
|
|
||||||
// Estados de Tabela e Grupo
|
// Estados de Tabela e Grupo
|
||||||
macrophonyGroups: MacrophonyGroup[] = [];
|
macrophonyGroups: MacrophonyGroup[] = [];
|
||||||
|
|
@ -126,11 +131,14 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(PLATFORM_ID) private platformId: object,
|
@Inject(PLATFORM_ID) private platformId: object,
|
||||||
|
private http: HttpClient,
|
||||||
private resumoService: ResumoService,
|
private resumoService: ResumoService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cdr: ChangeDetectorRef
|
private cdr: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
this.initTables();
|
this.initTables();
|
||||||
this.initGroupTables();
|
this.initGroupTables();
|
||||||
// Default chart configuration for Enterprise look
|
// Default chart configuration for Enterprise look
|
||||||
|
|
@ -269,6 +277,16 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
.sort((a, b) => b.lucro - a.lucro)
|
.sort((a, b) => b.lucro - a.lucro)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
|
// Garante altura suficiente para exibir todos os nomes no eixo Y sem auto-skip.
|
||||||
|
const parent = canvas.parentElement as HTMLElement | null;
|
||||||
|
if (parent) {
|
||||||
|
const minHeight = 300;
|
||||||
|
const rowHeight = 34;
|
||||||
|
parent.style.height = `${Math.max(minHeight, data.length * rowHeight)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const common = this.getCommonChartOptions('currency') as any;
|
||||||
|
|
||||||
this.charts['clientes'] = new Chart(canvas, {
|
this.charts['clientes'] = new Chart(canvas, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -282,8 +300,25 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
...this.getCommonChartOptions('currency'),
|
...common,
|
||||||
indexAxis: 'y'
|
indexAxis: 'y',
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
...(common.scales?.x ?? {}),
|
||||||
|
ticks: {
|
||||||
|
...((common.scales?.x as any)?.ticks ?? {}),
|
||||||
|
maxTicksLimit: 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
...(common.scales?.y ?? {}),
|
||||||
|
ticks: {
|
||||||
|
...((common.scales?.y as any)?.ticks ?? {}),
|
||||||
|
autoSkip: false,
|
||||||
|
maxTicksLimit: data.length || 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -407,12 +442,15 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
this.dataReady = false;
|
this.dataReady = false;
|
||||||
|
this.totaisLineFromGeral = [];
|
||||||
|
|
||||||
this.resumoService.getResumo().subscribe({
|
this.resumoService.getResumo().subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
this.resumo = data ? this.normalizeResumo(data) : null;
|
this.resumo = data ? this.normalizeResumo(data) : null;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.dataReady = true;
|
this.dataReady = true;
|
||||||
this.bindTables();
|
this.bindTables();
|
||||||
|
this.loadTotaisLineFromGeral();
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
this.tryBuildCharts();
|
this.tryBuildCharts();
|
||||||
},
|
},
|
||||||
|
|
@ -424,6 +462,46 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadTotaisLineFromGeral(): void {
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
|
this.http.get<any>(`${this.baseApi}/dashboard/geral/insights`).subscribe({
|
||||||
|
next: (dto) => {
|
||||||
|
const rows = this.extractTotaisLineFromInsights(dto);
|
||||||
|
if (!rows.length) return;
|
||||||
|
|
||||||
|
this.totaisLineFromGeral = rows;
|
||||||
|
this.bindTables();
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
this.tryBuildCharts();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
// Mantem fallback para a tabela de Resumo quando insights falhar.
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTotaisLineFromInsights(dto: any): LineTotal[] {
|
||||||
|
const kpis = dto?.kpis ?? dto?.Kpis;
|
||||||
|
const rawRows = kpis?.totaisLine ?? kpis?.TotaisLine;
|
||||||
|
if (!Array.isArray(rawRows)) return [];
|
||||||
|
|
||||||
|
return rawRows
|
||||||
|
.map((row: GeralLineTotalPayload) => ({
|
||||||
|
tipo: this.toText(row?.tipo),
|
||||||
|
qtdLinhas: this.toNumber(row?.qtdLinhas),
|
||||||
|
valorTotalLine: this.toNumber(row?.valorTotalLine),
|
||||||
|
lucroTotalLine: this.toNumber(row?.lucroTotalLine),
|
||||||
|
}))
|
||||||
|
.filter((row) => !!(row.tipo ?? '').toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private toText(value: unknown): string | null {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text ? text : null;
|
||||||
|
}
|
||||||
|
|
||||||
// Animação de entrada
|
// Animação de entrada
|
||||||
private animateIn(): void {
|
private animateIn(): void {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
@ -840,7 +918,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.tablePlanoContrato.data = this.buildPlanoContratoResumoConsolidado(resumo.planoContratoResumos ?? []);
|
this.tablePlanoContrato.data = this.buildPlanoContratoResumoConsolidado(resumo.planoContratoResumos ?? []);
|
||||||
this.tableClientes.data = resumo.vivoLineResumos ?? [];
|
this.tableClientes.data = resumo.vivoLineResumos ?? [];
|
||||||
this.tableClientesEspeciais.data = resumo.clienteEspeciais ?? [];
|
this.tableClientesEspeciais.data = resumo.clienteEspeciais ?? [];
|
||||||
this.tableTotaisLine.data = resumo.lineTotais ?? [];
|
this.tableTotaisLine.data = this.getEffectiveLineTotais();
|
||||||
this.tableReserva.data = resumo.reservaLines ?? [];
|
this.tableReserva.data = resumo.reservaLines ?? [];
|
||||||
|
|
||||||
this.updateMacrophonyView();
|
this.updateMacrophonyView();
|
||||||
|
|
@ -1148,13 +1226,27 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
exportMacrophonyCsv() { this.exportCsv(this.tableMacrophony, 'macrophony-planos'); }
|
exportMacrophonyCsv() { this.exportCsv(this.tableMacrophony, 'macrophony-planos'); }
|
||||||
findLineTotal(k: string[]): LineTotal | null {
|
findLineTotal(k: string[]): LineTotal | null {
|
||||||
const keys = k.map((item) => item.toUpperCase());
|
const keys = k.map((item) => item.toUpperCase());
|
||||||
const list = Array.isArray(this.resumo?.lineTotais) ? this.resumo?.lineTotais : [];
|
const list = this.getEffectiveLineTotais();
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
const tipo = (item?.tipo ?? '').toString().toUpperCase();
|
const tipo = (item?.tipo ?? '').toString().toUpperCase();
|
||||||
if (keys.some((key) => tipo.includes(key))) return item;
|
if (keys.some((key) => tipo.includes(key))) return item;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get totaisLineLucroConsolidado(): number {
|
||||||
|
const pf = this.toNumber(this.findLineTotal(['PF', 'PESSOA FISICA'])?.lucroTotalLine) ?? 0;
|
||||||
|
const pj = this.toNumber(this.findLineTotal(['PJ', 'PESSOA JURIDICA'])?.lucroTotalLine) ?? 0;
|
||||||
|
return pf + pj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEffectiveLineTotais(): LineTotal[] {
|
||||||
|
if (this.totaisLineFromGeral.length > 0) {
|
||||||
|
return this.totaisLineFromGeral;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(this.resumo?.lineTotais) ? (this.resumo?.lineTotais ?? []) : [];
|
||||||
|
}
|
||||||
|
|
||||||
get macrophonyPageStart() { return (this.macrophonyPage - 1) * this.macrophonyPageSize + 1; }
|
get macrophonyPageStart() { return (this.macrophonyPage - 1) * this.macrophonyPageSize + 1; }
|
||||||
get macrophonyPageEnd() { return Math.min(this.macrophonyPage * this.macrophonyPageSize, this.macrophonyFilteredGroups.length); }
|
get macrophonyPageEnd() { return Math.min(this.macrophonyPage * this.macrophonyPageSize, this.macrophonyFilteredGroups.length); }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue