From 49cdaefddfc0d5c12c0728be104a9c25d5a23de1 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Mon, 9 Feb 2026 16:30:43 -0300 Subject: [PATCH] =?UTF-8?q?Minha=20altera=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/logo.png | Bin 31218 -> 157594 bytes src/app/app-title.strategy.ts | 19 + src/app/app.config.ts | 9 +- src/app/app.routes.ts | 33 +- src/app/app.ts | 29 +- .../custom-select/custom-select.scss | 27 +- src/app/components/header/header.html | 36 +- src/app/components/header/header.scss | 17 +- src/app/components/header/header.ts | 47 +- src/app/guards/admin.guard.ts | 27 + src/app/guards/auth.guard.ts | 8 +- src/app/interceptors/auth.interceptor.ts | 5 +- src/app/interceptors/session.interceptor.ts | 25 + .../chips-controle-recebidos.html | 284 +++- .../chips-controle-recebidos.scss | 202 ++- .../chips-controle-recebidos.ts | 360 ++++- .../pages/dados-usuarios/dados-usuarios.html | 245 ++- .../pages/dados-usuarios/dados-usuarios.scss | 195 ++- .../pages/dados-usuarios/dados-usuarios.ts | 367 ++++- src/app/pages/dashboard/dashboard.html | 508 ++++--- src/app/pages/dashboard/dashboard.scss | 865 +++++++---- src/app/pages/dashboard/dashboard.ts | 1333 ++++++++++++++--- src/app/pages/faturamento/faturamento.html | 131 +- src/app/pages/faturamento/faturamento.scss | 169 ++- src/app/pages/faturamento/faturamento.ts | 156 +- src/app/pages/geral/geral.html | 197 ++- src/app/pages/geral/geral.scss | 127 +- src/app/pages/geral/geral.ts | 991 ++++++++++-- src/app/pages/historico/historico.html | 259 ++++ src/app/pages/historico/historico.scss | 679 +++++++++ src/app/pages/historico/historico.ts | 275 ++++ src/app/pages/login/login.html | 3 +- src/app/pages/login/login.ts | 20 +- src/app/pages/notificacoes/notificacoes.html | 84 +- src/app/pages/notificacoes/notificacoes.scss | 154 +- src/app/pages/notificacoes/notificacoes.ts | 126 +- src/app/pages/novo-usuario/novo-usuario.html | 2 +- .../parcelamento-create-modal.html | 151 ++ .../parcelamento-create-modal.scss | 389 +++++ .../parcelamento-create-modal.ts | 253 ++++ ...parcelamento-detalhamento-anual-modal.html | 56 + ...parcelamento-detalhamento-anual-modal.scss | 202 +++ .../parcelamento-detalhamento-anual-modal.ts | 41 + .../parcelamentos-filters.html | 74 + .../parcelamentos-filters.scss | 300 ++++ .../parcelamentos-filters.ts | 36 + .../parcelamentos-kpis.html | 14 + .../parcelamentos-kpis.scss | 57 + .../parcelamentos-kpis/parcelamentos-kpis.ts | 20 + .../parcelamentos-table.html | 161 ++ .../parcelamentos-table.scss | 499 ++++++ .../parcelamentos-table.ts | 66 + .../pages/parcelamentos/parcelamentos.html | 251 ++++ .../pages/parcelamentos/parcelamentos.scss | 614 ++++++++ src/app/pages/parcelamentos/parcelamentos.ts | 1091 ++++++++++++++ src/app/pages/perfil/perfil.html | 158 ++ src/app/pages/perfil/perfil.scss | 295 ++++ src/app/pages/perfil/perfil.ts | 229 +++ src/app/pages/register/register.ts | 2 +- src/app/pages/resumo/resumo.html | 615 ++++++++ src/app/pages/resumo/resumo.scss | 790 ++++++++++ src/app/pages/resumo/resumo.ts | 1120 ++++++++++++++ src/app/pages/troca-numero/troca-numero.html | 2 +- src/app/pages/troca-numero/troca-numero.scss | 2 +- src/app/pages/vigencia/vigencia.html | 330 +++- src/app/pages/vigencia/vigencia.scss | 489 +++++- src/app/pages/vigencia/vigencia.ts | 365 ++++- src/app/services/auth.service.ts | 161 +- src/app/services/billing.ts | 28 + src/app/services/chips-controle.service.ts | 54 + src/app/services/dados-usuarios.service.ts | 49 +- src/app/services/historico.service.ts | 77 + src/app/services/lines.service.ts | 2 + src/app/services/notifications.service.ts | 31 +- src/app/services/parcelamentos.service.ts | 119 ++ src/app/services/plan-autofill.service.ts | 116 ++ src/app/services/profile.service.ts | 44 + src/app/services/resumo.service.ts | 129 ++ src/app/services/session-notice.service.ts | 110 ++ src/app/services/vigencia.service.ts | 34 +- src/index.html | 6 +- src/main.server.ts | 4 + src/main.ts | 4 + src/styles.scss | 49 +- 84 files changed, 16546 insertions(+), 1157 deletions(-) create mode 100644 src/app/app-title.strategy.ts create mode 100644 src/app/guards/admin.guard.ts create mode 100644 src/app/interceptors/session.interceptor.ts create mode 100644 src/app/pages/historico/historico.html create mode 100644 src/app/pages/historico/historico.scss create mode 100644 src/app/pages/historico/historico.ts create mode 100644 src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.html create mode 100644 src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.scss create mode 100644 src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts create mode 100644 src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html create mode 100644 src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss create mode 100644 src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.html create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.scss create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.ts create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.html create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.scss create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.ts create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss create mode 100644 src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts create mode 100644 src/app/pages/parcelamentos/parcelamentos.html create mode 100644 src/app/pages/parcelamentos/parcelamentos.scss create mode 100644 src/app/pages/parcelamentos/parcelamentos.ts create mode 100644 src/app/pages/perfil/perfil.html create mode 100644 src/app/pages/perfil/perfil.scss create mode 100644 src/app/pages/perfil/perfil.ts create mode 100644 src/app/pages/resumo/resumo.html create mode 100644 src/app/pages/resumo/resumo.scss create mode 100644 src/app/pages/resumo/resumo.ts create mode 100644 src/app/services/historico.service.ts create mode 100644 src/app/services/parcelamentos.service.ts create mode 100644 src/app/services/plan-autofill.service.ts create mode 100644 src/app/services/profile.service.ts create mode 100644 src/app/services/resumo.service.ts create mode 100644 src/app/services/session-notice.service.ts diff --git a/public/logo.png b/public/logo.png index 54ca585bef67241e6b2d26dbfefd51fc888131ee..dc9fdf61f56818d619ff00cceeb64805dc33c2cf 100644 GIT binary patch literal 157594 zcmYg%bwHF|^ERQNq(Mq4pdcwA9ZQEO9ZE_lB`n>rgn)pApdivA9g-`tyC5aq9ZT2J zyGw3-@P5DNeV;$}pSjPOGv}IX&V8Sm*=QXtHA=DvWCR2Rlp5;FdISW7sO#S?k}FBy zBs_+I;0}R?vVwsx;V$-e&f_UVRx*&~gyg1{Y_Vy+&|49%kB<^R(i7bvO2FJ8@=GLC zCrUU>K74pP^p3@cKX0EXF;I5Beb#XFkovfwEu4?~PYPdOjf_Lh2aSezJwKwd_B|sZ z@Y6kWFRe_EgFWcNPD<0IQ6qHLsmh@WQtDV$DiyR&v^V)aXFxtZRE*e}WR@c1h|!+> zf)PYEce{?!US#+`&j^>s=}=}ST#Gec*&)jRr$9#ZTBx6VsxR$`fczu(HY1r0gRW}%NiYBZP1Z3=Fo_J)p=yuH zvt}jh7ynso>3P4r>9BTt@@VAv|3HZszczpIbN$8D_9{;Qt0I@8?k4u*=;VX?|7~@Q z_j)X3P>pGzWI+BO6YQ>#3k#min)SB+2g6^dV=KmV9yFXH;%_DX?Wm!D()2N73C zkzs$}m3Njt4_#>(I@6!l`j@K-oE3moVb^oDyhu#&*Ggq>U*ngYxaR%oUph;#vC_|6 zo&;UP|6dUFu9nXl9cKL(Dbg~pNRd6WSrrGp246ot^x9=2Pp(0+(K2MR|DXS+rLO%i zC0Mcwem z#5-%s+vxu=y3zv4_P*YggU^fEkbl{FWOs#BcXsV$-akC~54K#1*ISSuHA(�!{C( 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

&dV07 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 h00006Nklu+=;hq2h002ovPDHLkV1nl{Mj8MB diff --git a/src/app/app-title.strategy.ts b/src/app/app-title.strategy.ts new file mode 100644 index 0000000..8d2f179 --- /dev/null +++ b/src/app/app-title.strategy.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; + +@Injectable() +export class AppTitleStrategy extends TitleStrategy { + private readonly appName = 'LineGestão'; + + constructor(private readonly titleService: Title) { + super(); + } + + override updateTitle(routerState: RouterStateSnapshot): void { + const pageTitle = this.buildTitle(routerState); + this.titleService.setTitle( + pageTitle ? `${pageTitle} - ${this.appName}` : this.appName + ); + } +} diff --git a/src/app/app.config.ts b/src/app/app.config.ts index e4018c5..a91a2a6 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,26 +1,31 @@ import { ApplicationConfig, + LOCALE_ID, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { provideRouter, TitleStrategy } from '@angular/router'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; import { routes } from './app.routes'; import { authInterceptor } from './interceptors/auth.interceptor'; +import { sessionInterceptor } from './interceptors/session.interceptor'; +import { AppTitleStrategy } from './app-title.strategy'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideZoneChangeDetection({ eventCoalescing: true }), + { provide: LOCALE_ID, useValue: 'pt-BR' }, provideRouter(routes), + { provide: TitleStrategy, useClass: AppTitleStrategy }, provideClientHydration(withEventReplay()), // ✅ HttpClient com fetch + interceptor provideHttpClient( withFetch(), - withInterceptors([authInterceptor]) + withInterceptors([authInterceptor, sessionInterceptor]) ), ] }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index ea4c859..0333c55 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -8,6 +8,7 @@ import { Mureg } from './pages/mureg/mureg'; import { Faturamento } from './pages/faturamento/faturamento'; import { authGuard } from './guards/auth.guard'; +import { adminGuard } from './guards/admin.guard'; import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios'; import { VigenciaComponent } from './pages/vigencia/vigencia'; import { TrocaNumero } from './pages/troca-numero/troca-numero'; @@ -15,24 +16,32 @@ import { Dashboard } from './pages/dashboard/dashboard'; import { Notificacoes } from './pages/notificacoes/notificacoes'; import { NovoUsuario } from './pages/novo-usuario/novo-usuario'; import { ChipsControleRecebidos } from './pages/chips-controle-recebidos/chips-controle-recebidos'; +import { Resumo } from './pages/resumo/resumo'; +import { Parcelamentos } from './pages/parcelamentos/parcelamentos'; +import { Historico } from './pages/historico/historico'; +import { Perfil } from './pages/perfil/perfil'; export const routes: Routes = [ { path: '', component: Home }, - { path: 'register', component: Register }, - { path: 'login', component: LoginComponent }, + { path: 'register', component: Register, title: 'Cadastro' }, + { path: 'login', component: LoginComponent, title: 'Login' }, - { path: 'geral', component: Geral, canActivate: [authGuard] }, - { path: 'mureg', component: Mureg, canActivate: [authGuard] }, - { path: 'faturamento', component: Faturamento, canActivate: [authGuard] }, - { path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard] }, - { path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard] }, - { path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard] }, - { path: 'notificacoes', component: Notificacoes, canActivate: [authGuard] }, - { path: 'novo-usuario', component: NovoUsuario, canActivate: [authGuard] }, - { path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard] }, + { path: 'geral', component: Geral, canActivate: [authGuard], title: 'Geral' }, + { path: 'mureg', component: Mureg, canActivate: [authGuard], title: 'Mureg' }, + { path: 'faturamento', component: Faturamento, canActivate: [authGuard], title: 'Faturamento' }, + { path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard], title: 'Dados dos Usuários' }, + { path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' }, + { path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' }, + { path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' }, + { path: 'novo-usuario', component: NovoUsuario, canActivate: [authGuard], title: 'Novo Usuário' }, + { path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard], title: 'Chips Controle Recebidos' }, + { path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' }, + { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard], title: 'Parcelamentos' }, + { path: 'historico', component: Historico, canActivate: [authGuard, adminGuard], title: 'Histórico' }, + { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, // ✅ rota correta - { path: 'dashboard', component: Dashboard, canActivate: [authGuard] }, + { path: 'dashboard', component: Dashboard, canActivate: [authGuard], title: 'Dashboard' }, // ✅ compatibilidade: se alguém acessar /portal/dashboard, manda pra /dashboard { path: 'portal/dashboard', redirectTo: 'dashboard', pathMatch: 'full' }, diff --git a/src/app/app.ts b/src/app/app.ts index 3cea806..3ad4aab 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,10 +1,11 @@ // src/app/app.ts import { Component, Inject, PLATFORM_ID } from '@angular/core'; import { Router, NavigationEnd, RouterOutlet } from '@angular/router'; -import { CommonModule } from '@angular/common'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; import { Header } from './components/header/header'; import { FooterComponent } from './components/footer/footer'; +import { AuthService } from './services/auth.service'; @Component({ selector: 'app-root', @@ -36,10 +37,15 @@ export class AppComponent { '/dashboard', // ✅ ADICIONADO: esconde footer na página de dashboard '/notificacoes', '/chips-controle-recebidos', + '/resumo', + '/parcelamentos', + '/historico', + '/perfil', ]; constructor( private router: Router, + private authService: AuthService, @Inject(PLATFORM_ID) private platformId: object ) { this.router.events.subscribe((event) => { @@ -58,9 +64,30 @@ export class AppComponent { // ✅ footer some ao logar + também no login/register this.hideFooter = isLoggedRoute || this.isFullScreenPage; + + // Em SSR não existe storage do navegador. + if (!isPlatformBrowser(this.platformId)) return; + + if (isLoggedRoute && !this.hasValidSession()) { + this.router.navigateByUrl('/login'); + } } }); } + + private hasValidSession(): boolean { + const token = this.authService.token; + if (!token) return false; + + const payload = this.authService.getTokenPayload(); + const tenantId = payload?.['tenantId'] ?? payload?.['tenant'] ?? payload?.['TenantId']; + if (!tenantId) { + this.authService.logout(); + return false; + } + + return true; + } } // ✅ SSR espera importar { App } de './app/app' diff --git a/src/app/components/custom-select/custom-select.scss b/src/app/components/custom-select/custom-select.scss index b6fed55..acfa098 100644 --- a/src/app/components/custom-select/custom-select.scss +++ b/src/app/components/custom-select/custom-select.scss @@ -3,24 +3,39 @@ width: 100%; } +:host(.form-control), +:host(.form-select), +:host(.select-glass) { + /* Reset Bootstrap field skin on host to avoid duplicate "field behind" effect. */ + padding: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + background-image: none !important; + box-shadow: none !important; + height: auto !important; + min-height: 0 !important; +} + .app-select { position: relative; width: 100%; } .app-select-trigger { + position: relative; width: 100%; height: 42px; border-radius: 10px; border: 1.5px solid rgba(15, 23, 42, 0.12); - padding: 0 36px 0 12px; + padding: 0 28px 0 12px; background: #fff; color: #0f172a; font-size: 14px; font-weight: 500; display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; gap: 8px; cursor: pointer; transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; @@ -49,10 +64,12 @@ .app-select.sm .app-select-trigger { height: 36px; font-size: 13px; - padding-right: 32px; + padding-right: 24px; } .app-select-label { + flex: 1 1 auto; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -60,6 +77,10 @@ .app-select-trigger i { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); color: #64748b; font-size: 12px; } diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index 273f743..44eadb5 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -76,14 +76,27 @@

- {{ n.linha || 'Sem Linha' }} - {{ n.referenciaData ? (n.referenciaData | date:'dd/MM') : '' }} + + {{ n.linha || 'Sem Linha' }} + + {{ abbreviateName(n.cliente) }} +

- {{ n.tipo === 'Vencido' ? 'Venceu' : 'Vence em' }} - {{ n.cliente || 'Cliente não ident.' }} + {{ getVigenciaLabel(n) }}: + + {{ getVigenciaDate(n) }} +

-
- {{ n.usuario }} +
+
+ Usuário: + {{ abbreviateName(n.usuario) }} +
+
+ Conta: + {{ n.conta || '-' }} +
@@ -111,7 +124,7 @@
-
@@ -413,6 +426,9 @@ Dashboard + + Resumo + Geral @@ -422,8 +438,14 @@ Faturamento + + Parcelamentos + + + Histórico + - Dados de Usuários + Dados PF/PJ Vigência diff --git a/src/app/components/header/header.scss b/src/app/components/header/header.scss index 8d62ea0..704ab16 100644 --- a/src/app/components/header/header.scss +++ b/src/app/components/header/header.scss @@ -119,9 +119,20 @@ $border-color: #e5e7eb; &.warn { background-color: #fef3c7; color: #d97706; } } .notif-content { flex: 1; } - .notif-header { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 2px; } - .notif-date { font-size: 11px; color: $text-muted; } - .notif-desc { margin: 0; font-size: 12px; color: $text-muted; line-height: 1.3; } + .notif-header { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 4px; } + .notif-title-line { font-weight: 700; color: $text-main; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } + .notif-line { font-weight: 800; flex: 0 0 auto; } + .notif-sep { color: $text-muted; flex: 0 0 auto; } + .notif-client { font-weight: 600; color: $text-muted; max-width: 130px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; } + .notif-desc { margin: 2px 0 6px; font-size: 12px; color: $text-muted; line-height: 1.3; display: flex; align-items: center; gap: 4px; } + .notif-verb { font-weight: 600; color: $text-muted; } + .notif-date-strong { font-weight: 800; color: $text-main; } + .notif-date-strong.warn { color: #d97706; } + .notif-date-strong.danger { color: #dc2626; } + .notif-meta-lines { display: flex; flex-direction: column; gap: 4px; } + .notif-meta-line { display: flex; gap: 6px; font-size: 12px; color: $text-muted; } + .meta-label { font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px; } + .meta-value { font-weight: 600; color: $text-main; } } /* MODAIS GERAIS */ diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index 7825a8e..3b766ff 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -70,6 +70,10 @@ export class Header { '/notificacoes', '/novo-usuario', '/chips-controle-recebidos', + '/resumo', + '/parcelamentos', + '/historico', + '/perfil', ]; constructor( @@ -122,7 +126,9 @@ export class Header { } private syncHeaderState(rawUrl: string) { - const url = (rawUrl || '').split('?')[0].split('#')[0]; + let url = (rawUrl || '').split('?')[0].split('#')[0]; + if (url && !url.startsWith('/')) url = `/${url}`; + url = url.replace(/\/+$/, ''); this.isHome = (url === '/' || url === ''); @@ -156,6 +162,11 @@ export class Header { this.optionsOpen = false; } + goToProfile() { + this.closeOptions(); + this.router.navigate(['/perfil']); + } + openCreateUserModal() { if (!this.isAdmin) return; this.createUserOpen = true; @@ -203,6 +214,40 @@ export class Header { }); } + getVigenciaLabel(notification: NotificationDto): string { + return notification.tipo === 'Vencido' ? 'Venceu em' : 'Vence em'; + } + + getVigenciaDate(notification: NotificationDto): string { + const raw = + notification.dtTerminoFidelizacao ?? + notification.referenciaData ?? + notification.data; + if (!raw) return '-'; + return new Date(raw).toLocaleDateString('pt-BR'); + } + + abbreviateName(value?: string | null): string { + const name = (value ?? '').trim(); + if (!name) return '-'; + const parts = name.split(/\s+/).filter(Boolean); + if (parts.length === 1) return parts[0]; + + const maxLen = 18; + const full = parts.join(' '); + if (full.length <= maxLen) return full; + + if (parts.length >= 3) { + const candidate = `${parts[0]} ${parts[1]} ${parts[2][0]}.`; + if (candidate.length <= maxLen) return candidate; + return `${parts[0]} ${parts[1][0]}.`; + } + + const two = `${parts[0]} ${parts[1]}`; + if (two.length <= maxLen) return two; + return `${parts[0]} ${parts[1][0]}.`; + } + get unreadCount() { return this.notifications.filter(n => !n.lida).length; } diff --git a/src/app/guards/admin.guard.ts b/src/app/guards/admin.guard.ts new file mode 100644 index 0000000..19118f4 --- /dev/null +++ b/src/app/guards/admin.guard.ts @@ -0,0 +1,27 @@ +import { inject, PLATFORM_ID } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { isPlatformBrowser } from '@angular/common'; +import { AuthService } from '../services/auth.service'; + +export const adminGuard: CanActivateFn = () => { + const router = inject(Router); + const platformId = inject(PLATFORM_ID); + const authService = inject(AuthService); + + if (!isPlatformBrowser(platformId)) { + // Em SSR não há storage do usuário para validar sessão/perfil. + return true; + } + + const token = authService.token; + if (!token) { + return router.parseUrl('/login'); + } + + const isAdmin = authService.hasRole('admin'); + if (!isAdmin) { + return router.parseUrl('/dashboard'); + } + + return true; +}; diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts index ba3018f..faa10ed 100644 --- a/src/app/guards/auth.guard.ts +++ b/src/app/guards/auth.guard.ts @@ -10,10 +10,12 @@ export const authGuard: CanActivateFn = () => { // SSR: não existe localStorage. Bloqueia e manda pro login. if (!isPlatformBrowser(platformId)) { - return router.parseUrl('/login'); + // Em SSR não existe acesso ao storage do usuário. + // Deixa renderizar e valida no browser após hidratação. + return true; } - const token = localStorage.getItem('token'); + const token = authService.token; if (!token) { return router.parseUrl('/login'); @@ -22,7 +24,7 @@ export const authGuard: CanActivateFn = () => { const payload = authService.getTokenPayload(); const tenantId = payload?.['tenantId'] ?? payload?.['tenant'] ?? payload?.['TenantId']; if (!tenantId) { - localStorage.removeItem('token'); + authService.logout(); return router.parseUrl('/login'); } diff --git a/src/app/interceptors/auth.interceptor.ts b/src/app/interceptors/auth.interceptor.ts index f45079f..69eb54a 100644 --- a/src/app/interceptors/auth.interceptor.ts +++ b/src/app/interceptors/auth.interceptor.ts @@ -1,10 +1,13 @@ import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { AuthService } from '../services/auth.service'; export const authInterceptor: HttpInterceptorFn = (req, next) => { // ✅ SSR-safe if (typeof window === 'undefined') return next(req); - const token = localStorage.getItem('token'); + const authService = inject(AuthService); + const token = authService.token; if (!token) return next(req); return next( diff --git a/src/app/interceptors/session.interceptor.ts b/src/app/interceptors/session.interceptor.ts new file mode 100644 index 0000000..9d58e1b --- /dev/null +++ b/src/app/interceptors/session.interceptor.ts @@ -0,0 +1,25 @@ +import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'; +import { inject, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { catchError, throwError } from 'rxjs'; +import { SessionNoticeService } from '../services/session-notice.service'; + +export const sessionInterceptor: HttpInterceptorFn = (req, next) => { + const platformId = inject(PLATFORM_ID); + if (!isPlatformBrowser(platformId)) { + return next(req); + } + + const sessionNotice = inject(SessionNoticeService); + + return next(req).pipe( + catchError((err: HttpErrorResponse) => { + if (err?.status === 401) { + sessionNotice.handleUnauthorized(); + } else if (err?.status === 403) { + sessionNotice.handleForbidden(); + } + return throwError(() => err); + }) + ); +}; diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html index 092ce92..d65894f 100644 --- a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html @@ -34,7 +34,22 @@ Importação e acompanhamento
-
+
+ + +
@@ -80,14 +95,14 @@ @@ -169,7 +184,7 @@ ITEM NÚMERO DO CHIP OBSERVAÇÕES - AÇÕES + AÇÕES @@ -182,6 +197,12 @@ + +
@@ -258,7 +279,7 @@ QTD. CONTEÚDO DA NF DATA DO RECEBIMENTO - AÇÕES + AÇÕES @@ -274,6 +295,12 @@ + + @@ -295,7 +322,7 @@ NÚMERO DA LINHA VALOR UNIT. VALOR DA NF - AÇÕES + AÇÕES @@ -312,6 +339,12 @@ + + @@ -351,7 +384,7 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss index b2ee8cf..0194b05 100644 --- a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss @@ -135,6 +135,7 @@ text-align: center; .title-badge { justify-self: center; margin-bottom: 8px; } + .header-actions { justify-self: center; } } } @@ -173,6 +174,22 @@ } .subtitle { color: rgba(17, 18, 20, 0.65); font-weight: 700; } +.header-actions { + justify-self: end; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + white-space: nowrap; + } +} /* ========================================================== */ /* TABS E FILTROS */ @@ -213,9 +230,9 @@ /* Pesquisa */ .search-group { - max-width: 300px; + max-width: 270px; border-radius: 12px; - overflow-y: auto; + overflow: hidden; display: flex; align-items: stretch; background: #fff; @@ -261,6 +278,37 @@ &:hover { background: #fff; border-color: var(--blue); } } +.btn-brand { + background-color: var(--brand); + border-color: var(--brand); + color: #fff; + font-weight: 900; + border-radius: 12px; + transition: transform 0.2s, box-shadow 0.2s; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); + filter: brightness(1.05); + } +} + +.btn-glass { + border-radius: 12px; + font-weight: 900; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(3, 15, 170, 0.24); + color: var(--blue); + transition: all 0.2s ease; + + &:hover { + background: #fff; + border-color: var(--brand); + color: var(--brand); + transform: translateY(-1px); + } +} + /* ========================================================== */ /* BODY (scroll interno igual Mureg) */ /* ========================================================== */ @@ -412,6 +460,7 @@ .font-monospace { font-family: 'JetBrains Mono', monospace; letter-spacing: -0.5px; } .td-clip { max-width: 260px; overflow-y: auto; text-overflow: ellipsis; } .row-clickable { cursor: pointer; } +.actions-col { min-width: 152px; } /* Paginação interna */ .table-pagination { @@ -428,7 +477,14 @@ } /* Ações na tabela (estilo Mureg) */ -.action-group { display: flex; justify-content: center; gap: 6px; } +.action-group { + display: flex; + justify-content: center; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + white-space: nowrap; +} .action-group .btn-icon { width: 32px; height: 32px; @@ -443,6 +499,8 @@ cursor: pointer; &:hover { background: rgba(17,18,20,0.05); color: var(--text); transform: translateY(-1px); } &.info:hover { color: var(--brand); background: rgba(227, 61, 207, 0.12); } + &.primary:hover { color: var(--blue); background: rgba(3, 15, 170, 0.1); } + &.danger:hover { color: #dc3545; background: rgba(220, 53, 69, 0.12); } } /* ========================================================== */ @@ -485,7 +543,7 @@ /* ========================================================== */ .modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } .modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } -.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow-y: auto; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; } +.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; min-height: 0; } .modal-card.modal-xl-custom { width: min(980px, 92vw); max-height: 82vh; } .modal-card.modal-lg { width: min(720px, 92vw); max-height: 80vh; } @keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } @@ -495,13 +553,61 @@ display: flex; justify-content: space-between; align-items: center; .modal-title { font-size: 1.1rem; font-weight: 800; color: var(--text); display: flex; align-items: center; gap: 12px; } - .icon-bg { width: 32px; height: 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 16px; + .icon-bg { width: 32px; height: 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 16px; background: rgba(3, 15, 170, 0.1); color: var(--blue); &.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); } + &.danger-soft { background: rgba(220, 53, 69, 0.12); color: #dc3545; } + &.success { background: var(--success-bg); color: var(--success-text); } + &.brand-soft { background: rgba(227, 61, 207, 0.1); color: var(--brand); } } .btn-icon { color: var(--muted); background: transparent; font-size: 1.2rem; border:none; cursor: pointer; &:hover { color: var(--brand); } } } -.modal-body { padding: 20px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } } +.modal-body { padding: 20px; overflow-y: auto; flex: 1; min-height: 0; &.bg-light-gray { background-color: #f8f9fa; } } +.modal-body .box-body { overflow: visible; } +.modal-footer { flex-shrink: 0; } +.modal-card.create-modal { width: min(1080px, 95vw); max-height: 86vh; } +.modal-card.create-modal .modal-header { background: linear-gradient(180deg, rgba(227, 61, 207, 0.08), #ffffff 70%); } +.modal-card.create-modal .modal-body { background: linear-gradient(180deg, rgba(248, 249, 250, 0.96), rgba(255, 255, 255, 0.98)); } +.modal-card.create-modal .edit-sections { gap: 14px; } +.modal-card.create-modal .detail-box { border: 1px solid rgba(227, 61, 207, 0.14); box-shadow: 0 10px 24px rgba(17, 18, 20, 0.06); } +.modal-card.create-modal .box-header { color: var(--brand); background: linear-gradient(135deg, rgba(227, 61, 207, 0.1), rgba(3, 15, 170, 0.07)); } +.modal-card.create-modal .box-body { background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(250, 250, 252, 0.96)); } +.modal-card.create-modal .form-field label { color: rgba(17, 18, 20, 0.68); } +.modal-card.create-modal .form-control, +.modal-card.create-modal .form-select { min-height: 40px; } +.modal-card.create-modal .form-check-input { + width: 1.05rem; + height: 1.05rem; + border-color: rgba(17, 18, 20, 0.32); + + &:focus { box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); } + &:checked { background-color: var(--brand); border-color: var(--brand); } +} +.modal-card.create-modal .modal-footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + padding: 14px 20px !important; + background: linear-gradient(180deg, #ffffff, rgba(248, 249, 251, 0.95)); +} +.modal-card.create-modal .modal-footer .btn { + border-radius: 12px; + font-weight: 900; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 120px; +} +.modal-card.create-modal .modal-footer .btn.me-2 { margin-right: 0 !important; } + +@media (max-width: 700px) { + .modal-card { border-radius: 16px; } + .modal-header { padding: 12px 16px; } + .modal-body { padding: 16px; } + .modal-card.create-modal .modal-footer { flex-direction: column-reverse; } + .modal-card.create-modal .modal-footer .btn { width: 100%; min-width: 0; } +} .details-dashboard { display: grid; grid-template-columns: 1fr; gap: 20px; } div.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.02); overflow-y: auto; height: auto; display: flex; flex-direction: column; } @@ -517,3 +623,87 @@ div.box-body { padding: 16px; } .val { font-size: 0.85rem; font-weight: 700; color: var(--text); word-break: break-word; line-height: 1.2; } } +.edit-sections { display: grid; gap: 12px; } +.edit-sections .detail-box { border: 1px solid rgba(17, 18, 20, 0.08); box-shadow: 0 8px 22px rgba(17, 18, 20, 0.06); } + +summary.box-header { + cursor: pointer; + user-select: none; + list-style: none; + + i:not(.transition-icon) { color: var(--brand); margin-right: 6px; } + &::-webkit-details-marker { display: none; } +} + +.transition-icon { color: var(--muted); transition: transform 0.25s ease, color 0.25s ease; } +details[open] .transition-icon { transform: rotate(180deg); color: var(--brand); } + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + + @media (max-width: 700px) { + grid-template-columns: 1fr; + } +} + +.form-field { + display: flex; + flex-direction: column; + gap: 6px; + + &.span-2 { grid-column: span 2; } + + label { + font-size: 0.72rem; + font-weight: 900; + letter-spacing: 0.04em; + text-transform: uppercase; + color: rgba(17, 18, 20, 0.64); + } +} + +.form-control, +.form-select { + border-radius: 10px; + border: 1px solid rgba(17,18,20,0.15); + background: #fff; + font-size: 0.9rem; + font-weight: 600; + color: var(--text); + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; + + &:hover { border-color: rgba(17, 18, 20, 0.36); } + &:focus { + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(227,61,207,0.15); + outline: none; + transform: translateY(-1px); + } +} + +.confirm-delete { + border: 1px solid rgba(220, 53, 69, 0.16); + background: #fff; + border-radius: 14px; + padding: 18px 16px; + display: flex; + align-items: center; + gap: 12px; + + p { font-weight: 700; color: rgba(17, 18, 20, 0.85); } +} + +.confirm-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(220, 53, 69, 0.12); + color: #dc3545; + flex-shrink: 0; +} + diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts index 769dee2..2877707 100644 --- a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts @@ -2,8 +2,9 @@ import { Component, Inject, PLATFORM_ID, OnInit, OnDestroy } from '@angular/core import { CommonModule, isPlatformBrowser } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; -import { ChipsControleService, ChipVirgemListDto, ControleRecebidoListDto, SortDir } from '../../services/chips-controle.service'; +import { ChipsControleService, ChipVirgemListDto, ControleRecebidoListDto, SortDir, UpdateChipVirgemRequest, UpdateControleRecebidoRequest, CreateChipVirgemRequest, CreateControleRecebidoRequest } from '../../services/chips-controle.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { AuthService } from '../../services/auth.service'; // Interface para o Agrupamento interface ChipGroup { @@ -18,6 +19,13 @@ interface ControleGroup { items: ControleRecebidoListDto[]; } +interface ChipVirgemCreateModel { + id: string; + item: number | null; + numeroDoChip: string | null; + observacoes: string | null; +} + type ChipsSortKey = 'item' | 'numeroDoChip' | 'observacoes'; type ControleSortKey = | 'ano' | 'item' | 'notaFiscal' | 'chip' | 'serial' | 'conteudoDaNf' | 'numeroDaLinha' @@ -82,19 +90,45 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy { chipDetailOpen = false; chipDetailLoading = false; chipDetailData: ChipVirgemListDto | null = null; + chipCreateOpen = false; + chipCreateSaving = false; + chipCreateModel: ChipVirgemCreateModel | null = null; + chipEditOpen = false; + chipEditSaving = false; + chipEditModel: ChipVirgemListDto | null = null; + chipEditingId: string | null = null; + chipDeleteOpen = false; + chipDeleteTarget: ChipVirgemListDto | null = null; controleDetailOpen = false; controleDetailLoading = false; controleDetailData: ControleRecebidoListDto | null = null; + controleCreateOpen = false; + controleCreateSaving = false; + controleCreateModel: ControleRecebidoListDto | null = null; + controleCreateDataNf = ''; + controleCreateRecebimento = ''; + controleEditOpen = false; + controleEditSaving = false; + controleEditModel: ControleRecebidoListDto | null = null; + controleEditDataNf = ''; + controleEditRecebimento = ''; + controleEditingId: string | null = null; + controleDeleteOpen = false; + controleDeleteTarget: ControleRecebidoListDto | null = null; + + isAdmin = false; constructor( @Inject(PLATFORM_ID) private platformId: object, private service: ChipsControleService, - private http: HttpClient + private http: HttpClient, + private authService: AuthService ) {} ngOnInit(): void { if (!isPlatformBrowser(this.platformId)) return; + this.isAdmin = this.authService.hasRole('admin'); this.fetchChips(); this.fetchControle(); } @@ -200,6 +234,118 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy { }); } + openChipCreate() { + if (!this.isAdmin) return; + this.chipCreateModel = { + id: '', + item: null, + numeroDoChip: '', + observacoes: '' + }; + this.chipCreateOpen = true; + this.chipCreateSaving = false; + } + + closeChipCreate() { + this.chipCreateOpen = false; + this.chipCreateSaving = false; + this.chipCreateModel = null; + } + + saveChipCreate() { + if (!this.chipCreateModel) return; + this.chipCreateSaving = true; + + const payload: CreateChipVirgemRequest = { + item: this.toNullableNumber(this.chipCreateModel.item), + numeroDoChip: this.chipCreateModel.numeroDoChip, + observacoes: this.chipCreateModel.observacoes + }; + + this.service.createChipVirgem(payload).subscribe({ + next: () => { + this.chipCreateSaving = false; + this.closeChipCreate(); + this.fetchChips(); + this.showToast('Chip criado com sucesso!', 'success'); + }, + error: () => { + this.chipCreateSaving = false; + this.showToast('Erro ao criar chip.', 'danger'); + } + }); + } + + openChipEdit(row: ChipVirgemListDto) { + if (!this.isAdmin) return; + this.service.getChipVirgemById(row.id).subscribe({ + next: (data) => { + this.chipEditingId = data.id; + this.chipEditModel = { ...data }; + this.chipEditOpen = true; + }, + error: () => this.showToast('Erro ao abrir edição.', 'danger') + }); + } + + closeChipEdit() { + this.chipEditOpen = false; + this.chipEditSaving = false; + this.chipEditModel = null; + this.chipEditingId = null; + } + + saveChipEdit() { + if (!this.chipEditModel || !this.chipEditingId) return; + this.chipEditSaving = true; + const payload: UpdateChipVirgemRequest = { + item: this.toNullableNumber(this.chipEditModel.item), + numeroDoChip: this.chipEditModel.numeroDoChip, + observacoes: this.chipEditModel.observacoes + }; + this.service.updateChipVirgem(this.chipEditingId, payload).subscribe({ + next: () => { + this.chipEditSaving = false; + this.closeChipEdit(); + this.fetchChips(); + this.showToast('Chip atualizado!', 'success'); + }, + error: () => { + this.chipEditSaving = false; + this.showToast('Erro ao salvar.', 'danger'); + } + }); + } + + openChipDelete(row: ChipVirgemListDto) { + if (!this.isAdmin) return; + this.chipDeleteTarget = row; + this.chipDeleteOpen = true; + } + + cancelChipDelete() { + this.chipDeleteOpen = false; + this.chipDeleteTarget = null; + } + + confirmChipDelete() { + if (!this.chipDeleteTarget) return; + const id = this.chipDeleteTarget.id; + this.service.removeChipVirgem(id).subscribe({ + next: () => { + this.chipDeleteOpen = false; + this.chipDeleteTarget = null; + this.fetchChips(); + this.showToast('Chip removido.', 'success'); + }, + error: () => { + this.chipDeleteOpen = false; + this.chipDeleteTarget = null; + this.showToast('Erro ao remover.', 'danger'); + } + }); + } + closeChipDetail() { this.chipDetailOpen = false; this.chipDetailLoading = false; @@ -349,6 +495,216 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy { }); } + openControleCreate() { + if (!this.isAdmin) return; + this.controleCreateModel = { + id: '', + ano: new Date().getFullYear(), + item: null, + notaFiscal: '', + chip: '', + serial: '', + conteudoDaNf: '', + numeroDaLinha: '', + valorUnit: null, + valorDaNf: null, + dataDaNf: null, + dataDoRecebimento: null, + quantidade: null, + isResumo: false + } as ControleRecebidoListDto; + this.controleCreateDataNf = ''; + this.controleCreateRecebimento = ''; + this.controleCreateOpen = true; + this.controleCreateSaving = false; + } + + closeControleCreate() { + this.controleCreateOpen = false; + this.controleCreateSaving = false; + this.controleCreateModel = null; + this.controleCreateDataNf = ''; + this.controleCreateRecebimento = ''; + } + + onControleCreateValueChange() { + if (!this.controleCreateModel) return; + this.recalculateControleTotals(this.controleCreateModel); + } + + onControleEditValueChange() { + if (!this.controleEditModel) return; + this.recalculateControleTotals(this.controleEditModel); + } + + onControleCreateDateChange() { + if (!this.controleCreateModel) return; + if (!this.controleCreateModel.ano && this.controleCreateDataNf) { + const year = new Date(this.controleCreateDataNf).getFullYear(); + if (Number.isFinite(year)) this.controleCreateModel.ano = year; + } + } + + onControleEditDateChange() { + if (!this.controleEditModel) return; + if (!this.controleEditModel.ano && this.controleEditDataNf) { + const year = new Date(this.controleEditDataNf).getFullYear(); + if (Number.isFinite(year)) this.controleEditModel.ano = year; + } + } + + private recalculateControleTotals(model: ControleRecebidoListDto) { + const quantidade = this.toNullableNumber(model.quantidade); + const valorUnit = this.toNullableNumber(model.valorUnit); + const valorDaNf = this.toNullableNumber(model.valorDaNf); + + if (quantidade != null && valorUnit != null && (valorDaNf == null || valorDaNf === 0)) { + model.valorDaNf = Number((valorUnit * quantidade).toFixed(2)); + } else if (quantidade != null && valorDaNf != null && (valorUnit == null || valorUnit === 0)) { + model.valorUnit = Number((valorDaNf / quantidade).toFixed(2)); + } + } + + saveControleCreate() { + if (!this.controleCreateModel) return; + this.recalculateControleTotals(this.controleCreateModel); + this.controleCreateSaving = true; + + const payload: CreateControleRecebidoRequest = { + ano: this.toNullableNumber(this.controleCreateModel.ano), + item: this.toNullableNumber(this.controleCreateModel.item), + notaFiscal: this.controleCreateModel.notaFiscal, + chip: this.controleCreateModel.chip, + serial: this.controleCreateModel.serial, + conteudoDaNf: this.controleCreateModel.conteudoDaNf, + numeroDaLinha: this.controleCreateModel.numeroDaLinha, + valorUnit: this.toNullableNumber(this.controleCreateModel.valorUnit), + valorDaNf: this.toNullableNumber(this.controleCreateModel.valorDaNf), + dataDaNf: this.dateInputToIso(this.controleCreateDataNf), + dataDoRecebimento: this.dateInputToIso(this.controleCreateRecebimento), + quantidade: this.toNullableNumber(this.controleCreateModel.quantidade), + isResumo: this.controleCreateModel.isResumo ?? false + }; + + this.service.createControleRecebido(payload).subscribe({ + next: () => { + this.controleCreateSaving = false; + this.closeControleCreate(); + this.fetchControle(); + this.showToast('Recebimento criado com sucesso!', 'success'); + }, + error: () => { + this.controleCreateSaving = false; + this.showToast('Erro ao criar recebimento.', 'danger'); + } + }); + } + + openControleEdit(row: ControleRecebidoListDto) { + if (!this.isAdmin) return; + this.service.getControleRecebidoById(row.id).subscribe({ + next: (data) => { + this.controleEditingId = data.id; + this.controleEditModel = { ...data }; + this.controleEditDataNf = this.toDateInput(data.dataDaNf); + this.controleEditRecebimento = this.toDateInput(data.dataDoRecebimento); + this.controleEditOpen = true; + }, + error: () => this.showToast('Erro ao abrir edição.', 'danger') + }); + } + + closeControleEdit() { + this.controleEditOpen = false; + this.controleEditSaving = false; + this.controleEditModel = null; + this.controleEditDataNf = ''; + this.controleEditRecebimento = ''; + this.controleEditingId = null; + } + + saveControleEdit() { + if (!this.controleEditModel || !this.controleEditingId) return; + this.recalculateControleTotals(this.controleEditModel); + this.controleEditSaving = true; + const payload: UpdateControleRecebidoRequest = { + ano: this.toNullableNumber(this.controleEditModel.ano), + item: this.toNullableNumber(this.controleEditModel.item), + notaFiscal: this.controleEditModel.notaFiscal, + chip: this.controleEditModel.chip, + serial: this.controleEditModel.serial, + conteudoDaNf: this.controleEditModel.conteudoDaNf, + numeroDaLinha: this.controleEditModel.numeroDaLinha, + valorUnit: this.toNullableNumber(this.controleEditModel.valorUnit), + valorDaNf: this.toNullableNumber(this.controleEditModel.valorDaNf), + dataDaNf: this.dateInputToIso(this.controleEditDataNf), + dataDoRecebimento: this.dateInputToIso(this.controleEditRecebimento), + quantidade: this.toNullableNumber(this.controleEditModel.quantidade), + isResumo: this.controleEditModel.isResumo ?? false + }; + this.service.updateControleRecebido(this.controleEditingId, payload).subscribe({ + next: () => { + this.controleEditSaving = false; + this.closeControleEdit(); + this.fetchControle(); + this.showToast('Registro atualizado!', 'success'); + }, + error: () => { + this.controleEditSaving = false; + this.showToast('Erro ao salvar.', 'danger'); + } + }); + } + + openControleDelete(row: ControleRecebidoListDto) { + if (!this.isAdmin) return; + this.controleDeleteTarget = row; + this.controleDeleteOpen = true; + } + + cancelControleDelete() { + this.controleDeleteOpen = false; + this.controleDeleteTarget = null; + } + + confirmControleDelete() { + if (!this.controleDeleteTarget) return; + const id = this.controleDeleteTarget.id; + this.service.removeControleRecebido(id).subscribe({ + next: () => { + this.controleDeleteOpen = false; + this.controleDeleteTarget = null; + this.fetchControle(); + this.showToast('Registro removido.', 'success'); + }, + error: () => { + this.controleDeleteOpen = false; + this.controleDeleteTarget = null; + this.showToast('Erro ao remover.', 'danger'); + } + }); + } + + private toDateInput(value: string | null): string { + if (!value) return ''; + const d = new Date(value); + if (isNaN(d.getTime())) return ''; + return d.toISOString().slice(0, 10); + } + + private dateInputToIso(value: string): string | null { + if (!value) return null; + const d = new Date(`${value}T00:00:00`); + if (isNaN(d.getTime())) return null; + return d.toISOString(); + } + + private toNullableNumber(value: any): number | null { + if (value === undefined || value === null || value === '') return null; + const n = Number(value); + return Number.isNaN(n) ? null : n; + } + closeControleDetail() { this.controleDetailOpen = false; this.controleDetailLoading = false; diff --git a/src/app/pages/dados-usuarios/dados-usuarios.html b/src/app/pages/dados-usuarios/dados-usuarios.html index 703d9f5..d3de0ff 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.html +++ b/src/app/pages/dados-usuarios/dados-usuarios.html @@ -22,16 +22,19 @@
- DADOS USUÁRIOS + DADOS PF/PJ
-
GESTÃO DE USUÁRIOS
- Base de dados agrupada por cliente +
GESTÃO DE USUÁRIOS PF/PJ
+ Base de dados separada por pessoa física e jurídica
+
@@ -51,10 +54,10 @@
- Com CPF + {{ tipoFilter === 'PJ' ? 'Com CNPJ' : 'Com CPF' }} - {{ kpiComCpf || 0 }} + {{ tipoFilter === 'PJ' ? (kpiComCnpj || 0) : (kpiComCpf || 0) }}
@@ -67,9 +70,17 @@
+
+ + +
- +
@@ -99,7 +110,8 @@
{{ g.cliente }}
{{ g.totalRegistros }} Registros - {{ g.comCpf }} CPF + {{ g.comCpf }} CPF + {{ g.comCnpj }} CNPJ {{ g.comEmail }} Email
@@ -122,10 +134,10 @@ ITEM LINHA - CPF + {{ tipoFilter === 'PJ' ? 'CNPJ' : 'CPF' }} E-MAIL CELULAR - AÇÕES + AÇÕES @@ -135,12 +147,14 @@ {{ r.item }} {{ r.linha }} - {{ r.cpf || '-' }} + {{ tipoFilter === 'PJ' ? (r.cnpj || '-') : (r.cpf || '-') }} {{ r.email || '-' }} {{ r.celular || '-' }}
+ +
@@ -167,9 +181,9 @@ - -
+ +
+ + +
+
+
Modo
+
+ + + +
+
+ +
+
Serviços
+
+ +
+
+ + +
+
Total Clientes - - - {{ kpiTotalClientes || 0 }} + + + {{ kpiTotalClientes || 0 }}
Total Linhas - - - {{ kpiTotalLinhas || 0 }} + + + {{ kpiTotalLinhas || 0 }}
Ativas - - - {{ kpiAtivas || 0 }} + + + {{ kpiAtivas || 0 }}
Bloqueadas - - - {{ kpiBloqueadas || 0 }} + + + {{ kpiBloqueadas || 0 }}
@@ -204,14 +290,14 @@
{{ group.cliente }}
-
- {{ group.totalLinhas }} Linhas - {{ group.ativos }} Ativas - {{ group.bloqueados }} Bloq. -
-
-
+
+ {{ group.totalLinhas }} linhas + {{ group.ativos }} ativas + {{ group.bloqueados }} bloqueadas
+
+
+
@@ -253,7 +339,7 @@ - +
@@ -370,7 +456,7 @@ - +
@@ -428,9 +514,8 @@ @@ -615,6 +734,7 @@
+
@@ -698,6 +818,10 @@ Chip (ICCID) {{ detailData.chip || '-' }} +
+ Tipo de Chip + {{ detailData.tipoDeChip || '-' }} +
@@ -744,6 +868,14 @@ Data Bloqueio {{ formatDateBr(detailData.dataBloqueio) }} +
+ Efetivação Serviço + {{ formatDateBr(detailData.dtEfetivacaoServico) }} +
+
+ Término Fidelização + {{ formatDateBr(detailData.dtTerminoFidelizacao) }} +
@@ -817,6 +949,7 @@
Vivo News+ {{ formatMoney(financeData.vivoNewsPlus) }}
Travel Mundo {{ formatMoney(financeData.vivoTravelMundo) }}
Gestão Disp. {{ formatMoney(financeData.vivoGestaoDispositivo) }}
+
Vivo Sync {{ formatMoney(financeData.vivoSync) }}
Total Vivo {{ formatMoney(financeData.valorContratoVivo) }}
@@ -882,9 +1015,11 @@
+
+
@@ -895,7 +1030,7 @@ Contrato & Plano
-
+
@@ -910,6 +1045,8 @@
+
+
@@ -928,6 +1065,7 @@
+
@@ -963,4 +1101,3 @@ - diff --git a/src/app/pages/geral/geral.scss b/src/app/pages/geral/geral.scss index 05a0880..4151b81 100644 --- a/src/app/pages/geral/geral.scss +++ b/src/app/pages/geral/geral.scss @@ -41,13 +41,13 @@ /* 2. LAYOUT DA PÁGINA (Vertical Destravado) */ /* ========================================================== */ .geral-page { - min-height: 100vh; - padding: 0 12px var(--page-bottom-gap); + min-height: 100dvh; + padding: var(--page-top-gap) 12px var(--page-bottom-gap); display: flex; align-items: flex-start; justify-content: center; position: relative; - overflow-y: auto; /* Scroll na janela */ + overflow: visible; background: radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%), radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%), @@ -80,7 +80,7 @@ max-width: 1100px; /* Largura controlada */ position: relative; z-index: 1; - margin-top: var(--page-top-gap); + margin-top: 0; margin-bottom: var(--page-bottom-gap); margin-left: auto; margin-right: auto; } @@ -138,9 +138,123 @@ .dropdown-list { overflow-y: auto; max-height: 300px; } .dropdown-item-custom { padding: 10px 16px; font-size: 0.85rem; color: var(--text); cursor: pointer; border-bottom: 1px solid rgba(0,0,0,0.03); transition: background 0.1s; &:hover { background: rgba(227,61,207,0.05); color: var(--brand); font-weight: 600; } &.selected { background: rgba(227, 61, 207, 0.08); color: var(--brand); font-weight: 700; } } +.additional-filter-wrap { + position: relative; +} + +.btn-additional-filter { + min-width: 160px; + max-width: 230px; +} + +.additional-dropdown { + width: min(420px, calc(100vw - 24px)); + max-height: 460px; + padding: 10px; + overflow: auto; + gap: 10px; +} + +.additional-dropdown-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.additional-dropdown-title { + font-size: 0.74rem; + font-weight: 900; + letter-spacing: 0.04em; + text-transform: uppercase; + color: rgba(17, 18, 20, 0.6); +} + +.additional-dropdown-footer { + padding-top: 4px; + display: flex; + justify-content: flex-start; +} + +.additional-mode-tabs { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.additional-mode-btn { + border: 1px solid rgba(17, 18, 20, 0.12); + background: rgba(255, 255, 255, 0.7); + color: var(--muted); + font-weight: 800; + font-size: 0.8rem; + border-radius: 999px; + padding: 6px 12px; + transition: all 0.2s ease; + + &:hover { + border-color: var(--blue); + color: var(--blue); + background: #fff; + } + + &.active { + border-color: var(--brand); + color: var(--brand); + background: #fff; + box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.additional-services-chips { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.additional-chip-btn { + border: 1px solid rgba(17, 18, 20, 0.12); + background: rgba(255, 255, 255, 0.68); + color: var(--muted); + font-weight: 700; + font-size: 0.78rem; + border-radius: 999px; + padding: 5px 10px; + transition: all 0.2s ease; + + &:hover { + border-color: var(--blue); + color: var(--blue); + background: #fff; + } + + &.active { + border-color: var(--brand); + color: var(--brand); + background: rgba(227, 61, 207, 0.12); + } + + &.clear { + color: var(--blue); + border-color: rgba(3, 15, 170, 0.18); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + /* KPIs */ .geral-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 20px; margin-bottom: 16px; width: 100%; @media (max-width: 992px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 576px) { grid-template-columns: 1fr; } } .kpi { background: rgba(255,255,255,0.7); border: 1px solid rgba(17,18,20,0.08); border-radius: 16px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; backdrop-filter: blur(8px); transition: transform 0.2s, box-shadow 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.02); &:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(227, 61, 207, 0.1); background: #fff; border-color: var(--brand); } .lbl { font-size: 0.72rem; font-weight: 900; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); &.text-success { color: var(--success-text) !important; } &.text-danger { color: var(--danger-text) !important; } } .val { font-size: 1.25rem; font-weight: 950; color: var(--text); } } +.kpi .val-loading { font-size: 0.86rem; font-weight: 900; color: var(--muted); display: inline-flex; align-items: center; } + +/* Insights */ /* Controls */ .controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; } @@ -166,6 +280,10 @@ .group-info { display: flex; flex-direction: column; gap: 6px; } .group-badges { display: flex; gap: 8px; flex-wrap: wrap; } .badge-pill { font-size: 0.7rem; padding: 4px 10px; border-radius: 999px; font-weight: 800; text-transform: uppercase; &.total { background: rgba(3,15,170,0.1); color: var(--blue); } &.ok { background: var(--success-bg); color: var(--success-text); } } +.group-tags { display: flex; flex-wrap: wrap; gap: 6px; } +.tag-pill { font-size: 0.65rem; padding: 4px 8px; border-radius: 999px; font-weight: 800; text-transform: uppercase; background: rgba(3,15,170,0.08); color: var(--blue); border: 1px solid rgba(3,15,170,0.16); } +.tag-pill.active { background: var(--success-bg); color: var(--success-text); border-color: rgba(25,135,84,0.22); } +.tag-pill.blocked { background: var(--danger-bg); color: var(--danger-text); border-color: rgba(220,53,69,0.22); } .group-toggle-icon { font-size: 1.2rem; color: var(--muted); transition: transform 0.3s ease; } .client-group-card.expanded .group-toggle-icon { transform: rotate(180deg); color: var(--brand); } .group-body { border-top: 1px solid rgba(17,18,20,0.06); background: #fbfbfc; animation: slideDown 0.3s cubic-bezier(0.16, 1, 0.3, 1); } @@ -209,6 +327,7 @@ .modal-body { padding: 24px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } } .modal-body .box-body { overflow: visible; } .modal-xl-custom { width: min(1100px, 95vw); max-height: 85vh; } +.modal-card.modal-create { width: min(1280px, 96vw); max-height: 92vh; } /* === MODAL DE EDITAR E SEÇÕES (Accordion) === */ /* ✅ Restauração Crítica para "Editar" e "Novo Cliente" */ diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 4b919bc..8fc761c 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -16,10 +16,16 @@ import { HttpParams, HttpErrorResponse } from '@angular/common/http'; +import { NavigationEnd, Router } from '@angular/router'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { PlanAutoFillService } from '../../services/plan-autofill.service'; +import { AuthService } from '../../services/auth.service'; +import { firstValueFrom, Subscription, filter } from 'rxjs'; type SortDir = 'asc' | 'desc'; type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP'; +type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT'; +type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo'; interface LineRow { id: string; @@ -48,6 +54,12 @@ interface ApiLineList { vencConta: string | null; status?: string | null; skil?: string | null; + gestaoVozDados?: number | null; + skeelo?: number | null; + vivoNewsPlus?: number | null; + vivoTravelMundo?: number | null; + vivoSync?: number | null; + vivoGestaoDispositivo?: number | null; } interface ApiLineDetail { @@ -57,6 +69,7 @@ interface ApiLineDetail { conta?: string | null; linha?: string | null; chip?: string | null; + tipoDeChip?: string | null; cliente?: string | null; usuario?: string | null; planoContrato?: string | null; @@ -68,6 +81,8 @@ interface ApiLineDetail { solicitante?: string | null; dataEntregaOpera?: string | null; dataEntregaCliente?: string | null; + dtEfetivacaoServico?: string | null; + dtTerminoFidelizacao?: string | null; vencConta?: string | null; franquiaVivo?: number | null; @@ -77,6 +92,7 @@ interface ApiLineDetail { vivoNewsPlus?: number | null; vivoTravelMundo?: number | null; vivoGestaoDispositivo?: number | null; + vivoSync?: number | null; valorContratoVivo?: number | null; franquiaLine?: number | null; @@ -98,6 +114,12 @@ interface ClientGroupDto { bloqueados: number; } +interface AccountCompanyOption { + empresa: string; + contas: string[]; +} + + @Component({ standalone: true, imports: [CommonModule, FormsModule, CustomSelectComponent], @@ -118,11 +140,15 @@ export class Geral implements AfterViewInit, OnDestroy { constructor( @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + private planAutoFill: PlanAutoFillService, + private authService: AuthService, + private router: Router ) {} private readonly apiBase = 'https://localhost:7205/api/lines'; loading = false; + isAdmin = false; rows: LineRow[] = []; clientGroups: ClientGroupDto[] = []; @@ -132,12 +158,23 @@ export class Geral implements AfterViewInit, OnDestroy { searchTerm = ''; filterSkil: 'ALL' | 'PF' | 'PJ' | 'RESERVA' = 'ALL'; + additionalMode: AdditionalMode = 'ALL'; + selectedAdditionalServices: AdditionalServiceKey[] = []; + readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [ + { key: 'gvd', label: 'Gestão Voz e Dados' }, + { key: 'skeelo', label: 'Skeelo' }, + { key: 'news', label: 'Vivo News Plus' }, + { key: 'travel', label: 'Vivo Travel Mundo' }, + { key: 'sync', label: 'Vivo Sync' }, + { key: 'dispositivo', label: 'Vivo Gestão Dispositivo' } + ]; clientsList: string[] = []; loadingClientsList = false; selectedClients: string[] = []; showClientMenu = false; + showAdditionalMenu = false; clientSearchTerm = ''; viewMode: 'GROUPS' | 'TABLE' = 'GROUPS'; @@ -164,8 +201,14 @@ export class Geral implements AfterViewInit, OnDestroy { private editingId: string | null = null; private searchTimer: any = null; + private navigationSub?: Subscription; + private keepPageOnNextGroupsLoad = false; private searchResolvedClient: string | null = null; + private kpiRequestVersion = 0; + private groupsRequestVersion = 0; + private linesRequestVersion = 0; + private clientsRequestVersion = 0; loadingKpis = false; kpiTotalClientes = 0; @@ -176,7 +219,7 @@ export class Geral implements AfterViewInit, OnDestroy { readonly statusOptions = ['ATIVO', 'BLOQUEIO PERDA/ROUBO', 'BLOQUEIO 120 DIAS']; readonly skilOptions = ['PESSOA FÍSICA', 'PESSOA JURÍDICA', 'RESERVA']; - readonly planOptions = [ + planOptions = [ 'SMART EMPRESAS 0.2GB TE', 'SMART EMPRESAS 0.5GB TE', 'SMART EMPRESAS 2GB D', @@ -190,20 +233,35 @@ export class Geral implements AfterViewInit, OnDestroy { 'M2M 50MB' ]; - readonly contaOptions = [ - '0172593311', - '0172593840', - '0430237019', - '0435288088', - '0437488125', - '0449508564', - '0455371844', - 'CLARO', - 'TIM' + private readonly fallbackAccountCompanies: AccountCompanyOption[] = [ + { empresa: 'CLARO LINE MÓVEL', contas: ['172593311', '172593840'] }, + { empresa: 'VIVO MACROPHONY', contas: ['0430237019', '0437488125', '0449508564', '0454371844'] }, + { empresa: 'VIVO LINE MÓVEL', contas: ['0435288088'] }, + { empresa: 'TIM LINE MÓVEL', contas: ['0072046192'] } ]; + accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies]; + loadingAccountCompanies = false; + + get contaEmpresaOptions(): string[] { + return this.accountCompanies.map((x) => x.empresa); + } + + get contaOptionsForCreate(): string[] { + return this.getContasByEmpresa(this.createModel?.contaEmpresa); + } + + get contaEmpresaOptionsForEdit(): string[] { + return this.mergeOption(this.editModel?.contaEmpresa, this.contaEmpresaOptions); + } + get contaOptionsForEdit(): string[] { - return this.mergeOption(this.editModel?.conta, this.contaOptions); + const empresaSelecionada = (this.editModel?.contaEmpresa ?? '').toString().trim(); + const baseOptions = empresaSelecionada + ? this.getContasByEmpresa(empresaSelecionada) + : this.getAllContas(); + + return this.mergeOption(this.editModel?.conta, baseOptions); } get planOptionsForEdit(): string[] { @@ -222,8 +280,10 @@ export class Geral implements AfterViewInit, OnDestroy { cliente: '', docType: 'PF', docNumber: '', + contaEmpresa: '', linha: '', chip: '', + tipoDeChip: '', usuario: '', status: '', planoContrato: '', @@ -237,6 +297,8 @@ export class Geral implements AfterViewInit, OnDestroy { dataBloqueio: '', dataEntregaOpera: '', dataEntregaCliente: '', + dtEfetivacaoServico: '', + dtTerminoFidelizacao: '', franquiaVivo: null, valorPlanoVivo: null, gestaoVozDados: null, @@ -244,6 +306,7 @@ export class Geral implements AfterViewInit, OnDestroy { vivoNewsPlus: null, vivoTravelMundo: null, vivoGestaoDispositivo: null, + vivoSync: null, valorContratoVivo: null, franquiaLine: null, franquiaGestao: null, @@ -257,6 +320,25 @@ export class Geral implements AfterViewInit, OnDestroy { return this.viewMode === 'GROUPS'; } + get isKpiLoading(): boolean { + return this.loading || this.loadingKpis; + } + + get hasAdditionalFiltersApplied(): boolean { + return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0; + } + + get additionalModeLabel(): string { + if (this.additionalMode === 'WITH') return 'Com adicionais'; + if (this.additionalMode === 'WITHOUT') return 'Sem adicionais'; + return 'Todos os adicionais'; + } + + get additionalSelectedLabels(): string[] { + return this.selectedAdditionalServices + .map((key) => this.additionalServiceOptions.find((x) => x.key === key)?.label ?? key); + } + // ✅ fecha dropdown ao clicar fora @HostListener('document:click', ['$event']) onDocumentClick(ev: MouseEvent) { @@ -265,33 +347,57 @@ export class Geral implements AfterViewInit, OnDestroy { // Se modal estiver aberto, não mexe no dropdown por clique no overlay if (this.anyModalOpen()) return; - if (!this.showClientMenu) return; + if (!this.showClientMenu && !this.showAdditionalMenu) return; const target = ev.target as HTMLElement | null; if (!target) return; - const inside = !!target.closest('.client-filter-wrap'); - if (!inside) { + const insideClient = !!target.closest('.client-filter-wrap'); + const insideAdditional = !!target.closest('.additional-filter-wrap'); + let changed = false; + + if (this.showClientMenu && !insideClient) { this.showClientMenu = false; + changed = true; + } + + if (this.showAdditionalMenu && !insideAdditional) { + this.showAdditionalMenu = false; + changed = true; + } + + if (changed) { this.cdr.detectChanges(); } } // ✅ ESC fecha dropdown OU modal (sem conflito) @HostListener('document:keydown', ['$event']) - onDocumentKeydown(ev: KeyboardEvent) { + onDocumentKeydown(ev: Event) { if (!isPlatformBrowser(this.platformId)) return; - if (ev.key === 'Escape') { + const keyboard = ev as KeyboardEvent; + if (keyboard.key === 'Escape') { if (this.anyModalOpen()) { - ev.preventDefault(); - ev.stopPropagation(); + keyboard.preventDefault(); + keyboard.stopPropagation(); this.closeAllModals(); return; } + let changed = false; + if (this.showClientMenu) { this.showClientMenu = false; - ev.stopPropagation(); + changed = true; + } + + if (this.showAdditionalMenu) { + this.showAdditionalMenu = false; + changed = true; + } + + if (changed) { + keyboard.stopPropagation(); this.cdr.detectChanges(); } } @@ -299,16 +405,20 @@ export class Geral implements AfterViewInit, OnDestroy { ngOnDestroy(): void { if (this.searchTimer) clearTimeout(this.searchTimer); + this.navigationSub?.unsubscribe(); } async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; + this.isAdmin = this.authService.hasRole('admin'); this.initAnimations(); setTimeout(() => { this.refreshData(); this.loadClients(); + this.loadPlanRules(); + this.loadAccountCompanies(); const state = history.state; if (state && state.toastMessage) { @@ -319,6 +429,17 @@ export class Geral implements AfterViewInit, OnDestroy { this.showToast(msg); } }); + + this.navigationSub = this.router.events + .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) + .subscribe((event) => { + const url = (event.urlAfterRedirects || '').toLowerCase(); + if (!url.includes('/geral')) return; + + this.searchResolvedClient = null; + this.loadClients(); + this.refreshData(); + }); } private initAnimations() { @@ -329,6 +450,43 @@ export class Geral implements AfterViewInit, OnDestroy { }, 100); } + private async loadPlanRules() { + try { + await this.planAutoFill.load(); + const extraPlans = this.planAutoFill.getPlanOptions(); + if (extraPlans.length > 0) { + this.planOptions = this.mergeOptionList(this.planOptions, extraPlans); + this.cdr.detectChanges(); + } + } catch { + // silencioso: segue com a lista estática + } + } + + private loadAccountCompanies() { + this.loadingAccountCompanies = true; + + this.http.get(`${this.apiBase}/account-companies`).subscribe({ + next: (data) => { + const normalized = this.normalizeAccountCompanies(data); + this.accountCompanies = + normalized.length > 0 ? normalized : [...this.fallbackAccountCompanies]; + this.loadingAccountCompanies = false; + + this.syncContaEmpresaSelection(this.createModel); + this.syncContaEmpresaSelection(this.editModel); + this.cdr.detectChanges(); + }, + error: () => { + this.accountCompanies = [...this.fallbackAccountCompanies]; + this.loadingAccountCompanies = false; + + this.syncContaEmpresaSelection(this.createModel); + this.syncContaEmpresaSelection(this.editModel); + } + }); + } + // ============================================================ // ✅ FIX PRINCIPAL: limpeza forçada de backdrops/scroll lock // ============================================================ @@ -381,7 +539,16 @@ export class Geral implements AfterViewInit, OnDestroy { // ============================================================ - refreshData() { + private withNoCache(params: HttpParams): HttpParams { + return params.set('_ts', Date.now().toString()); + } + + refreshData(opts?: { keepCurrentPage?: boolean }) { + const keepCurrentPage = !!opts?.keepCurrentPage; + this.keepPageOnNextGroupsLoad = keepCurrentPage; + if (!keepCurrentPage && this.filterSkil === 'RESERVA') { + this.page = 1; + } this.searchResolvedClient = null; this.loadKpis(); this.viewMode = 'GROUPS'; @@ -405,20 +572,21 @@ export class Geral implements AfterViewInit, OnDestroy { const s = (term ?? '').trim(); if (!s) return Promise.resolve(null); - let params = new HttpParams().set('page', '1').set('pageSize', '1').set('search', s); - - if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); - else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); - else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); + const pageSize = this.hasAdditionalFiltersApplied ? '500' : '1'; + let params = new HttpParams().set('page', '1').set('pageSize', pageSize).set('search', s); + params = this.applyBaseFilters(params); if (this.selectedClients.length > 0) { this.selectedClients.forEach((c) => (params = params.append('client', c))); } return new Promise((resolve) => { - this.http.get>(this.apiBase, { params }).subscribe({ + this.http.get>(this.apiBase, { params: this.withNoCache(params) }).subscribe({ next: (res) => { - const first = (res.items ?? [])[0]; + const source = this.hasAdditionalFiltersApplied + ? this.applyAdditionalFiltersClientSide(res.items ?? []) + : (res.items ?? []); + const first = source[0]; const client = (first?.cliente ?? '').trim(); resolve(client || null); }, @@ -464,31 +632,43 @@ export class Geral implements AfterViewInit, OnDestroy { } private loadOnlyThisClientGroup(clientName: string): Promise { + const requestVersion = ++this.groupsRequestVersion; this.loading = true; - let params = new HttpParams().set('page', '1').set('pageSize', '9999'); + if (this.hasAdditionalFiltersApplied) { + return this.loadOnlyThisClientGroupFromLines(clientName, requestVersion); + } - if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); - else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); - else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); + let params = new HttpParams().set('page', '1').set('pageSize', '9999'); + params = this.applyBaseFilters(params); params = params.append('client', clientName); return new Promise((resolve) => { - this.http.get>(`${this.apiBase}/groups`, { params }).subscribe({ + this.http.get>(`${this.apiBase}/groups`, { params: this.withNoCache(params) }).subscribe({ next: (res) => { + if (requestVersion !== this.groupsRequestVersion) { + resolve(); + return; + } + let items = res.items || []; items = items.filter( (g) => (g.cliente || '').trim().toUpperCase() === (clientName || '').trim().toUpperCase() ); - this.clientGroups = items; + this.clientGroups = this.sortGroupsWithReservaFirst(items); this.total = items.length; this.loading = false; this.cdr.detectChanges(); resolve(); }, error: () => { + if (requestVersion !== this.groupsRequestVersion) { + resolve(); + return; + } + this.loading = false; this.showToast('Erro ao carregar grupos.'); resolve(); @@ -497,34 +677,112 @@ export class Geral implements AfterViewInit, OnDestroy { }); } + private async loadOnlyThisClientGroupFromLines(clientName: string, requestVersion: number): Promise { + try { + const lines = await this.fetchLinesForGrouping(); + if (requestVersion !== this.groupsRequestVersion) return; + + const target = (clientName || '').trim().toUpperCase(); + let groups = this.buildGroupsFromLines(lines); + groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target); + + this.clientGroups = this.sortGroupsWithReservaFirst(groups); + this.total = groups.length; + this.loading = false; + this.cdr.detectChanges(); + } catch { + if (requestVersion !== this.groupsRequestVersion) return; + this.loading = false; + this.showToast('Erro ao carregar grupos.'); + } + } + private loadClients() { + const requestVersion = ++this.clientsRequestVersion; this.loadingClientsList = true; this.clientsList = []; + if (this.hasAdditionalFiltersApplied) { + void this.loadClientsFromLines(requestVersion); + return; + } + let params = new HttpParams(); + params = this.applyBaseFilters(params); - if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); - else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); - else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); - - this.http.get(`${this.apiBase}/clients`, { params }).subscribe({ + this.http.get(`${this.apiBase}/clients`, { params: this.withNoCache(params) }).subscribe({ next: (data) => { + if (requestVersion !== this.clientsRequestVersion) return; this.clientsList = data || []; this.loadingClientsList = false; }, error: () => { + if (requestVersion !== this.clientsRequestVersion) return; this.loadingClientsList = false; console.error('Erro ao carregar lista de clientes para o filtro.'); } }); } + private async loadClientsFromLines(requestVersion: number): Promise { + try { + let baseParams = new HttpParams() + .set('sortBy', 'cliente') + .set('sortDir', 'asc'); + baseParams = this.applyBaseFilters(baseParams); + + const pageSize = 5000; + let page = 1; + let expectedTotal = 0; + const allLines: ApiLineList[] = []; + + while (page <= 500) { + const params = baseParams + .set('page', String(page)) + .set('pageSize', String(pageSize)); + + const response = await firstValueFrom( + this.http.get>(this.apiBase, { params: this.withNoCache(params) }) + ); + + const items = response?.items ?? []; + expectedTotal = this.toInt(response?.total); + allLines.push(...items); + + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && allLines.length >= expectedTotal) break; + + page += 1; + } + + if (requestVersion !== this.clientsRequestVersion) return; + + const filteredLines = this.applyAdditionalFiltersClientSide(allLines); + const fallbackClient = this.filterSkil === 'RESERVA' ? 'RESERVA' : ''; + const clients = filteredLines + .map((x) => ((x.cliente ?? '').toString().trim()) || fallbackClient) + .filter((x) => !!x); + + this.clientsList = Array.from(new Set(clients)).sort((a, b) => + a.localeCompare(b, 'pt-BR', { sensitivity: 'base' }) + ); + this.loadingClientsList = false; + } catch { + if (requestVersion !== this.clientsRequestVersion) return; + this.loadingClientsList = false; + console.error('Erro ao carregar lista de clientes para o filtro.'); + } + } + setFilter(type: 'ALL' | 'PF' | 'PJ' | 'RESERVA') { - if (this.filterSkil === type) return; + const isSameFilter = this.filterSkil === type; this.expandedGroup = null; this.groupLines = []; - this.filterSkil = type; + if (!isSameFilter) { + this.filterSkil = type; + } this.selectedClients = []; this.clientSearchTerm = ''; @@ -536,69 +794,281 @@ export class Geral implements AfterViewInit, OnDestroy { this.refreshData(); } - private loadKpis() { - this.loadingKpis = true; + setAdditionalMode(mode: AdditionalMode) { + if (this.additionalMode === mode) return; - let params = new HttpParams().set('page', '1').set('pageSize', '99999'); + this.additionalMode = mode; + this.expandedGroup = null; + this.groupLines = []; + this.searchResolvedClient = null; + this.page = 1; - if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); - else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); - else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); - - if (this.searchResolvedClient) { - params = params.append('client', this.searchResolvedClient); - } else { - if (this.searchTerm) params = params.set('search', this.searchTerm); - - if (this.selectedClients.length > 0) { - this.selectedClients.forEach((c) => (params = params.append('client', c))); - } - } - - this.http.get>(`${this.apiBase}/groups`, { params }).subscribe({ - next: (res) => { - let allGroups = res.items || []; - - if (this.searchResolvedClient) { - const target = (this.searchResolvedClient || '').trim().toUpperCase(); - allGroups = allGroups.filter((g) => (g.cliente || '').trim().toUpperCase() === target); - } else if (this.selectedClients.length > 0) { - allGroups = allGroups.filter((g) => - this.selectedClients.some( - (selected) => - (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase() - ) - ); - } - - this.kpiTotalClientes = allGroups.length; - this.kpiTotalLinhas = allGroups.reduce((acc, g) => acc + (g.totalLinhas || 0), 0); - this.kpiAtivas = allGroups.reduce((acc, g) => acc + (g.ativos || 0), 0); - this.kpiBloqueadas = allGroups.reduce((acc, g) => acc + (g.bloqueados || 0), 0); - - this.loadingKpis = false; - this.cdr.detectChanges(); - }, - error: () => { - this.loadingKpis = false; - } - }); + this.loadClients(); + this.refreshData(); } + toggleAdditionalService(key: AdditionalServiceKey) { + const idx = this.selectedAdditionalServices.indexOf(key); + if (idx >= 0) this.selectedAdditionalServices.splice(idx, 1); + else this.selectedAdditionalServices.push(key); + + this.expandedGroup = null; + this.groupLines = []; + this.searchResolvedClient = null; + this.page = 1; + + this.loadClients(); + this.refreshData(); + } + + isAdditionalServiceSelected(key: AdditionalServiceKey): boolean { + return this.selectedAdditionalServices.includes(key); + } + + clearAdditionalFilters() { + this.additionalMode = 'ALL'; + this.selectedAdditionalServices = []; + this.expandedGroup = null; + this.groupLines = []; + this.searchResolvedClient = null; + this.page = 1; + + this.loadClients(); + this.refreshData(); + } + + private applyBaseFilters(params: HttpParams): HttpParams { + let next = params; + + if (this.filterSkil === 'PF') next = next.set('skil', 'PESSOA FÍSICA'); + else if (this.filterSkil === 'PJ') next = next.set('skil', 'PESSOA JURÍDICA'); + else if (this.filterSkil === 'RESERVA') next = next.set('skil', 'RESERVA'); + + if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with'); + else if (this.additionalMode === 'WITHOUT') next = next.set('additionalMode', 'without'); + + if (this.selectedAdditionalServices.length > 0) { + next = next.set('additionalServices', this.selectedAdditionalServices.join(',')); + } + + return next; + } + + private getAdditionalValue(line: ApiLineList, key: AdditionalServiceKey): number { + const raw = key === 'gvd' + ? line.gestaoVozDados + : key === 'skeelo' + ? line.skeelo + : key === 'news' + ? line.vivoNewsPlus + : key === 'travel' + ? line.vivoTravelMundo + : key === 'sync' + ? line.vivoSync + : line.vivoGestaoDispositivo; + + const n = this.toNullableNumber(raw); + return n ?? 0; + } + + private hasAnyAdditional(line: ApiLineList): boolean { + return (this.getAdditionalValue(line, 'gvd') > 0) || + (this.getAdditionalValue(line, 'skeelo') > 0) || + (this.getAdditionalValue(line, 'news') > 0) || + (this.getAdditionalValue(line, 'travel') > 0) || + (this.getAdditionalValue(line, 'sync') > 0) || + (this.getAdditionalValue(line, 'dispositivo') > 0); + } + + private matchesAdditionalFilters(line: ApiLineList): boolean { + const selected = this.selectedAdditionalServices; + const hasSelected = selected.length > 0; + + if (hasSelected) { + if (this.additionalMode === 'WITHOUT') { + return selected.every((svc) => this.getAdditionalValue(line, svc) <= 0); + } + + // WITH e também ALL com serviços selecionados + return selected.some((svc) => this.getAdditionalValue(line, svc) > 0); + } + + if (this.additionalMode === 'WITH') { + return this.hasAnyAdditional(line); + } + + if (this.additionalMode === 'WITHOUT') { + return !this.hasAnyAdditional(line); + } + + return true; + } + + private applyAdditionalFiltersClientSide(lines: ApiLineList[]): ApiLineList[] { + if (!Array.isArray(lines) || lines.length === 0) return []; + return lines.filter((line) => this.matchesAdditionalFilters(line)); + } + + private loadKpis() { + const requestVersion = ++this.kpiRequestVersion; + this.loadingKpis = true; + this.cdr.detectChanges(); + + void this.loadKpisInternal(requestVersion); + } + + private async loadKpisInternal(requestVersion: number) { + try { + const groups = await this.fetchAllGroupsForKpis(); + if (requestVersion !== this.kpiRequestVersion) return; + + if (groups.length === 0) { + await this.loadKpisFromLines(requestVersion); + return; + } + + this.applyKpisFromGroups(groups); + } catch { + if (requestVersion !== this.kpiRequestVersion) return; + await this.loadKpisFromLines(requestVersion); + return; + } + + if (requestVersion !== this.kpiRequestVersion) return; + this.loadingKpis = false; + this.cdr.detectChanges(); + } + + private async fetchAllGroupsForKpis(): Promise { + if (this.hasAdditionalFiltersApplied) { + const lines = await this.fetchLinesForGrouping(); + let groups = this.buildGroupsFromLines(lines); + + if (this.searchResolvedClient) { + const target = (this.searchResolvedClient || '').trim().toUpperCase(); + groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target); + } + + if (this.selectedClients.length > 0) { + groups = groups.filter((g) => + this.selectedClients.some( + (selected) => (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase() + ) + ); + } + + return groups; + } + + let baseParams = new HttpParams(); + baseParams = this.applyBaseFilters(baseParams); + + if (!this.searchResolvedClient && this.searchTerm) { + baseParams = baseParams.set('search', this.searchTerm); + } + + const pageSize = 2000; + let page = 1; + let expectedTotal = 0; + const allGroups: ClientGroupDto[] = []; + + while (page <= 500) { + const params = baseParams + .set('page', String(page)) + .set('pageSize', String(pageSize)); + + const response = await firstValueFrom( + this.http.get>(`${this.apiBase}/groups`, { params: this.withNoCache(params) }) + ); + + const items = response?.items ?? []; + expectedTotal = this.toInt(response?.total); + allGroups.push(...items); + + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && allGroups.length >= expectedTotal) break; + + page += 1; + } + + if (this.searchResolvedClient) { + const target = (this.searchResolvedClient || '').trim().toUpperCase(); + return allGroups.filter((g) => (g.cliente || '').trim().toUpperCase() === target); + } + + if (this.selectedClients.length > 0) { + return allGroups.filter((g) => + this.selectedClients.some( + (selected) => (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase() + ) + ); + } + + return allGroups; + } + + private async loadKpisFromLines(requestVersion: number = this.kpiRequestVersion) { + try { + const lines = await this.fetchLinesForGrouping(); + let groups = this.buildGroupsFromLines(lines); + + if (this.searchResolvedClient) { + const target = (this.searchResolvedClient || '').trim().toUpperCase(); + groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target); + } else if (this.selectedClients.length > 0) { + groups = groups.filter((g) => + this.selectedClients.some( + (selected) => + (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase() + ) + ); + } + + if (requestVersion !== this.kpiRequestVersion) return; + this.applyKpisFromGroups(groups); + } catch { + if (requestVersion !== this.kpiRequestVersion) return; + this.applyKpisFromGroups([]); + } finally { + if (requestVersion !== this.kpiRequestVersion) return; + this.loadingKpis = false; + this.cdr.detectChanges(); + } + } + + private applyKpisFromGroups(groups: ClientGroupDto[]): void { + const safe = Array.isArray(groups) ? groups : []; + this.kpiTotalClientes = safe.length; + this.kpiTotalLinhas = safe.reduce((acc, group) => acc + this.toInt(group?.totalLinhas), 0); + this.kpiAtivas = safe.reduce((acc, group) => acc + this.toInt(group?.ativos), 0); + this.kpiBloqueadas = safe.reduce((acc, group) => acc + this.toInt(group?.bloqueados), 0); + } + + private loadGroups() { + const requestVersion = ++this.groupsRequestVersion; this.loading = true; const hasSelection = this.selectedClients.length > 0; const hasResolved = !!this.searchResolvedClient; + const keepCurrentPage = this.keepPageOnNextGroupsLoad; + this.keepPageOnNextGroupsLoad = false; + + if (!keepCurrentPage && this.filterSkil === 'RESERVA' && !hasSelection && !hasResolved) { + this.page = 1; + } + + if (this.hasAdditionalFiltersApplied) { + void this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion); + return; + } const pageToLoad = (hasSelection || hasResolved) ? '1' : String(this.page); const sizeToLoad = (hasSelection || hasResolved) ? '9999' : String(this.pageSize); let params = new HttpParams().set('page', pageToLoad).set('pageSize', sizeToLoad); - - if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); - else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); - else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); + params = this.applyBaseFilters(params); if (!hasResolved && this.searchTerm) params = params.set('search', this.searchTerm); @@ -608,8 +1078,10 @@ export class Geral implements AfterViewInit, OnDestroy { this.selectedClients.forEach((c) => (params = params.append('client', c))); } - this.http.get>(`${this.apiBase}/groups`, { params }).subscribe({ + this.http.get>(`${this.apiBase}/groups`, { params: this.withNoCache(params) }).subscribe({ next: (res) => { + if (requestVersion !== this.groupsRequestVersion) return; + let items = res.items || []; if (hasResolved) { @@ -628,17 +1100,141 @@ export class Geral implements AfterViewInit, OnDestroy { this.total = res.total; } - this.clientGroups = items; + if (items.length === 0) { + this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion); + return; + } + + this.clientGroups = this.sortGroupsWithReservaFirst(items); this.loading = false; this.cdr.detectChanges(); }, error: () => { - this.loading = false; - this.showToast('Erro ao carregar grupos.'); + if (requestVersion !== this.groupsRequestVersion) return; + this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion); } }); } + private async loadGroupsFromLines(hasSelection: boolean, hasResolved: boolean, requestVersion: number) { + try { + const lines = await this.fetchLinesForGrouping(); + if (requestVersion !== this.groupsRequestVersion) return; + let groups = this.buildGroupsFromLines(lines); + + if (hasResolved) { + const target = (this.searchResolvedClient || '').trim().toUpperCase(); + groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target); + } else if (hasSelection) { + groups = groups.filter((g) => + this.selectedClients.some( + (selected) => + (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase() + ) + ); + } + + this.total = groups.length; + + if (!hasSelection && !hasResolved) { + const start = Math.max(0, (this.page - 1) * this.pageSize); + groups = groups.slice(start, start + this.pageSize); + } + + this.clientGroups = this.sortGroupsWithReservaFirst(groups); + } catch { + if (requestVersion !== this.groupsRequestVersion) return; + this.clientGroups = []; + this.total = 0; + this.showToast('Erro ao carregar grupos.'); + } finally { + if (requestVersion !== this.groupsRequestVersion) return; + this.loading = false; + this.cdr.detectChanges(); + } + } + + private async fetchLinesForGrouping(): Promise { + let baseParams = new HttpParams() + .set('sortBy', 'cliente') + .set('sortDir', 'asc'); + baseParams = this.applyBaseFilters(baseParams); + + if (this.searchResolvedClient) { + baseParams = baseParams.set('client', this.searchResolvedClient); + } else if (this.searchTerm) { + baseParams = baseParams.set('search', this.searchTerm); + } + + const pageSize = 5000; + let page = 1; + let expectedTotal = 0; + const allLines: ApiLineList[] = []; + + while (page <= 500) { + const params = baseParams + .set('page', String(page)) + .set('pageSize', String(pageSize)); + + const response = await firstValueFrom( + this.http.get>(this.apiBase, { params: this.withNoCache(params) }) + ); + + const items = response?.items ?? []; + expectedTotal = this.toInt(response?.total); + allLines.push(...items); + + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && allLines.length >= expectedTotal) break; + + page += 1; + } + + return this.applyAdditionalFiltersClientSide(allLines); + } + + private buildGroupsFromLines(lines: ApiLineList[]): ClientGroupDto[] { + const grouped = new Map(); + const fallbackClient = this.filterSkil === 'RESERVA' ? 'RESERVA' : 'SEM CLIENTE'; + + for (const row of lines ?? []) { + const client = ((row?.cliente ?? '').toString().trim()) || fallbackClient; + const key = client.toUpperCase(); + + let group = grouped.get(key); + if (!group) { + group = { + cliente: client, + totalLinhas: 0, + ativos: 0, + bloqueados: 0 + }; + grouped.set(key, group); + } + + group.totalLinhas += 1; + + const status = ((row?.status ?? '').toString().trim()).toLowerCase(); + if (status.includes('ativo')) group.ativos += 1; + if (status.includes('bloque') || status.includes('perda') || status.includes('roubo')) { + group.bloqueados += 1; + } + } + + return this.sortGroupsWithReservaFirst(Array.from(grouped.values())); + } + + private sortGroupsWithReservaFirst(groups: ClientGroupDto[]): ClientGroupDto[] { + const list = Array.isArray(groups) ? [...groups] : []; + return list.sort((a, b) => { + const aReserva = (a?.cliente || '').trim().localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0; + const bReserva = (b?.cliente || '').trim().localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0; + if (aReserva !== bReserva) return aReserva ? -1 : 1; + return (a?.cliente || '').localeCompare((b?.cliente || ''), 'pt-BR', { sensitivity: 'base' }); + }); + } + toggleGroup(clientName: string) { if (this.expandedGroup === clientName) { this.expandedGroup = null; @@ -653,6 +1249,7 @@ export class Geral implements AfterViewInit, OnDestroy { } fetchGroupLines(clientName: string, search?: string) { + const requestVersion = ++this.linesRequestVersion; this.groupLines = []; this.loadingLines = true; @@ -662,16 +1259,15 @@ export class Geral implements AfterViewInit, OnDestroy { .set('pageSize', '500') .set('sortBy', 'item') .set('sortDir', 'asc'); - - if (this.filterSkil === 'PF') params = params.set('skil', 'PESSOA FÍSICA'); - else if (this.filterSkil === 'PJ') params = params.set('skil', 'PESSOA JURÍDICA'); - else if (this.filterSkil === 'RESERVA') params = params.set('skil', 'RESERVA'); + params = this.applyBaseFilters(params); if (search) params = params.set('search', search); - this.http.get>(this.apiBase, { params }).subscribe({ + this.http.get>(this.apiBase, { params: this.withNoCache(params) }).subscribe({ next: (res) => { - this.groupLines = (res.items ?? []).map((x) => ({ + if (requestVersion !== this.linesRequestVersion) return; + const filteredItems = this.applyAdditionalFiltersClientSide(res.items ?? []); + this.groupLines = filteredItems.map((x) => ({ id: x.id, item: String(x.item ?? ''), linha: x.linha ?? '', @@ -684,6 +1280,7 @@ export class Geral implements AfterViewInit, OnDestroy { this.loadingLines = false; }, error: () => { + if (requestVersion !== this.linesRequestVersion) return; this.loadingLines = false; this.showToast('Erro ao carregar linhas do grupo.'); } @@ -691,13 +1288,28 @@ export class Geral implements AfterViewInit, OnDestroy { } toggleClientMenu() { + if (!this.showClientMenu) this.showAdditionalMenu = false; this.showClientMenu = !this.showClientMenu; } + toggleAdditionalMenu() { + if (!this.showAdditionalMenu) this.showClientMenu = false; + this.showAdditionalMenu = !this.showAdditionalMenu; + } + closeClientDropdown() { this.showClientMenu = false; } + closeAdditionalDropdown() { + this.showAdditionalMenu = false; + } + + closeFilterDropdowns() { + this.showClientMenu = false; + this.showAdditionalMenu = false; + } + selectClient(client: string | null) { if (client === null) { this.selectedClients = []; @@ -762,7 +1374,7 @@ export class Geral implements AfterViewInit, OnDestroy { goToPage(p: number) { this.page = Math.max(1, Math.min(this.totalPages, p)); - this.refreshData(); + this.refreshData({ keepCurrentPage: true }); } trackById(_: number, row: LineRow) { @@ -823,12 +1435,19 @@ export class Geral implements AfterViewInit, OnDestroy { } async onImportExcel() { + if (!this.isAdmin) { + await this.showToast('Você não tem permissão para importar planilha.'); + return; + } + if (!this.excelInput?.nativeElement) return; this.excelInput.nativeElement.value = ''; this.excelInput.nativeElement.click(); } onExcelSelected(ev: Event) { + if (!this.isAdmin) return; + const file = (ev.target as HTMLInputElement).files?.[0]; if (!file) return; @@ -888,6 +1507,7 @@ export class Geral implements AfterViewInit, OnDestroy { this.http.get(`${this.apiBase}/${r.id}`).subscribe({ next: (d) => { this.editModel = this.toEditModel(d); + this.syncContaEmpresaSelection(this.editModel); this.cdr.detectChanges(); }, error: async () => { @@ -897,6 +1517,30 @@ export class Geral implements AfterViewInit, OnDestroy { }); } + onPlanoChange(isEdit: boolean) { + const model = isEdit ? this.editModel : this.createModel; + if (!model) return; + + const plan = (model.planoContrato ?? '').toString().trim(); + if (!plan) return; + + const suggestion = this.planAutoFill.suggest(plan); + if (!suggestion) return; + + if (suggestion.franquiaGb != null) { + model.franquiaVivo = suggestion.franquiaGb; + if (model.franquiaLine === null || model.franquiaLine === undefined || model.franquiaLine === '') { + model.franquiaLine = suggestion.franquiaGb; + } + } + + if (suggestion.valorPlano != null) { + model.valorPlanoVivo = suggestion.valorPlano; + } + + this.onFinancialChange(isEdit); + } + calculateFinancials(model: any) { if (!model) return; @@ -912,8 +1556,9 @@ export class Geral implements AfterViewInit, OnDestroy { const news = parse(model.vivoNewsPlus); const travel = parse(model.vivoTravelMundo); const gestaoDisp = parse(model.vivoGestaoDispositivo); + const vivoSync = parse(model.vivoSync); - const totalVivo = valorPlano + gestaoVoz + skeelo + news + travel + gestaoDisp; + const totalVivo = valorPlano + gestaoVoz + skeelo + news + travel + gestaoDisp + vivoSync; model.valorContratoVivo = parseFloat(totalVivo.toFixed(2)); const totalLineManual = parse(model.valorContratoLine); @@ -934,12 +1579,16 @@ export class Geral implements AfterViewInit, OnDestroy { this.editSaving = true; this.calculateFinancials(this.editModel); + const { contaEmpresa: _contaEmpresa, ...editModelPayload } = this.editModel; + const payload: UpdateMobileLineRequest = { - ...this.editModel, + ...editModelPayload, item: this.toInt(this.editModel.item), dataBloqueio: this.dateInputToIso(this.editModel.dataBloqueio), dataEntregaOpera: this.dateInputToIso(this.editModel.dataEntregaOpera), dataEntregaCliente: this.dateInputToIso(this.editModel.dataEntregaCliente), + dtEfetivacaoServico: this.dateInputToIso(this.editModel.dtEfetivacaoServico), + dtTerminoFidelizacao: this.dateInputToIso(this.editModel.dtTerminoFidelizacao), vencConta: (this.editModel.vencConta ?? '').toString(), franquiaVivo: this.toNullableNumber(this.editModel.franquiaVivo), valorPlanoVivo: this.toNullableNumber(this.editModel.valorPlanoVivo), @@ -948,13 +1597,15 @@ export class Geral implements AfterViewInit, OnDestroy { vivoNewsPlus: this.toNullableNumber(this.editModel.vivoNewsPlus), vivoTravelMundo: this.toNullableNumber(this.editModel.vivoTravelMundo), vivoGestaoDispositivo: this.toNullableNumber(this.editModel.vivoGestaoDispositivo), + vivoSync: this.toNullableNumber(this.editModel.vivoSync), valorContratoVivo: this.toNullableNumber(this.editModel.valorContratoVivo), franquiaLine: this.toNullableNumber(this.editModel.franquiaLine), franquiaGestao: this.toNullableNumber(this.editModel.franquiaGestao), locacaoAp: this.toNullableNumber(this.editModel.locacaoAp), valorContratoLine: this.toNullableNumber(this.editModel.valorContratoLine), desconto: this.toNullableNumber(this.editModel.desconto), - lucro: this.toNullableNumber(this.editModel.lucro) + lucro: this.toNullableNumber(this.editModel.lucro), + tipoDeChip: (this.editModel.tipoDeChip ?? '').toString() }; this.http.put(`${this.apiBase}/${this.editingId}`, payload).subscribe({ @@ -985,6 +1636,11 @@ export class Geral implements AfterViewInit, OnDestroy { } async onRemover(r: LineRow, fromGroup = false) { + if (!this.isAdmin) { + await this.showToast('Apenas administradores podem remover linhas.'); + return; + } + if (!confirm(`Remover linha ${r.linha}?`)) return; this.loading = true; @@ -1028,6 +1684,8 @@ export class Geral implements AfterViewInit, OnDestroy { if (this.filterSkil === 'PJ') this.createModel.skil = 'PESSOA JURÍDICA'; else if (this.filterSkil === 'RESERVA') this.createModel.skil = 'RESERVA'; + this.syncContaEmpresaSelection(this.createModel); + this.createOpen = true; this.cdr.detectChanges(); } @@ -1037,8 +1695,10 @@ export class Geral implements AfterViewInit, OnDestroy { cliente: '', docType: 'PF', docNumber: '', + contaEmpresa: '', linha: '', chip: '', + tipoDeChip: '', usuario: '', status: '', planoContrato: '', @@ -1052,6 +1712,8 @@ export class Geral implements AfterViewInit, OnDestroy { dataBloqueio: '', dataEntregaOpera: '', dataEntregaCliente: '', + dtEfetivacaoServico: '', + dtTerminoFidelizacao: '', franquiaVivo: null, valorPlanoVivo: null, gestaoVozDados: null, @@ -1059,6 +1721,7 @@ export class Geral implements AfterViewInit, OnDestroy { vivoNewsPlus: null, vivoTravelMundo: null, vivoGestaoDispositivo: null, + vivoSync: null, valorContratoVivo: null, franquiaLine: null, franquiaGestao: null, @@ -1070,6 +1733,19 @@ export class Geral implements AfterViewInit, OnDestroy { this.createSaving = false; } + onContaEmpresaChange(isEdit: boolean) { + const model = isEdit ? this.editModel : this.createModel; + if (!model) return; + + const contas = this.getContasByEmpresa(model.contaEmpresa); + const selectedConta = (model.conta ?? '').toString().trim(); + + if (!selectedConta) return; + + const hasMatch = contas.some((c) => this.sameConta(c, selectedConta)); + if (!hasMatch) model.conta = ''; + } + onDocTypeChange() { this.createModel.docNumber = ''; this.createModel.skil = this.createModel.docType === 'PF' ? 'PESSOA FÍSICA' : 'PESSOA JURÍDICA'; @@ -1104,6 +1780,10 @@ export class Geral implements AfterViewInit, OnDestroy { } } + if (!this.createModel.contaEmpresa) { + this.showToast('Selecione a Empresa (Conta).'); + return; + } if (!this.createModel.conta) { this.showToast('Selecione uma Conta.'); return; @@ -1124,16 +1804,28 @@ export class Geral implements AfterViewInit, OnDestroy { this.showToast('Selecione um Plano.'); return; } + if (!this.createModel.dtEfetivacaoServico) { + this.showToast('A Dt. Efetivação Serviço é obrigatória.'); + return; + } + if (!this.createModel.dtTerminoFidelizacao) { + this.showToast('A Dt. Término Fidelização é obrigatória.'); + return; + } this.createSaving = true; this.calculateFinancials(this.createModel); + const { contaEmpresa: _contaEmpresa, ...createModelPayload } = this.createModel; + const payload: CreateMobileLineRequest = { - ...this.createModel, + ...createModelPayload, item: Number(this.createModel.item), dataBloqueio: this.dateInputToIso(this.createModel.dataBloqueio), dataEntregaOpera: this.dateInputToIso(this.createModel.dataEntregaOpera), dataEntregaCliente: this.dateInputToIso(this.createModel.dataEntregaCliente), + dtEfetivacaoServico: this.dateInputToIso(this.createModel.dtEfetivacaoServico), + dtTerminoFidelizacao: this.dateInputToIso(this.createModel.dtTerminoFidelizacao), franquiaVivo: this.toNullableNumber(this.createModel.franquiaVivo), valorPlanoVivo: this.toNullableNumber(this.createModel.valorPlanoVivo), gestaoVozDados: this.toNullableNumber(this.createModel.gestaoVozDados), @@ -1141,13 +1833,15 @@ export class Geral implements AfterViewInit, OnDestroy { vivoNewsPlus: this.toNullableNumber(this.createModel.vivoNewsPlus), vivoTravelMundo: this.toNullableNumber(this.createModel.vivoTravelMundo), vivoGestaoDispositivo: this.toNullableNumber(this.createModel.vivoGestaoDispositivo), + vivoSync: this.toNullableNumber(this.createModel.vivoSync), valorContratoVivo: this.toNullableNumber(this.createModel.valorContratoVivo), franquiaLine: this.toNullableNumber(this.createModel.franquiaLine), franquiaGestao: this.toNullableNumber(this.createModel.franquiaGestao), locacaoAp: this.toNullableNumber(this.createModel.locacaoAp), valorContratoLine: this.toNullableNumber(this.createModel.valorContratoLine), desconto: this.toNullableNumber(this.createModel.desconto), - lucro: this.toNullableNumber(this.createModel.lucro) + lucro: this.toNullableNumber(this.createModel.lucro), + tipoDeChip: (this.createModel.tipoDeChip ?? '').toString() }; @@ -1239,6 +1933,8 @@ export class Geral implements AfterViewInit, OnDestroy { dataBloqueio: this.isoToDateInput(d.dataBloqueio), dataEntregaOpera: this.isoToDateInput(d.dataEntregaOpera), dataEntregaCliente: this.isoToDateInput(d.dataEntregaCliente), + dtEfetivacaoServico: this.isoToDateInput(d.dtEfetivacaoServico), + dtTerminoFidelizacao: this.isoToDateInput(d.dtTerminoFidelizacao), franquiaVivo: d.franquiaVivo ?? null, valorPlanoVivo: d.valorPlanoVivo ?? null, @@ -1247,6 +1943,7 @@ export class Geral implements AfterViewInit, OnDestroy { vivoNewsPlus: d.vivoNewsPlus ?? null, vivoTravelMundo: d.vivoTravelMundo ?? null, vivoGestaoDispositivo: d.vivoGestaoDispositivo ?? null, + vivoSync: d.vivoSync ?? null, valorContratoVivo: d.valorContratoVivo ?? null, franquiaLine: d.franquiaLine ?? null, @@ -1255,7 +1952,9 @@ export class Geral implements AfterViewInit, OnDestroy { valorContratoLine: d.valorContratoLine ?? null, desconto: d.desconto ?? null, - lucro: d.lucro ?? null + lucro: d.lucro ?? null, + tipoDeChip: d.tipoDeChip ?? '', + contaEmpresa: this.findEmpresaByConta(d.conta) }; } @@ -1295,4 +1994,80 @@ export class Geral implements AfterViewInit, OnDestroy { if (!v) return list; return list.includes(v) ? list : [v, ...list]; } + + private mergeOptionList(base: string[], extra: string[]): string[] { + const result: string[] = [...base]; + const seen = new Set(base.map((x) => x.trim()).filter(Boolean)); + + extra.forEach((raw) => { + const v = (raw ?? '').toString().trim(); + if (!v || seen.has(v)) return; + seen.add(v); + result.push(v); + }); + + return result; + } + + private normalizeAccountCompanies(data: AccountCompanyOption[] | null | undefined): AccountCompanyOption[] { + if (!Array.isArray(data)) return []; + + const result: AccountCompanyOption[] = []; + + data.forEach((item) => { + const empresa = (item?.empresa ?? '').toString().trim(); + if (!empresa) return; + + const contas = this.mergeOptionList([], (item?.contas ?? []).map((x) => (x ?? '').toString().trim())); + result.push({ empresa, contas }); + }); + + return result; + } + + private getAllContas(): string[] { + const all = this.accountCompanies.flatMap((x) => x.contas ?? []); + return this.mergeOptionList([], all); + } + + private getContasByEmpresa(empresa: any): string[] { + const target = (empresa ?? '').toString().trim(); + if (!target) return []; + + const found = this.accountCompanies.find((x) => + x.empresa.localeCompare(target, 'pt-BR', { sensitivity: 'base' }) === 0 + ); + return found ? [...found.contas] : []; + } + + private findEmpresaByConta(conta: any): string { + const target = this.normalizeConta(conta); + if (!target) return ''; + + const found = this.accountCompanies.find((group) => + (group.contas ?? []).some((c) => this.sameConta(c, target)) + ); + return found?.empresa ?? ''; + } + + private normalizeConta(value: any): string { + const raw = (value ?? '').toString().trim(); + if (!raw) return ''; + if (!/^\d+$/.test(raw)) return raw.toUpperCase(); + const noLeadingZero = raw.replace(/^0+/, ''); + return noLeadingZero || '0'; + } + + private sameConta(a: any, b: any): boolean { + return this.normalizeConta(a) === this.normalizeConta(b); + } + + private syncContaEmpresaSelection(model: any) { + if (!model) return; + + const empresaAtual = (model.contaEmpresa ?? '').toString().trim(); + if (empresaAtual) return; + + model.contaEmpresa = this.findEmpresaByConta(model.conta); + } } diff --git a/src/app/pages/historico/historico.html b/src/app/pages/historico/historico.html new file mode 100644 index 0000000..fcc32b6 --- /dev/null +++ b/src/app/pages/historico/historico.html @@ -0,0 +1,259 @@ +
+ +
+ +
+ + + + + +
+
+
+
+
+ Auditoria +
+ +
+
Histórico
+ Registros de alterações feitas no sistema. +
+ +
+ +
+
+ +
+
+
+ + Filtros +
+
+ + +
+
+ +
+ +
+ + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ +
+ + + + + +
+
+
+
+
+ +
+
+
+ +
+ + + +
+ Nenhum log encontrado para os filtros atuais. +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Data/HoraUsuárioPáginaAçãoItem/Entidade
{{ formatDateTime(log.occurredAtUtc) }} +
+ {{ displayUserName(log) }} + {{ log.userEmail || '-' }} +
+
{{ log.page || '-' }} + {{ formatAction(log.action) }} + +
+
+ {{ displayEntity(log) }} +
+ +
+ {{ log.entityId }} +
+
+
+
+ Mudanças +
+
+
+
+ {{ change.field }} + + {{ changeTypeLabel(change.changeType) }} + +
+
+ {{ formatChangeValue(change.oldValue) }} + + {{ formatChangeValue(change.newValue) }} +
+
+
+ +
Sem mudanças registradas.
+
+
+ +
+
+ Detalhes técnicos +
+
+
+ Método + {{ log.requestMethod || '-' }} +
+
+ Endpoint + {{ log.requestPath || '-' }} +
+
+ IP + {{ log.ipAddress }} +
+
+
+
+
+
+
+ + +
+
+
diff --git a/src/app/pages/historico/historico.scss b/src/app/pages/historico/historico.scss new file mode 100644 index 0000000..3dbbf74 --- /dev/null +++ b/src/app/pages/historico/historico.scss @@ -0,0 +1,679 @@ +:host { + --brand: #E33DCF; + --blue: #030FAA; + --text: #111214; + --muted: rgba(17, 18, 20, 0.65); + + --success-bg: rgba(25, 135, 84, 0.12); + --success-text: #198754; + --info-bg: rgba(3, 15, 170, 0.1); + --info-text: #030FAA; + --danger-bg: rgba(220, 53, 69, 0.12); + --danger-text: #dc3545; + + --radius-xl: 22px; + --radius-lg: 16px; + --shadow-card: 0 22px 46px rgba(17, 18, 20, 0.10); + --glass-bg: rgba(255, 255, 255, 0.82); + --glass-border: 1px solid rgba(227, 61, 207, 0.16); + + display: block; + font-family: 'Inter', sans-serif; + color: var(--text); + box-sizing: border-box; +} + +.historico-page { + min-height: 100vh; + padding: 0 12px; + display: flex; + align-items: flex-start; + justify-content: center; + position: relative; + overflow-y: auto; + background: + radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%), + radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%); + + &::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: rgba(255, 255, 255, 0.25); + } +} + +.page-blob { + position: fixed; + pointer-events: none; + border-radius: 999px; + filter: blur(34px); + opacity: 0.55; + z-index: 0; + background: radial-gradient(circle at 30% 30%, rgba(227,61,207,0.55), rgba(227,61,207,0.06)); + animation: floaty 10s ease-in-out infinite; + &.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; } + &.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; } + &.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; } + &.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: .45; } +} + +@keyframes floaty { + 0% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(18px, 10px) scale(1.03); } + 100% { transform: translate(0, 0) scale(1); } +} + +.container-geral-responsive { + width: 100%; + max-width: 1400px !important; + width: 98% !important; + position: relative; + z-index: 1; + margin-top: 40px; + margin-bottom: 200px; +} + +.geral-card { + border-radius: var(--radius-xl); + overflow: hidden; + background: var(--glass-bg); + border: var(--glass-border); + backdrop-filter: blur(12px); + box-shadow: var(--shadow-card); + position: relative; + display: flex; + flex-direction: column; + min-height: 80vh; + &::before { + content: ''; + position: absolute; + inset: 1px; + border-radius: calc(var(--radius-xl) - 1px); + pointer-events: none; + border: 1px solid rgba(255, 255, 255, 0.65); + opacity: 0.75; + } +} + +.geral-header { + padding: 16px 24px; + border-bottom: 1px solid rgba(17, 18, 20, 0.06); + background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.2)); + flex-shrink: 0; +} + +.header-row-top { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 12px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + text-align: center; + gap: 16px; + .title-badge { justify-self: center; margin-bottom: 8px; } + .header-actions { justify-self: center; } + } +} + +.title-badge { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(227, 61, 207, 0.22); + backdrop-filter: blur(10px); + color: var(--text); + font-size: 13px; + font-weight: 800; + i { color: var(--brand); } +} + +.header-title { + justify-self: center; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.title { + font-size: 26px; + font-weight: 950; + letter-spacing: -0.3px; + color: var(--text); + margin-top: 10px; + margin-bottom: 0; +} + +.subtitle { + color: rgba(17, 18, 20, 0.65); + font-weight: 700; +} + +.header-actions { + justify-self: end; +} + +.btn-brand { + background-color: var(--brand); + border-color: var(--brand); + color: #fff; + font-weight: 900; + border-radius: 12px; + transition: transform 0.2s, box-shadow 0.2s; + &:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); filter: brightness(1.05); } +} + +.filters-card { + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 16px; + padding: 16px; + display: grid; + gap: 14px; + box-shadow: 0 14px 28px rgba(17, 18, 20, 0.08); +} + +.filters-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.filters-title { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 900; + font-size: 14px; + color: rgba(17, 18, 20, 0.82); +} + +.filters-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.filters-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 12px; +} + +.filter-field { + display: grid; + gap: 6px; + + label { + font-size: 11px; + font-weight: 800; + color: rgba(17, 18, 20, 0.6); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + input { + height: 40px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.15); + padding: 0 12px; + font-size: 14px; + background: #fff; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + } + + input:focus { + outline: none; + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.12); + } +} + +.filter-search { + grid-column: span 2; +} + +.search-group { + max-width: 270px; + border-radius: 12px; + overflow: hidden; + display: flex; + align-items: stretch; + background: #fff; + border: 1px solid rgba(17, 18, 20, 0.15); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); + transition: all 0.2s ease; + + &:focus-within { + border-color: var(--brand); + box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); + transform: translateY(-1px); + } + + .input-group-text { + background: transparent; + border: none; + color: rgba(17, 18, 20, 0.5); + padding-left: 14px; + padding-right: 8px; + display: flex; + align-items: center; + } + + .form-control { + border: none; + background: transparent; + height: auto; + padding: 10px 0; + font-size: 0.9rem; + color: var(--text); + box-shadow: none; + + &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } + &:focus { outline: none; } + } + + .btn-clear { + border: none; + background: transparent; + color: rgba(17, 18, 20, 0.45); + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 12px; + cursor: pointer; + transition: color 0.2s ease; + + &:hover { color: #dc3545; } + } +} + +.btn-primary, +.btn-ghost { + height: 38px; + border-radius: 10px; + border: none; + font-weight: 700; + font-size: 12px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 14px; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.btn-primary { + background: var(--blue); + color: #fff; + box-shadow: 0 8px 18px rgba(3, 15, 170, 0.18); +} + +.btn-ghost { + background: #fff; + color: rgba(17, 18, 20, 0.86); + border: 1px solid rgba(15, 23, 42, 0.12); +} + +.select-wrapper { position: relative; display: inline-block; min-width: 90px; } + +.select-glass { + background: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(17, 18, 20, 0.12); + border-radius: 10px; + color: var(--blue); + font-weight: 800; +} + +.geral-body { + padding: 0; + background: transparent; + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.table-wrap { + overflow-x: auto; + overflow-y: auto; + height: 100%; +} + +.table-modern { + width: 100%; + min-width: 1200px !important; + border-collapse: separate; + border-spacing: 0; + + thead th { + position: sticky; + top: 0; + z-index: 10; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + border-bottom: 2px solid rgba(227, 61, 207, 0.15); + padding: 12px; + color: rgba(17, 18, 20, 0.7); + font-size: 0.8rem; + font-weight: 950; + letter-spacing: 0.05em; + text-transform: uppercase; + white-space: nowrap; + text-align: center; + } + + tbody tr { + transition: background-color 0.2s; + border-bottom: 1px solid rgba(17,18,20,0.05); + + &:hover { + background-color: rgba(227, 61, 207, 0.05); + } + + td { border-bottom: 1px solid rgba(17,18,20,0.04); } + } + + td { + padding: 12px; + vertical-align: middle; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.875rem; + color: var(--text); + text-align: center; + } +} + +.table-modern th:nth-child(5), +.table-modern td:nth-child(5) { + text-align: left; +} + +.table-modern th:nth-child(2), +.table-modern td:nth-child(2) { + min-width: 260px; +} + +.table-row-item.expanded { + background: rgba(227, 61, 207, 0.06); +} + +.td-clip { + overflow: hidden; + text-overflow: ellipsis; + max-width: 240px; +} + +.empty-group { + background: rgba(255,255,255,0.7); + border: 1px dashed rgba(17,18,20,0.12); + border-radius: 16px; + padding: 18px; + text-align: center; + font-weight: 800; + color: var(--muted); + margin: 16px; +} + +.user-cell { + display: grid; + gap: 2px; + justify-items: center; +} + +.user-name { + font-weight: 800; + color: var(--text); +} + +.user-email { + color: rgba(17, 18, 20, 0.55); + font-size: 0.75rem; +} + +.entity-cell { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.entity-label { + font-weight: 700; + color: var(--text); +} + +.entity-id { + display: block; + font-size: 0.75rem; + color: rgba(17, 18, 20, 0.5); + word-break: break-all; +} + +.expand-btn { + border: none; + background: rgba(3, 15, 170, 0.08); + color: var(--blue); + width: 32px; + height: 32px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + transition: transform 0.15s ease, background 0.15s ease; + &:hover { transform: translateY(-1px); background: rgba(227, 61, 207, 0.12); color: var(--brand); } +} + +.badge-action { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 10px; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.action-create { + background: var(--success-bg); + color: var(--success-text); +} + +.action-update { + background: var(--info-bg); + color: var(--info-text); +} + +.action-delete { + background: var(--danger-bg); + color: var(--danger-text); +} + +.action-default { + background: rgba(17,18,20,0.08); + color: rgba(17,18,20,0.7); +} + +.details-row td { + background: rgba(255, 255, 255, 0.95); + padding: 0 12px 16px; + white-space: normal; + overflow: visible; +} + +.details-panel { + border-radius: 16px; + border: 1px solid rgba(17, 18, 20, 0.08); + background: #fff; + padding: 16px; + display: grid; + gap: 16px; +} + +.details-section { + display: grid; + gap: 12px; +} + +.section-title { + font-weight: 900; + font-size: 0.9rem; + color: var(--text); + display: inline-flex; + align-items: center; + gap: 8px; +} + +.changes-list { + display: grid; + gap: 10px; +} + +.change-item { + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 12px; + padding: 10px 12px; + background: rgba(255, 255, 255, 0.9); + display: grid; + gap: 6px; +} + +.change-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.change-field { + font-weight: 800; + color: var(--text); +} + +.change-type { + font-size: 0.7rem; + font-weight: 900; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 999px; + letter-spacing: 0.04em; +} + +.change-added { background: var(--success-bg); color: var(--success-text); } +.change-removed { background: var(--danger-bg); color: var(--danger-text); } +.change-modified { background: var(--info-bg); color: var(--info-text); } + +.change-values { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + font-size: 0.8rem; + color: rgba(17, 18, 20, 0.7); +} + +.change-values .old { + color: rgba(17, 18, 20, 0.6); +} + +.change-values .new { + color: var(--text); + font-weight: 700; +} + +.empty-state { + background: rgba(17, 18, 20, 0.04); + border: 1px dashed rgba(17, 18, 20, 0.1); + border-radius: 12px; + padding: 12px; + font-weight: 700; + color: rgba(17, 18, 20, 0.6); +} + +.tech-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; +} + +.tech-item { + display: grid; + gap: 4px; +} + +.tech-label { + font-size: 0.7rem; + font-weight: 800; + text-transform: uppercase; + color: rgba(17, 18, 20, 0.55); +} + +.tech-value { + font-weight: 700; + color: var(--text); + word-break: break-word; +} + +.geral-footer { + padding: 12px 20px 18px; + border-top: 1px solid rgba(17,18,20,0.06); + background: rgba(255, 255, 255, 0.6); + display: grid; + gap: 12px; +} + +.footer-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.pagination-modern .page-link { + color: var(--blue); + font-weight: 900; + border-radius: 10px; + border: 1px solid rgba(17,18,20,0.1); + background: rgba(255,255,255,0.6); + margin: 0 2px; + &:hover { transform: translateY(-1px); border-color: var(--brand); color: var(--brand); } +} + +.pagination-modern .page-item.active .page-link { + background-color: var(--blue); + border-color: var(--blue); + color: #fff; +} + +.text-brand { color: var(--brand) !important; } + +@media (max-width: 1100px) { + .filters-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} + +@media (max-width: 900px) { + .filters-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 700px) { + .filters-grid { grid-template-columns: 1fr; } + .search-group { width: 100%; max-width: 100%; } + .entity-cell { flex-direction: column; align-items: flex-start; } + .expand-btn { align-self: flex-end; } +} diff --git a/src/app/pages/historico/historico.ts b/src/app/pages/historico/historico.ts new file mode 100644 index 0000000..e929105 --- /dev/null +++ b/src/app/pages/historico/historico.ts @@ -0,0 +1,275 @@ +import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PLATFORM_ID } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; + +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { HistoricoService, AuditLogDto, AuditChangeType, HistoricoQuery } from '../../services/historico.service'; + +interface SelectOption { + value: string; + label: string; +} + +@Component({ + selector: 'app-historico', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './historico.html', + styleUrls: ['./historico.scss'], +}) +export class Historico implements OnInit { + @ViewChild('successToast', { static: false }) successToast!: ElementRef; + + logs: AuditLogDto[] = []; + loading = false; + error = false; + errorMsg = ''; + toastMessage = ''; + + expandedLogId: string | null = null; + + page = 1; + pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; + total = 0; + + filterPageName = ''; + filterAction = ''; + filterUserId = ''; + filterSearch = ''; + dateFrom = ''; + dateTo = ''; + + readonly pageOptions: SelectOption[] = [ + { value: '', label: 'Todas as páginas' }, + { value: 'Geral', label: 'Geral' }, + { value: 'Mureg', label: 'Mureg' }, + { value: 'Faturamento', label: 'Faturamento' }, + { value: 'Parcelamentos', label: 'Parcelamentos' }, + { value: 'Dados e Usuários', label: 'Dados PF/PJ' }, + { value: 'Vigência', label: 'Vigência' }, + { value: 'Chips Virgens e Recebidos', label: 'Chips Virgens e Recebidos' }, + { value: 'Troca de número', label: 'Troca de número' }, + ]; + + readonly actionOptions: SelectOption[] = [ + { value: '', label: 'Todas as ações' }, + { value: 'CREATE', label: 'Criação' }, + { value: 'UPDATE', label: 'Atualização' }, + { value: 'DELETE', label: 'Exclusão' }, + ]; + + private searchTimer: any = null; + + constructor( + private historicoService: HistoricoService, + private cdr: ChangeDetectorRef, + @Inject(PLATFORM_ID) private platformId: object + ) {} + + ngOnInit(): void { + this.fetch(1); + } + + refresh(): void { + this.fetch(1); + } + + applyFilters(): void { + this.page = 1; + this.fetch(); + } + + clearFilters(): void { + this.filterPageName = ''; + this.filterAction = ''; + this.filterUserId = ''; + this.filterSearch = ''; + this.dateFrom = ''; + this.dateTo = ''; + this.page = 1; + this.fetch(); + } + + clearSearch(): void { + this.filterSearch = ''; + this.page = 1; + this.fetch(); + } + + onSearchChange(): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + this.searchTimer = setTimeout(() => { + this.page = 1; + this.fetch(); + }, 300); + } + + onPageSizeChange(): void { + this.page = 1; + this.fetch(); + } + + goToPage(p: number): void { + this.page = Math.max(1, Math.min(this.totalPages, p)); + this.fetch(); + } + + get totalPages(): number { + return Math.ceil((this.total || 0) / this.pageSize) || 1; + } + + get pageNumbers(): number[] { + const total = this.totalPages; + const current = this.page; + const max = 5; + let start = Math.max(1, current - 2); + let end = Math.min(total, start + (max - 1)); + start = Math.max(1, end - (max - 1)); + + const pages: number[] = []; + for (let i = start; i <= end; i++) pages.push(i); + return pages; + } + + get pageStart(): number { + return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + } + + get pageEnd(): number { + if (this.total === 0) return 0; + return Math.min(this.page * this.pageSize, this.total); + } + + toggleDetails(log: AuditLogDto, event?: Event): void { + if (event) event.stopPropagation(); + this.expandedLogId = this.expandedLogId === log.id ? null : log.id; + } + + formatDateTime(value?: string | null): string { + if (!value) return '-'; + const d = new Date(value); + if (isNaN(d.getTime())) return '-'; + return d.toLocaleString('pt-BR'); + } + + displayUserName(log: AuditLogDto): string { + const name = (log.userName || '').trim(); + return name ? name : 'SISTEMA'; + } + + displayEntity(log: AuditLogDto): string { + const label = (log.entityLabel || '').trim(); + if (label) return label; + return log.entityName || '-'; + } + + formatAction(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (!value) return '-'; + if (value === 'CREATE') return 'Criação'; + if (value === 'UPDATE') return 'Atualização'; + if (value === 'DELETE') return 'Exclusão'; + return 'Outro'; + } + + actionClass(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (value === 'CREATE') return 'action-create'; + if (value === 'UPDATE') return 'action-update'; + if (value === 'DELETE') return 'action-delete'; + return 'action-default'; + } + + changeTypeLabel(type?: AuditChangeType | string | null): string { + if (!type) return 'Alterado'; + if (type === 'added') return 'Adicionado'; + if (type === 'removed') return 'Removido'; + return 'Alterado'; + } + + changeTypeClass(type?: AuditChangeType | string | null): string { + if (type === 'added') return 'change-added'; + if (type === 'removed') return 'change-removed'; + if (type === 'modified') return 'change-modified'; + return 'change-modified'; + } + + formatChangeValue(value?: string | null): string { + if (value === undefined || value === null || value === '') return '-'; + return String(value); + } + + trackByLog(_: number, log: AuditLogDto): string { + return log.id; + } + + trackByField(_: number, change: { field: string }): string { + return change.field; + } + + private fetch(goToPage?: number): void { + if (goToPage) this.page = goToPage; + this.loading = true; + this.error = false; + this.errorMsg = ''; + this.expandedLogId = null; + + const query: HistoricoQuery = { + page: this.page, + pageSize: this.pageSize, + pageName: this.filterPageName || undefined, + action: this.filterAction || undefined, + userId: this.filterUserId?.trim() || undefined, + search: this.filterSearch?.trim() || undefined, + dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, + dateTo: this.toIsoDate(this.dateTo, true) || undefined, + }; + + this.historicoService.list(query).subscribe({ + next: (res) => { + this.logs = res.items || []; + this.total = res.total || 0; + this.page = res.page || this.page; + this.pageSize = res.pageSize || this.pageSize; + this.loading = false; + }, + error: (err: HttpErrorResponse) => { + this.error = true; + if (err?.status === 403) { + this.errorMsg = 'Acesso restrito.'; + } else { + this.errorMsg = 'Erro ao carregar histórico. Tente novamente.'; + } + this.loading = false; + }, + }); + } + + private toIsoDate(value: string, endOfDay: boolean): string | null { + if (!value) return null; + const time = endOfDay ? '23:59:59' : '00:00:00'; + const date = new Date(`${value}T${time}`); + if (isNaN(date.getTime())) return null; + return date.toISOString(); + } + + private async showToast(message: string) { + if (!isPlatformBrowser(this.platformId)) return; + this.toastMessage = message; + this.cdr.detectChanges(); + if (!this.successToast?.nativeElement) return; + + try { + const bs = await import('bootstrap'); + const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, { + autohide: true, + delay: 3000 + }); + toastInstance.show(); + } catch (error) { + console.error(error); + } + } +} diff --git a/src/app/pages/login/login.html b/src/app/pages/login/login.html index 5e9715b..e66cde4 100644 --- a/src/app/pages/login/login.html +++ b/src/app/pages/login/login.html @@ -50,7 +50,6 @@ Lembrar de mim -
Esqueceu a senha? + +
+
+ + + Mostrando {{ filteredNotifications.length }} notificações + • {{ selectedIds.size }} selecionada(s) + +
+
+ + +
+
@@ -46,11 +79,6 @@
- -
- Mostrando {{ filteredNotifications.length }} notificações -
-
+ +
@@ -68,25 +101,34 @@

{{ n.linha || 'Linha Desconhecida' }} + + {{ n.cliente || '-' }} +

+
+ Efetivação: {{ formatDateLabel(n.dtEfetivacaoServico) }} + Término: {{ formatDateLabel(n.dtTerminoFidelizacao) }} +
+
+ +
+
+ Conta + {{ n.conta || '-' }} +
+
+ Usuário + {{ n.usuario || '-' }} +
+
+ Plano + {{ n.planoContrato || '-' }} +
+
{{ n.tipo === 'Vencido' ? 'Vencido' : 'A vencer' }} - - - {{ n.referenciaData ? (n.referenciaData | date:'dd/MM/yyyy') : '-' }} - +
- -

- Cliente: {{ n.cliente || '-' }} • Usuário: {{ n.usuario || '-' }} -

- -

- A vigência desta linha expirou. Verifique a renovação imediatamente. -

-

- A vigência irá expirar em breve. Programe-se. -

@@ -107,4 +149,4 @@
- \ No newline at end of file + diff --git a/src/app/pages/notificacoes/notificacoes.scss b/src/app/pages/notificacoes/notificacoes.scss index 1bd47b3..8aa8fcf 100644 --- a/src/app/pages/notificacoes/notificacoes.scss +++ b/src/app/pages/notificacoes/notificacoes.scss @@ -30,6 +30,74 @@ $border: #e5e7eb; p { color: $text-secondary; font-size: 16px; margin-bottom: 24px; } } +.bulk-left { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.bulk-actions-bar { + margin-top: 16px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.bulk-count { + font-size: 12px; + font-weight: 700; + color: $text-secondary; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.bulk-selected { + color: $text-main; +} + +.select-all { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 700; + color: $text-secondary; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + + input { + width: 20px; + height: 20px; + accent-color: $primary; + } +} + +.bulk-actions { display: flex; gap: 8px; flex-wrap: wrap; } + +.bulk-btn { + background: $white; + border: 1px solid $border; + padding: 8px 12px; + border-radius: 10px; + font-size: 12px; + font-weight: 700; + color: $text-main; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s; + cursor: pointer; + + &:hover { border-color: $primary; color: $primary; } + &:disabled { opacity: 0.6; cursor: default; } + + &.ghost { background: transparent; } +} + /* FILTROS (Estilo Tabs/Pills) */ .filters-bar { display: inline-flex; @@ -93,10 +161,7 @@ $border: #e5e7eb; display: flex; flex-direction: column; gap: 12px; } -.list-header-actions { - font-size: 12px; font-weight: 600; color: $text-secondary; text-transform: uppercase; letter-spacing: 0.5px; - margin-bottom: 8px; padding-left: 8px; -} +/* list-header-actions removido */ .list-item { background: $white; @@ -125,6 +190,24 @@ $border: #e5e7eb; } } +.item-select { + margin-left: 8px; + margin-right: 8px; + min-width: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + cursor: pointer; + align-self: center; + + input { + width: 20px; + height: 20px; + accent-color: $primary; + } +} + .status-strip { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; } @@ -141,33 +224,66 @@ $border: #e5e7eb; .item-content { flex: 1; min-width: 0; } .content-top { - display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; - margin-bottom: 6px; + display: grid; + grid-template-columns: 1fr auto; + align-items: start; + gap: 10px; + margin-bottom: 10px; } .item-title { - font-size: 16px; font-weight: 700; color: $text-main; margin: 0; - display: flex; align-items: center; gap: 8px; + font-size: 16px; + font-weight: 800; + color: $text-main; + margin: 0; + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; } +.separator { color: $text-secondary; } +.item-client { font-weight: 600; color: $text-secondary; } + +.date-stack { + display: flex; + flex-direction: column; + gap: 6px; + align-items: flex-end; + min-width: 170px; + text-align: right; +} + +.date-pill { + font-size: 11px; + font-weight: 700; + padding: 4px 8px; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.4px; + &.green { background: rgba($success, 0.12); color: $success; } + &.red { background: rgba($danger, 0.12); color: $danger; } +} + +.item-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 16px; +} + +.meta-row { display: flex; flex-direction: column; gap: 2px; } +.meta-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: $text-secondary; font-weight: 700; } +.meta-value { font-size: 13px; font-weight: 600; color: $text-main; } + .badge-tag { - font-size: 10px; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; + font-size: 10px; text-transform: uppercase; padding: 4px 8px; border-radius: 999px; font-weight: 800; letter-spacing: 0.5px; + width: fit-content; &.danger { background: rgba($danger, 0.1); color: $danger; } &.warn { background: rgba($warning, 0.1); color: color.adjust($warning, $lightness: -10%); } } -.item-time { font-size: 12px; color: $text-secondary; font-weight: 500; } - -.item-details { - font-size: 13px; color: $text-secondary; margin: 0 0 4px; -} - -.item-message { - font-size: 13px; color: $text-secondary; margin: 0; opacity: 0.8; -} - .item-actions { margin-left: 12px; align-self: center; } diff --git a/src/app/pages/notificacoes/notificacoes.ts b/src/app/pages/notificacoes/notificacoes.ts index 9a5bce1..930bf7d 100644 --- a/src/app/pages/notificacoes/notificacoes.ts +++ b/src/app/pages/notificacoes/notificacoes.ts @@ -15,6 +15,9 @@ export class Notificacoes implements OnInit { filter: 'todas' | 'vencidas' | 'aVencer' | 'lidas' = 'todas'; loading = false; error = false; + bulkLoading = false; + exportLoading = false; + selectedIds = new Set(); constructor(private notificationsService: NotificationsService) {} @@ -34,6 +37,7 @@ export class Notificacoes implements OnInit { setFilter(value: 'todas' | 'vencidas' | 'aVencer' | 'lidas') { this.filter = value; + this.clearSelection(); } get filteredNotifications() { @@ -49,6 +53,11 @@ export class Notificacoes implements OnInit { return this.notifications; } + formatDateLabel(date?: string | null): string { + if (!date) return '-'; + return new Date(date).toLocaleDateString('pt-BR'); + } + private loadNotifications() { this.loading = true; this.error = false; @@ -65,6 +74,119 @@ export class Notificacoes implements OnInit { } countByType(tipo: 'Vencido' | 'AVencer'): number { - return this.notifications.filter(n => n.tipo === tipo && !n.lida).length; -} + return this.notifications.filter(n => n.tipo === tipo && !n.lida).length; + } + + markAllAsRead() { + if (this.filter === 'lidas' || this.bulkLoading) return; + this.bulkLoading = true; + + const filterParam = this.getFilterParam(); + const ids = Array.from(this.selectedIds); + this.notificationsService.markAllAsRead(filterParam, ids.length ? ids : undefined).subscribe({ + next: () => { + const now = new Date().toISOString(); + this.notifications = this.notifications.map((n) => { + if (ids.length ? ids.includes(n.id) : this.shouldMarkRead(n)) { + return { ...n, lida: true, lidaEm: now }; + } + return n; + }); + this.clearSelection(); + this.bulkLoading = false; + }, + error: () => { + this.bulkLoading = false; + } + }); + } + + exportNotifications() { + if (this.filter === 'lidas' || this.exportLoading) return; + this.exportLoading = true; + + const filterParam = this.getFilterParam(); + const ids = Array.from(this.selectedIds); + this.notificationsService.export(filterParam, ids.length ? ids : undefined).subscribe({ + next: (res) => { + const blob = res.body; + if (!blob) { + this.exportLoading = false; + return; + } + + const filename = this.extractFilename(res.headers.get('content-disposition')) || this.buildDefaultFilename(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + this.clearSelection(); + this.exportLoading = false; + }, + error: () => { + this.exportLoading = false; + } + }); + } + + isSelected(notification: NotificationDto): boolean { + return this.selectedIds.has(notification.id); + } + + toggleSelection(notification: NotificationDto) { + if (this.selectedIds.has(notification.id)) { + this.selectedIds.delete(notification.id); + } else { + this.selectedIds.add(notification.id); + } + } + + get isAllSelected(): boolean { + const list = this.filteredNotifications; + return list.length > 0 && list.every(n => this.selectedIds.has(n.id)); + } + + toggleSelectAll() { + const list = this.filteredNotifications; + if (this.isAllSelected) { + this.clearSelection(); + return; + } + list.forEach(n => this.selectedIds.add(n.id)); + } + + clearSelection() { + this.selectedIds.clear(); + } + + private getFilterParam(): string | undefined { + if (this.filter === 'aVencer') return 'a-vencer'; + if (this.filter === 'vencidas') return 'vencidas'; + if (this.filter === 'todas') return undefined; + return undefined; + } + + private shouldMarkRead(n: NotificationDto): boolean { + if (this.filter === 'todas') return true; + if (this.filter === 'aVencer') return n.tipo === 'AVencer'; + if (this.filter === 'vencidas') return n.tipo === 'Vencido'; + return false; + } + + private extractFilename(contentDisposition: string | null): string | null { + if (!contentDisposition) return null; + const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i); + if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]); + const normalMatch = contentDisposition.match(/filename=\"?([^\";]+)\"?/i); + return normalMatch?.[1] ?? null; + } + + private buildDefaultFilename(): string { + const stamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '').slice(0, 14); + return `notificacoes-${stamp}.xlsx`; + } } diff --git a/src/app/pages/novo-usuario/novo-usuario.html b/src/app/pages/novo-usuario/novo-usuario.html index 52c9939..6436379 100644 --- a/src/app/pages/novo-usuario/novo-usuario.html +++ b/src/app/pages/novo-usuario/novo-usuario.html @@ -63,7 +63,7 @@

Gerencie permissões e status.

- +
diff --git a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.html b/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.html new file mode 100644 index 0000000..fd6c79b --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.html @@ -0,0 +1,151 @@ +
+
+
+ + + + + +
+
diff --git a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.scss b/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.scss new file mode 100644 index 0000000..f66ede4 --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.scss @@ -0,0 +1,389 @@ +:host { + display: block; + --brand: var(--pg-primary, #1f4fd6); + --blue: var(--pg-primary-strong, #153caa); + --focus-ring: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); +} + +.lg-backdrop { + position: fixed; + inset: 0; + background: + radial-gradient(circle at 15% 0%, rgba(31, 79, 214, 0.15), rgba(15, 23, 42, 0.66) 42%), + rgba(15, 23, 42, 0.58); + z-index: 9990; + backdrop-filter: blur(4px); +} + +.lg-modal { + position: fixed; + inset: 0; + z-index: 9995; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +.lg-modal-card { + width: min(1040px, 96vw); + max-height: 92vh; + overflow: hidden; + display: flex; + flex-direction: column; + border-radius: 18px; + border: 1px solid var(--pg-border, #dbe3ef); + background: #fff; + box-shadow: var(--pg-shadow-lg, 0 24px 56px rgba(15, 23, 42, 0.25)); + animation: pop-up 0.24s ease; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 14px 18px; + border-bottom: 1px solid var(--pg-border, #dbe3ef); + background: linear-gradient(180deg, #f4f8ff, #ffffff 85%); +} + +.modal-title { + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 0.98rem; + font-weight: 800; + color: var(--pg-text, #0f172a); + + .icon-bg { + width: 34px; + height: 34px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(31, 79, 214, 0.12); + color: var(--blue); + } +} + +.btn-icon { + width: 34px; + height: 34px; + border-radius: 10px; + border: 1px solid var(--pg-border-strong, #c8d4e4); + background: #fff; + color: var(--pg-text-soft, #64748b); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + border-color: var(--brand); + color: var(--blue); + transform: translateY(-1px); + } + + &:focus-visible { + outline: none; + box-shadow: var(--focus-ring); + } +} + +.modal-body { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 16px 18px; + background: linear-gradient(180deg, #f8fbff, #ffffff 82%); + display: grid; + gap: 16px; +} + +.form-section { + border: 1px solid var(--pg-border, #dbe3ef); + border-radius: 14px; + padding: 12px; + background: #fff; + display: grid; + gap: 12px; +} + +.section-head { + display: grid; + gap: 3px; + + h4 { + margin: 0; + font-size: 0.9rem; + color: var(--pg-text, #0f172a); + font-weight: 800; + } + + small { + color: var(--pg-text-soft, #64748b); + font-size: 12px; + font-weight: 600; + } +} + +.form-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.form-field { + display: grid; + gap: 6px; + + label { + color: var(--pg-text-soft, #64748b); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; + } + + input { + height: 40px; + border-radius: 10px; + border: 1px solid var(--pg-border-strong, #c8d4e4); + background: #fff; + color: var(--pg-text, #0f172a); + padding: 0 12px; + font-size: 14px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + + &:focus { + outline: none; + border-color: var(--brand); + box-shadow: var(--focus-ring); + } + } + + .error { + color: var(--pg-danger, #c52929); + font-size: 11px; + font-weight: 700; + } +} + +.competencia-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + align-items: center; +} + +.select-glass { + background: #fff; + border: 1px solid var(--pg-border-strong, #c8d4e4); + border-radius: 10px; + color: var(--pg-text, #0f172a); + font-weight: 700; +} + +.final-box { + min-height: 40px; + border-radius: 10px; + border: 1px dashed var(--pg-border-strong, #c8d4e4); + background: #f8fbff; + color: var(--pg-text-muted, #475569); + padding: 0 12px; + display: flex; + align-items: center; + font-weight: 700; +} + +.preview-card { + border: 1px solid var(--pg-border, #dbe3ef); + border-radius: 14px; + padding: 12px; + display: grid; + gap: 10px; + background: #fff; +} + +.preview-head h4 { + margin: 0; + font-size: 0.9rem; + font-weight: 800; + color: var(--pg-text, #0f172a); +} + +.preview-head small { + color: var(--pg-text-soft, #64748b); + font-size: 12px; + font-weight: 600; +} + +.preview-table { + border: 1px solid var(--pg-border, #dbe3ef); + border-radius: 10px; + max-height: 240px; + overflow: auto; +} + +.preview-table table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + min-width: 460px; +} + +.preview-table th, +.preview-table td { + padding: 8px 10px; + border-bottom: 1px solid #e9eef6; + font-size: 12px; + color: var(--pg-text, #0f172a); +} + +.preview-table th { + position: sticky; + top: 0; + z-index: 1; + background: #f5f9ff; + color: var(--pg-text-soft, #64748b); + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 10px; + font-weight: 800; +} + +.preview-table .empty { + text-align: center; + color: var(--pg-text-muted, #475569); + font-weight: 700; +} + +.inline-input { + width: 100%; + height: 32px; + border-radius: 8px; + border: 1px solid var(--pg-border-strong, #c8d4e4); + background: #fff; + color: var(--pg-text, #0f172a); + text-align: right; + padding: 0 8px; + font-size: 12px; + + &:focus { + outline: none; + border-color: var(--brand); + box-shadow: var(--focus-ring); + } +} + +.todo-note { + margin: 0; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid rgba(197, 41, 41, 0.28); + background: rgba(197, 41, 41, 0.1); + color: var(--pg-danger, #c52929); + font-size: 12px; + font-weight: 700; +} + +.modal-footer { + border-top: 1px solid var(--pg-border, #dbe3ef); + padding: 12px 18px; + display: flex; + justify-content: flex-end; + gap: 10px; + background: #fff; +} + +.btn-primary, +.btn-ghost { + height: 38px; + border-radius: 10px; + border: 1px solid transparent; + padding: 0 14px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; + + &:focus-visible { + outline: none; + box-shadow: var(--focus-ring); + } + + &:disabled { + opacity: 0.62; + cursor: not-allowed; + } +} + +.btn-primary { + color: #fff; + border-color: var(--blue); + background: linear-gradient(140deg, var(--brand), var(--blue)); + box-shadow: 0 10px 22px rgba(31, 79, 214, 0.28); +} + +.btn-ghost { + color: var(--pg-text, #0f172a); + background: #fff; + border-color: var(--pg-border-strong, #c8d4e4); +} + +.btn-primary:hover, +.btn-ghost:hover { + transform: translateY(-1px); +} + +.btn-ghost:hover { + border-color: var(--brand); + color: var(--blue); +} + +@keyframes pop-up { + from { + opacity: 0; + transform: translateY(10px) scale(0.98); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@media (max-width: 940px) { + .form-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 760px) { + .modal-header, + .modal-body, + .modal-footer { + padding-left: 12px; + padding-right: 12px; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .modal-footer { + justify-content: stretch; + } + + .modal-footer .btn-primary, + .modal-footer .btn-ghost { + flex: 1; + } +} diff --git a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts b/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts new file mode 100644 index 0000000..ab003b4 --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts @@ -0,0 +1,253 @@ +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../../../components/custom-select/custom-select'; + +export type MonthOption = { value: number; label: string }; + +export type ParcelamentoCreateModel = { + anoRef: number | null; + linha: string; + cliente: string; + item: number | null; + qtParcelas: string; + parcelaAtual: number | null; + totalParcelas: number | null; + valorCheio: string; + desconto: string; + valorComDesconto: string; + competenciaAno: number | null; + competenciaMes: number | null; + monthValues: Array<{ competencia: string; valor: string }>; +}; + +type PreviewRow = { + competencia: string; + label: string; + parcela: number; + valor: string; +}; + +@Component({ + selector: 'app-parcelamento-create-modal', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './parcelamento-create-modal.html', + styleUrls: ['./parcelamento-create-modal.scss'], +}) +export class ParcelamentoCreateModalComponent implements OnChanges { + @Input() open = false; + @Input() monthOptions: MonthOption[] = []; + @Input() model!: ParcelamentoCreateModel; + @Input() title = 'Novo Parcelamento'; + @Input() submitLabel = 'Salvar'; + @Input() loading = false; + @Input() errorMessage = ''; + + @Output() close = new EventEmitter(); + @Output() save = new EventEmitter(); + + touched = false; + previewRows: PreviewRow[] = []; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['model'] && this.model) { + this.syncMonthValues(); + return; + } + if (changes['open'] && this.model) { + this.rebuildPreviewRows(); + } + } + + onValueChange(): void { + const cheio = this.toNumber(this.model.valorCheio); + const desconto = this.toNumber(this.model.desconto); + if (cheio === null) { + this.model.valorComDesconto = ''; + this.syncMonthValues(); + return; + } + const calc = Math.max(0, cheio - (desconto ?? 0)); + this.model.valorComDesconto = this.formatInput(calc); + this.syncMonthValues(); + } + + onCompetenciaChange(): void { + this.syncMonthValues(); + } + + onValorComDescontoChange(): void { + this.syncMonthValues(); + } + + onParcelaChange(): void { + this.syncQtParcelas(); + this.syncMonthValues(); + } + + onQtParcelasChange(): void { + const parsed = this.parseQtParcelas(this.model.qtParcelas); + if (parsed) { + this.model.parcelaAtual = parsed.atual; + this.model.totalParcelas = parsed.total; + } + this.syncMonthValues(); + } + + get competenciaFinalLabel(): string { + if (this.model.monthValues?.length) { + const last = this.model.monthValues[this.model.monthValues.length - 1]; + return this.formatCompetenciaLabel(last.competencia); + } + const total = this.model.totalParcelas ?? 0; + const ano = this.model.competenciaAno ?? 0; + const mes = this.model.competenciaMes ?? 0; + if (!total || !ano || !mes) return '-'; + + const index = (mes - 1) + (total - 1); + const finalAno = ano + Math.floor(index / 12); + const finalMes = (index % 12) + 1; + return `${String(finalMes).padStart(2, '0')}/${finalAno}`; + } + + onPreviewValueChange(competencia: string, value: string): void { + const list = this.model.monthValues ?? []; + const item = list.find((entry) => entry.competencia === competencia); + if (item) item.valor = value ?? ''; + + const row = this.previewRows.find((entry) => entry.competencia === competencia); + if (row) row.valor = value ?? ''; + } + + trackByPreview(_: number, row: PreviewRow): string { + return row.competencia; + } + + get isValid(): boolean { + return !!( + this.model.anoRef && + this.model.item && + this.model.linha?.trim() && + this.model.cliente?.trim() && + this.model.totalParcelas && + this.model.totalParcelas > 0 && + this.model.valorCheio && + this.model.competenciaAno && + this.model.competenciaMes + ); + } + + onSave(): void { + this.touched = true; + if (!this.isValid) return; + this.save.emit(this.model); + } + + private syncQtParcelas(): void { + const atual = this.model.parcelaAtual; + const total = this.model.totalParcelas; + if (atual && total) { + this.model.qtParcelas = `${atual}/${total}`; + } + } + + private syncMonthValues(): void { + const total = this.model.totalParcelas ?? 0; + const ano = this.model.competenciaAno ?? 0; + const mes = this.model.competenciaMes ?? 0; + if (!total || !ano || !mes) { + this.model.monthValues = []; + this.previewRows = []; + return; + } + + const existing = new Map(); + (this.model.monthValues ?? []).forEach((m) => { + if (m?.competencia) existing.set(m.competencia, m.valor ?? ''); + }); + + const valorTotal = this.toNumber(this.model.valorComDesconto) ?? this.toNumber(this.model.valorCheio); + const valorParcela = valorTotal !== null ? valorTotal / total : null; + const defaultValor = valorParcela !== null ? this.formatInput(valorParcela) : ''; + + const list: Array<{ competencia: string; valor: string }> = []; + for (let i = 0; i < total; i++) { + const index = (mes - 1) + i; + const y = ano + Math.floor(index / 12); + const m = (index % 12) + 1; + const competencia = `${y}-${String(m).padStart(2, '0')}-01`; + list.push({ + competencia, + valor: existing.get(competencia) ?? defaultValor, + }); + } + + this.model.monthValues = list; + this.rebuildPreviewRows(); + } + + private rebuildPreviewRows(): void { + const list = this.model?.monthValues ?? []; + if (!list.length) { + this.previewRows = []; + return; + } + + this.previewRows = list.slice(0, 36).map((item, idx) => ({ + competencia: item.competencia, + label: this.formatCompetenciaLabel(item.competencia), + parcela: idx + 1, + valor: item.valor ?? '', + })); + } + + private formatCompetenciaLabel(value: string): string { + const match = value.match(/^(\d{4})-(\d{2})/); + if (!match) return value || '-'; + return `${match[2]}/${match[1]}`; + } + + private parseQtParcelas(raw: string | null | undefined): { atual: number; total: number } | null { + if (!raw) return null; + const parts = raw.split('/'); + if (parts.length < 2) return null; + const atualStr = this.onlyDigits(parts[0]); + const totalStr = this.onlyDigits(parts[1]); + if (!atualStr || !totalStr) return null; + return { atual: Number(atualStr), total: Number(totalStr) }; + } + + private toNumber(value: any): number | null { + if (value === null || value === undefined || value === '') return null; + if (typeof value === 'number') return Number.isFinite(value) ? value : null; + const raw = String(value).trim(); + if (!raw) return null; + let cleaned = raw.replace(/[^\d,.-]/g, ''); + if (cleaned.includes(',') && cleaned.includes('.')) { + if (cleaned.lastIndexOf(',') > cleaned.lastIndexOf('.')) { + cleaned = cleaned.replace(/\./g, '').replace(',', '.'); + } else { + cleaned = cleaned.replace(/,/g, ''); + } + } else if (cleaned.includes(',')) { + cleaned = cleaned.replace(/\./g, '').replace(',', '.'); + } else { + cleaned = cleaned.replace(/,/g, ''); + } + const n = Number(cleaned); + return Number.isNaN(n) ? null : n; + } + + private onlyDigits(value: string): string { + let out = ''; + for (const ch of value ?? '') { + if (ch >= '0' && ch <= '9') out += ch; + } + return out; + } + + private formatInput(value: number): string { + return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value); + } +} diff --git a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html b/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html new file mode 100644 index 0000000..1f152f3 --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html @@ -0,0 +1,56 @@ +
+
+
+ + + + + +
+
diff --git a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss b/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss new file mode 100644 index 0000000..8b610ab --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss @@ -0,0 +1,202 @@ +:host { + --brand: #E33DCF; + --blue: #030FAA; + --focus-ring: 0 0 0 3px rgba(227, 61, 207, 0.16); +} + +.lg-backdrop { + position: fixed; + inset: 0; + background: radial-gradient(circle at 20% 0%, rgba(227, 61, 207, 0.2), rgba(0, 0, 0, 0.56) 42%); + z-index: 9990; + backdrop-filter: blur(5px); +} + +.lg-modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 9995; + padding: 16px; +} + +.lg-modal-card { + background: #ffffff; + border: 1px solid rgba(255, 255, 255, 0.88); + border-radius: 20px; + box-shadow: 0 30px 62px -16px rgba(0, 0, 0, 0.42); + width: min(1200px, 98vw); + overflow: hidden; + animation: popUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 16px 20px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.1), rgba(255, 255, 255, 0.95) 72%); +} + +.modal-title { + display: flex; + align-items: center; + gap: 10px; + font-weight: 900; + + .icon-bg { + width: 32px; + height: 32px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(3, 15, 170, 0.1); + color: var(--blue); + } +} + +.modal-actions { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + select { + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 10px; + padding: 6px 10px; + font-weight: 800; + background: rgba(255, 255, 255, 0.92); + transition: border-color 0.2s ease, box-shadow 0.2s ease; + + &:focus { + outline: none; + border-color: var(--brand); + box-shadow: var(--focus-ring); + } + } +} + +.btn-icon { + width: 34px; + height: 34px; + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 10px; + background: rgba(255, 255, 255, 0.86); + color: rgba(17, 18, 20, 0.58); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + border-color: rgba(227, 61, 207, 0.26); + background: #fff; + color: var(--brand); + transform: translateY(-1px); + } + + &:focus-visible { + outline: none; + box-shadow: var(--focus-ring); + } +} + +.modal-body { + padding: 16px; + background: linear-gradient(180deg, rgba(248, 249, 251, 0.98), rgba(255, 255, 255, 0.98)); +} + +.annual-table { + overflow-x: auto; + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 12px; + background: #fff; +} + +.annual-table table { + border-collapse: collapse; + min-width: 1100px; + width: 100%; + font-size: 12px; +} + +.annual-table th, +.annual-table td { + padding: 10px 12px; + border-bottom: 1px solid rgba(17, 18, 20, 0.06); + white-space: nowrap; +} + +.annual-table thead th { + background: #f8f9fb; + text-transform: uppercase; + letter-spacing: 0.04em; + font-size: 10px; + color: rgba(17, 18, 20, 0.6); +} + +.sticky-col { + position: sticky; + left: 0; + background: #fff; + z-index: 2; + box-shadow: 2px 0 0 rgba(17, 18, 20, 0.04); +} + +.col-1 { left: 0; min-width: 180px; } +.col-2 { left: 180px; min-width: 140px; } +.col-3 { left: 320px; min-width: 120px; } +.col-4 { left: 440px; min-width: 120px; text-align: right; } +.col-5 { left: 560px; min-width: 80px; } + +.modal-footer { + padding: 14px 20px; + border-top: 1px solid rgba(0, 0, 0, 0.06); + display: flex; + justify-content: flex-end; + background: linear-gradient(180deg, #ffffff, rgba(248, 249, 251, 0.96)); +} + +.btn-primary { + height: 38px; + border-radius: 10px; + border: 1px solid #030faa; + font-weight: 700; + font-size: 12px; + cursor: pointer; + padding: 0 14px; + background: linear-gradient(135deg, #1543ff, #030faa); + color: #fff; + box-shadow: 0 10px 20px rgba(3, 15, 170, 0.24); + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 12px 24px rgba(3, 15, 170, 0.28); + filter: brightness(1.04); + } + + &:focus-visible { + outline: none; + box-shadow: var(--focus-ring); + } +} + +.empty-state { + text-align: center; + padding: 24px; + font-weight: 700; + color: rgba(17, 18, 20, 0.6); +} + +@keyframes popUp { + from { opacity: 0; transform: scale(0.95) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} diff --git a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts b/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts new file mode 100644 index 0000000..99a923f --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts @@ -0,0 +1,41 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +export type AnnualMonthValue = { + month: number; + label: string; + value: number | null; +}; + +export type AnnualRow = { + cliente: string; + linha: string; + item: string; + total: number; + parcelasLabel: string; + months: AnnualMonthValue[]; +}; + +@Component({ + selector: 'app-parcelamento-detalhamento-anual-modal', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './parcelamento-detalhamento-anual-modal.html', + styleUrls: ['./parcelamento-detalhamento-anual-modal.scss'], +}) +export class ParcelamentoDetalhamentoAnualModalComponent { + @Input() open = false; + @Input() years: number[] = []; + @Input() selectedYear: number | null = null; + @Input() data: AnnualRow | null = null; + + @Output() close = new EventEmitter(); + @Output() yearChange = new EventEmitter(); + + onYearChange(value: unknown): void { + const year = Number(value); + if (!Number.isFinite(year)) return; + this.yearChange.emit(year); + } +} diff --git a/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.html b/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.html new file mode 100644 index 0000000..98e4cfd --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.html @@ -0,0 +1,74 @@ +
+
+
+
+ + Filtros da listagem +
+ Use os campos abaixo para refinar a consulta sem alterar os dados. +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ Informe ano e mes. +
+
+ +
+ + +
+ + {{ chip.label }}: {{ chip.value }} + +
+
+
diff --git a/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.scss b/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.scss new file mode 100644 index 0000000..5b97ec9 --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.scss @@ -0,0 +1,300 @@ +:host { + display: block; + min-width: 0; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +.filters-card { + border: 1px solid var(--pg-border, #dbe3ef); + border-radius: var(--pg-radius-md, 14px); + padding: 16px; + display: grid; + gap: 14px; + background: rgba(255, 255, 255, 0.95); + box-shadow: var(--pg-shadow-sm, 0 8px 18px rgba(15, 23, 42, 0.08)); + overflow: hidden; +} + +.filters-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.filters-head > * { + min-width: 0; +} + +.filters-title-wrap { + display: grid; + gap: 6px; + flex: 1 1 360px; + min-width: 0; + + small { + color: var(--pg-text-soft, #64748b); + font-size: 12px; + font-weight: 600; + } +} + +.filters-title { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--pg-text, #0f172a); + font-weight: 800; + + i { + color: var(--pg-primary, #1f4fd6); + } +} + +.filters-actions { + display: inline-flex; + gap: 8px; + flex-wrap: wrap; + min-width: 0; +} + +.btn-primary, +.btn-ghost { + height: 38px; + border-radius: var(--pg-radius-sm, 10px); + border: 1px solid transparent; + padding: 0 12px; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; + + &:focus-visible { + outline: none; + box-shadow: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); + } + + &:disabled { + opacity: 0.62; + cursor: not-allowed; + transform: none; + box-shadow: none; + } +} + +.btn-primary { + color: #fff; + border-color: var(--pg-primary-strong, #153caa); + background: linear-gradient(140deg, var(--pg-primary, #1f4fd6), var(--pg-primary-strong, #153caa)); + box-shadow: 0 10px 20px rgba(31, 79, 214, 0.25); +} + +.btn-ghost { + color: var(--pg-text, #0f172a); + background: #fff; + border-color: var(--pg-border-strong, #c8d4e4); +} + +.btn-primary:hover, +.btn-ghost:hover { + transform: translateY(-1px); +} + +.btn-ghost:hover { + border-color: var(--pg-primary, #1f4fd6); + color: var(--pg-primary-strong, #153caa); +} + +.filters-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + min-width: 0; +} + +.filter-field { + display: grid; + gap: 6px; + min-width: 0; + + label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; + color: var(--pg-text-soft, #64748b); + } + + input { + width: 100%; + height: 40px; + border-radius: var(--pg-radius-sm, 10px); + border: 1px solid var(--pg-border-strong, #c8d4e4); + background: #fff; + color: var(--pg-text, #0f172a); + padding: 0 12px; + font-size: 14px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + + &:focus { + outline: none; + border-color: var(--pg-primary, #1f4fd6); + box-shadow: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); + } + + &:disabled { + background: #f5f8fd; + color: var(--pg-text-soft, #64748b); + cursor: not-allowed; + } + } +} + +.competencia-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 8px; + align-items: center; + min-width: 0; +} + +.competencia-row > * { + min-width: 0; + width: 100%; +} + +.filters-meta { + border-top: 1px dashed var(--pg-border, #dbe3ef); + padding-top: 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + min-width: 0; +} + +.search-box { + display: inline-flex; + align-items: center; + gap: 8px; + width: min(420px, 100%); + min-width: 0; + border: 1px solid var(--pg-border-strong, #c8d4e4); + border-radius: var(--pg-radius-sm, 10px); + background: #fff; + padding: 0 12px; + height: 40px; + + i { + color: var(--pg-text-soft, #64748b); + } + + input { + width: 100%; + border: none; + outline: none; + font-size: 13px; + color: var(--pg-text, #0f172a); + background: transparent; + } + + &:focus-within { + border-color: var(--pg-primary, #1f4fd6); + box-shadow: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); + } +} + +.filter-chips { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + min-width: 0; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 4px; + border-radius: 999px; + border: 1px solid rgba(31, 79, 214, 0.2); + background: rgba(31, 79, 214, 0.1); + padding: 4px 10px; + color: var(--pg-text-muted, #475569); + font-size: 11px; + font-weight: 700; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + strong { + color: var(--pg-primary-strong, #153caa); + } +} + +.hint { + font-size: 12px; + color: var(--pg-text-muted, #475569); +} + +.hint.warn { + color: var(--pg-warning, #b4690e); + font-weight: 700; +} + +.select-glass { + display: block; + width: 100%; + min-width: 0; + background: #fff; + border: 1px solid var(--pg-border-strong, #c8d4e4); + border-radius: var(--pg-radius-sm, 10px); + color: var(--pg-text, #0f172a); + font-weight: 700; +} + +@media (max-width: 1100px) { + .filters-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 760px) { + .filters-actions { + width: 100%; + } + + .filters-actions .btn-primary, + .filters-actions .btn-ghost { + flex: 1; + } + + .filters-grid { + grid-template-columns: 1fr; + } + + .filters-meta { + align-items: stretch; + } + + .search-box { + width: 100%; + min-width: 0; + } + + .filter-chips { + justify-content: flex-start; + } +} diff --git a/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.ts b/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.ts new file mode 100644 index 0000000..546a7bd --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.ts @@ -0,0 +1,36 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../../../components/custom-select/custom-select'; + +export type MonthOption = { value: number; label: string }; + +export type ParcelamentosFiltersModel = { + anoRef: string; + linha: string; + cliente: string; + competenciaAno: string; + competenciaMes: number | ''; + search: string; +}; + +export type FilterChip = { label: string; value: string }; + +@Component({ + selector: 'app-parcelamentos-filters', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './parcelamentos-filters.html', + styleUrls: ['./parcelamentos-filters.scss'], +}) +export class ParcelamentosFiltersComponent { + @Input() filters!: ParcelamentosFiltersModel; + @Input() monthOptions: MonthOption[] = []; + @Input() loading = false; + @Input() competenciaInvalid = false; + @Input() activeChips: FilterChip[] = []; + + @Output() apply = new EventEmitter(); + @Output() clear = new EventEmitter(); + @Output() searchChange = new EventEmitter(); +} diff --git a/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.html b/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.html new file mode 100644 index 0000000..dbe1c2b --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.html @@ -0,0 +1,14 @@ +
+
+ {{ k?.label }} + + {{ k?.value }} + + {{ k?.hint }} +
+
diff --git a/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.scss b/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.scss new file mode 100644 index 0000000..26dad0c --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.scss @@ -0,0 +1,57 @@ +:host { + display: block; + min-width: 0; +} + +.parcelamentos-kpis { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(196px, 1fr)); + gap: 12px; +} + +.kpi-card { + border: 1px solid var(--pg-border, #dbe3ef); + border-radius: var(--pg-radius-md, 14px); + background: linear-gradient(180deg, #ffffff, #f8fbff); + padding: 14px 15px; + display: grid; + gap: 6px; + box-shadow: var(--pg-shadow-sm, 0 8px 18px rgba(15, 23, 42, 0.08)); +} + +.kpi-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; + color: var(--pg-text-soft, #64748b); +} + +.kpi-value { + font-size: 1.2rem; + line-height: 1.2; + font-weight: 800; + color: var(--pg-text, #0f172a); +} + +.kpi-hint { + font-size: 12px; + font-weight: 600; + color: var(--pg-text-muted, #475569); +} + +.tone-brand { + color: var(--pg-primary, #1f4fd6); +} + +.tone-success { + color: #1c7a3e; +} + +.tone-danger { + color: #b42323; +} + +.tone-info { + color: #1f4fd6; +} diff --git a/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.ts b/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.ts new file mode 100644 index 0000000..6a90665 --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +export type ParcelamentoKpi = { + label: string; + value: string; + hint?: string; + tone?: 'brand' | 'success' | 'danger' | 'info' | 'muted'; +}; + +@Component({ + selector: 'app-parcelamentos-kpis', + standalone: true, + imports: [CommonModule], + templateUrl: './parcelamentos-kpis.html', + styleUrls: ['./parcelamentos-kpis.scss'], +}) +export class ParcelamentosKpisComponent { + @Input() cards: ParcelamentoKpi[] = []; +} diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html new file mode 100644 index 0000000..db3af8f --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html @@ -0,0 +1,161 @@ +
+
+
+

Carteira de Parcelamentos

+ Visualizacao paginada com filtros e acoes por registro +
+ +
+ +
+
+ +
+
+
+ Carregando parcelamentos... + Aguarde enquanto os dados sao atualizados. +
+
+
+ + + +
+
+
+ +
+
+
+ Falha ao carregar dados + {{ errorMessage }} +
+
+ +
+
+
+ Nenhum parcelamento encontrado + Altere os filtros para tentar novamente. +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Ano ref.LinhaClienteStatusParcela atualValor cheioDescontoValor c/ descontoAcoes
{{ row.anoRef ?? '-' }}{{ row.linha || '-' }}{{ row.cliente || '-' }} + + {{ row.statusLabel }} + + {{ row.progressLabel || '-' }} + {{ row.valorCheioNumber === null || row.valorCheioNumber === undefined + ? '-' : (row.valorCheioNumber | currency:'BRL':'symbol':'1.2-2':'pt-BR') }} + + {{ row.descontoNumber === null || row.descontoNumber === undefined + ? '-' : (row.descontoNumber | currency:'BRL':'symbol':'1.2-2':'pt-BR') }} + + {{ row.valorComDescontoNumber === null || row.valorComDescontoNumber === undefined + ? '-' : (row.valorComDescontoNumber | currency:'BRL':'symbol':'1.2-2':'pt-BR') }} + +
+ + + + + +
+
+
+ + +
diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss new file mode 100644 index 0000000..f1c8de2 --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss @@ -0,0 +1,499 @@ +:host { + display: block; + min-width: 0; +} + +.table-card { + border: 1px solid var(--pg-border, #dbe3ef); + border-radius: var(--pg-radius-md, 14px); + overflow: hidden; + background: #fff; + box-shadow: var(--pg-shadow-sm, 0 8px 18px rgba(15, 23, 42, 0.08)); +} + +.table-head { + padding: 14px 16px; + border-bottom: 1px solid var(--pg-border, #dbe3ef); + background: linear-gradient(180deg, #f8fbff, #ffffff 75%); + display: grid; + gap: 12px; +} + +.table-head-left { + display: grid; + gap: 4px; + + h3 { + margin: 0; + font-size: 1rem; + font-weight: 800; + color: var(--pg-text, #0f172a); + } + + small { + color: var(--pg-text-soft, #64748b); + font-size: 12px; + font-weight: 600; + } +} + +.segmented { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.segment-btn { + border: 1px solid var(--pg-border-strong, #c8d4e4); + border-radius: 999px; + background: #fff; + color: var(--pg-text-muted, #475569); + padding: 7px 12px; + font-size: 12px; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 7px; + cursor: pointer; + transition: all 0.2s ease; + + &:focus-visible { + outline: none; + box-shadow: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); + } +} + +.segment-btn:hover { + border-color: var(--pg-primary, #1f4fd6); + color: var(--pg-primary-strong, #153caa); +} + +.segment-btn.active { + background: rgba(31, 79, 214, 0.12); + border-color: rgba(31, 79, 214, 0.3); + color: var(--pg-primary-strong, #153caa); +} + +.segment-btn .count { + border-radius: 999px; + padding: 2px 7px; + font-size: 11px; + font-weight: 800; + background: rgba(15, 23, 42, 0.08); + color: var(--pg-text-muted, #475569); +} + +.table-state { + padding: 24px 16px; + display: grid; + gap: 12px; + justify-items: center; + text-align: center; +} + +.state-icon { + width: 44px; + height: 44px; + border-radius: 12px; + border: 1px solid var(--pg-border, #dbe3ef); + background: var(--pg-surface-alt, #f8fafc); + color: var(--pg-text-muted, #475569); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.15rem; +} + +.state-copy { + display: grid; + gap: 4px; + + strong { + color: var(--pg-text, #0f172a); + font-size: 0.92rem; + font-weight: 800; + } + + span { + color: var(--pg-text-soft, #64748b); + font-size: 0.82rem; + font-weight: 600; + } +} + +.table-state.error .state-icon { + color: var(--pg-danger, #c52929); + border-color: rgba(197, 41, 41, 0.32); + background: rgba(197, 41, 41, 0.08); +} + +.table-state.empty .state-icon { + color: var(--pg-warning, #b4690e); + border-color: rgba(180, 105, 14, 0.32); + background: rgba(180, 105, 14, 0.1); +} + +.skeleton-group { + width: min(760px, 100%); + display: grid; + gap: 8px; +} + +.skeleton-row { + display: grid; + gap: 8px; + grid-template-columns: 1fr 1fr 1fr; +} + +.skeleton-line { + height: 10px; + border-radius: 999px; + background: linear-gradient(90deg, #e6edf8, #dbe6f3, #e6edf8); + background-size: 240px 100%; + animation: shimmer 1.3s infinite linear; +} + +.parcelamentos-table-wrap { + width: 100%; + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; +} + +.table-modern { + width: max-content; + min-width: 1120px; + border-collapse: separate; + border-spacing: 0; + + thead th { + position: sticky; + top: 0; + z-index: 2; + padding: 11px 10px; + border-bottom: 1px solid var(--pg-border-strong, #c8d4e4); + background: #f5f9ff; + color: var(--pg-text-soft, #64748b); + font-size: 0.67rem; + letter-spacing: 0.05em; + text-transform: uppercase; + font-weight: 800; + white-space: nowrap; + text-align: left; + } + + tbody tr { + transition: background 0.18s ease; + } + + tbody tr:nth-child(even) { + background: #f9fbff; + } + + tbody tr:hover { + background: rgba(31, 79, 214, 0.08); + } + + td { + padding: 11px 10px; + border-bottom: 1px solid #e9eef6; + font-size: 0.84rem; + color: var(--pg-text, #0f172a); + white-space: nowrap; + text-align: left; + vertical-align: middle; + } +} + +.table-row { + cursor: default; +} + +.nowrap { + white-space: nowrap; +} + +.col-ano { + width: 90px; +} + +.col-linha { + width: 138px; +} + +.col-cliente { + width: 260px; + max-width: 260px; +} + +.col-status { + width: 110px; +} + +.col-parcela { + width: 130px; +} + +.col-valor { + width: 150px; + text-align: right; +} + +.col-acoes { + width: 152px; + min-width: 152px; + text-align: center; +} + +.table-modern thead .col-ano, +.table-modern thead .col-linha, +.table-modern thead .col-cliente, +.table-modern thead .col-status, +.table-modern thead .col-parcela, +.table-modern thead .col-acoes, +.table-modern tbody .col-ano, +.table-modern tbody .col-linha, +.table-modern tbody .col-cliente, +.table-modern tbody .col-status, +.table-modern tbody .col-parcela, +.table-modern tbody .col-acoes { + text-align: center; +} + +.text-end { + text-align: right; +} + +.text-center { + text-align: center; +} + +.text-blue { + color: var(--pg-primary-strong, #153caa); +} + +.text-danger { + color: var(--pg-danger, #c52929); +} + +.text-muted { + color: var(--pg-text-muted, #475569); +} + +.fw-bold { + font-weight: 700; +} + +.fw-black { + font-weight: 800; +} + +.money-strong { + color: var(--pg-primary, #1f4fd6); + font-weight: 800; +} + +.status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid #d5dfef; + background: #f2f6fc; + color: #334155; + font-size: 11px; + font-weight: 800; +} + +.status-ativos { + background: rgba(28, 122, 62, 0.14); + color: #1c7a3e; + border-color: rgba(28, 122, 62, 0.28); +} + +.status-futuros { + background: rgba(31, 79, 214, 0.12); + color: #1f4fd6; + border-color: rgba(31, 79, 214, 0.28); +} + +.status-finalizados { + background: rgba(197, 41, 41, 0.12); + color: #b42323; + border-color: rgba(197, 41, 41, 0.25); +} + +.action-group { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + flex-wrap: nowrap; +} + +.btn-icon { + width: 34px; + height: 34px; + border: 1px solid #cfd9e9; + border-radius: 10px; + background: #eff4ff; + color: var(--pg-primary-strong, #153caa); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.18s ease; + + &:hover { + transform: translateY(-1px); + border-color: var(--pg-primary, #1f4fd6); + background: #e3edff; + } + + &:focus-visible { + outline: none; + box-shadow: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); + } +} + +.btn-icon.ghost { + background: #f4f6fb; + color: #475569; +} + +.btn-icon.danger { + background: rgba(197, 41, 41, 0.12); + color: #b42323; + border-color: rgba(197, 41, 41, 0.22); +} + +.table-footer { + border-top: 1px solid var(--pg-border, #dbe3ef); + background: #fff; + padding: 12px 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.page-info { + color: var(--pg-text-muted, #475569); + font-size: 12px; + font-weight: 700; +} + +.pagination { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn-ghost.icon-only, +.btn-page { + width: 36px; + height: 36px; + border-radius: 10px; + border: 1px solid var(--pg-border-strong, #c8d4e4); + background: #fff; + color: var(--pg-text, #0f172a); + font-size: 12px; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + + &:focus-visible { + outline: none; + box-shadow: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); + } + + &:disabled { + opacity: 0.55; + cursor: not-allowed; + } +} + +.btn-page.active { + color: #fff; + border-color: var(--pg-primary-strong, #153caa); + background: linear-gradient(140deg, var(--pg-primary, #1f4fd6), var(--pg-primary-strong, #153caa)); +} + +.page-size { + display: inline-flex; + align-items: center; + gap: 8px; + + span { + color: var(--pg-text-soft, #64748b); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; + } +} + +.select-glass { + min-width: 76px; + height: 36px; + border-radius: 10px; + border: 1px solid var(--pg-border-strong, #c8d4e4); + background: #fff; + color: var(--pg-text, #0f172a); + font-weight: 700; + padding: 0 8px; +} + +@keyframes shimmer { + 0% { + background-position: -120px 0; + } + + 100% { + background-position: 120px 0; + } +} + +@media (max-width: 1180px) { + .table-modern { + min-width: 1020px; + } + + .col-cliente { + width: 210px; + max-width: 210px; + } +} + +@media (max-width: 760px) { + .table-head, + .table-footer { + padding-left: 12px; + padding-right: 12px; + } + + .segmented { + width: 100%; + } + + .segment-btn { + flex: 1; + justify-content: space-between; + min-width: 0; + } + + .table-footer { + justify-content: center; + } + + .page-size { + width: 100%; + justify-content: center; + } +} diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts new file mode 100644 index 0000000..fa8d84e --- /dev/null +++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ParcelamentoListItem } from '../../../../services/parcelamentos.service'; + +export type ParcelamentoSegment = 'todos' | 'ativos' | 'futuros' | 'finalizados'; + +export type ParcelamentoViewItem = ParcelamentoListItem & { + status: 'ativos' | 'futuros' | 'finalizados'; + statusLabel: string; + progressLabel: string; + valorParcela?: number | null; + valorCheioNumber?: number | null; + descontoNumber?: number | null; + valorComDescontoNumber?: number | null; +}; + +@Component({ + selector: 'app-parcelamentos-table', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './parcelamentos-table.html', + styleUrls: ['./parcelamentos-table.scss'], +}) +export class ParcelamentosTableComponent { + @Input() items: ParcelamentoViewItem[] = []; + @Input() loading = false; + @Input() errorMessage = ''; + @Input() isAdmin = false; + + @Input() segment: ParcelamentoSegment = 'todos'; + @Input() segmentCounts: Record = { + todos: 0, + ativos: 0, + futuros: 0, + finalizados: 0, + }; + + @Input() page = 1; + @Input() pageNumbers: number[] = []; + @Input() pageStart = 0; + @Input() pageEnd = 0; + @Input() total = 0; + @Input() pageSize = 10; + @Input() pageSizeOptions: number[] = []; + + @Output() segmentChange = new EventEmitter(); + @Output() detail = new EventEmitter(); + @Output() edit = new EventEmitter(); + @Output() remove = new EventEmitter(); + @Output() pageChange = new EventEmitter(); + @Output() pageSizeChange = new EventEmitter(); + + readonly segments: Array<{ key: ParcelamentoSegment; label: string }> = [ + { key: 'todos', label: 'Lista geral' }, + { key: 'ativos', label: 'Ativos' }, + { key: 'futuros', label: 'Futuros' }, + { key: 'finalizados', label: 'Finalizados' }, + ]; + + skeletonRows = Array.from({ length: 6 }); + + trackById(_: number, item: ParcelamentoViewItem): string { + return item.id; + } +} diff --git a/src/app/pages/parcelamentos/parcelamentos.html b/src/app/pages/parcelamentos/parcelamentos.html new file mode 100644 index 0000000..619e398 --- /dev/null +++ b/src/app/pages/parcelamentos/parcelamentos.html @@ -0,0 +1,251 @@ +
+
+
+ + + + + + + + + +
+
+
+ + +
+
+
+ + + + + +
+
+ + + + + + + + +
+
+ +
diff --git a/src/app/pages/parcelamentos/parcelamentos.scss b/src/app/pages/parcelamentos/parcelamentos.scss new file mode 100644 index 0000000..6c25851 --- /dev/null +++ b/src/app/pages/parcelamentos/parcelamentos.scss @@ -0,0 +1,614 @@ +:host { + --pg-font-sans: 'IBM Plex Sans', 'Source Sans 3', 'Manrope', 'Segoe UI', sans-serif; + + --pg-primary: #1f4fd6; + --pg-primary-strong: #153caa; + --pg-primary-soft: rgba(31, 79, 214, 0.12); + --pg-primary-soft-2: rgba(31, 79, 214, 0.18); + + --pg-text: #0f172a; + --pg-text-muted: #475569; + --pg-text-soft: #64748b; + + --pg-bg: #f3f6fb; + --pg-surface: #ffffff; + --pg-surface-alt: #f8fafc; + + --pg-border: #dbe3ef; + --pg-border-strong: #c8d4e4; + + --pg-warning: #b4690e; + --pg-warning-soft: rgba(180, 105, 14, 0.14); + --pg-danger: #c52929; + --pg-danger-soft: rgba(197, 41, 41, 0.12); + --pg-success: #1c7a3e; + + --pg-radius-sm: 10px; + --pg-radius-md: 14px; + --pg-radius-lg: 18px; + + --pg-shadow-sm: 0 8px 18px rgba(15, 23, 42, 0.08); + --pg-shadow-md: 0 16px 32px rgba(15, 23, 42, 0.12); + --pg-shadow-lg: 0 24px 56px rgba(15, 23, 42, 0.25); + + --pg-focus-ring: 0 0 0 3px rgba(31, 79, 214, 0.22); + + --brand: var(--pg-primary); + --blue: var(--pg-primary-strong); + --text: var(--pg-text); + --muted: var(--pg-text-muted); + --focus-ring: var(--pg-focus-ring); + + display: block; + color: var(--pg-text); + font-family: var(--pg-font-sans); +} + +.parcelamentos-page { + min-height: 100vh; + padding: 0 12px 72px; + display: flex; + justify-content: center; + position: relative; + background: + radial-gradient(1100px 450px at 10% -10%, rgba(31, 79, 214, 0.11), transparent 60%), + radial-gradient(900px 420px at 100% 0%, rgba(30, 64, 175, 0.07), transparent 58%), + linear-gradient(180deg, #f9fbff 0%, var(--pg-bg) 75%); +} + +.container-geral-responsive { + width: 100%; + max-width: 1280px; + position: relative; + z-index: 1; + margin-top: 28px; +} + +.parcelamentos-shell { + display: grid; + gap: 16px; + min-width: 0; +} + +.parcelamentos-shell > * { + min-width: 0; +} + +.page-header { + display: grid; + gap: 14px; + border: 1px solid var(--pg-border); + border-radius: var(--pg-radius-lg); + padding: 18px 20px; + background: linear-gradient(165deg, rgba(255, 255, 255, 0.98), rgba(245, 249, 255, 0.94)); + box-shadow: var(--pg-shadow-sm); +} + +.page-header-main { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.title-group { + display: grid; + gap: 8px; +} + +.title-badge { + display: inline-flex; + align-items: center; + gap: 8px; + width: fit-content; + padding: 6px 11px; + border-radius: 999px; + background: var(--pg-primary-soft); + border: 1px solid var(--pg-primary-soft-2); + color: var(--pg-primary-strong); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + + i { + color: var(--pg-primary); + } +} + +.header-title h2 { + margin: 0; + font-size: clamp(1.35rem, 2vw, 1.65rem); + font-weight: 800; + letter-spacing: -0.02em; +} + +.header-title p { + margin: 4px 0 0; + font-size: 0.92rem; + color: var(--pg-text-muted); + font-weight: 600; +} + +.header-actions { + display: inline-flex; + gap: 10px; + flex-wrap: wrap; +} + +.header-highlights { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.highlight-card { + border: 1px solid var(--pg-border); + border-radius: 12px; + background: rgba(255, 255, 255, 0.84); + padding: 10px 12px; + display: grid; + gap: 4px; + + span { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--pg-text-soft); + font-weight: 700; + } + + strong { + font-size: 0.95rem; + color: var(--pg-text); + font-weight: 800; + } +} + +.btn-primary, +.btn-ghost, +.btn-danger { + height: 40px; + border-radius: var(--pg-radius-sm); + border: 1px solid transparent; + font-size: 0.82rem; + font-weight: 700; + cursor: pointer; + padding: 0 14px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, background 0.18s ease; + + &:focus-visible { + outline: none; + box-shadow: var(--pg-focus-ring); + } + + &:disabled { + opacity: 0.62; + cursor: not-allowed; + transform: none; + box-shadow: none; + } +} + +.btn-primary { + color: #fff; + background: linear-gradient(140deg, var(--pg-primary), var(--pg-primary-strong)); + border-color: var(--pg-primary-strong); + box-shadow: 0 10px 22px rgba(31, 79, 214, 0.28); +} + +.btn-ghost { + color: var(--pg-text); + background: #fff; + border-color: var(--pg-border-strong); + box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08); +} + +.btn-danger { + color: #fff; + background: linear-gradient(145deg, #cf3131, #a91f1f); + border-color: #a91f1f; + box-shadow: 0 10px 20px rgba(169, 31, 31, 0.24); +} + +.btn-primary:hover, +.btn-ghost:hover, +.btn-danger:hover { + transform: translateY(-1px); +} + +.btn-ghost:hover { + border-color: var(--pg-primary); + color: var(--pg-primary-strong); +} + +.lg-backdrop { + position: fixed; + inset: 0; + background: + radial-gradient(circle at 15% 0%, rgba(31, 79, 214, 0.16), rgba(15, 23, 42, 0.64) 42%), + rgba(15, 23, 42, 0.6); + z-index: 9990; + backdrop-filter: blur(4px); +} + +.lg-modal { + position: fixed; + inset: 0; + z-index: 9995; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +.lg-modal-card { + width: min(1180px, 98vw); + max-height: 92vh; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--pg-surface); + border: 1px solid var(--pg-border); + border-radius: 18px; + box-shadow: var(--pg-shadow-lg); + animation: pop-up 0.24s ease; +} + +.modal-compact { + width: min(560px, 96vw); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 14px 18px; + border-bottom: 1px solid var(--pg-border); + background: linear-gradient(180deg, rgba(244, 248, 255, 0.96), rgba(255, 255, 255, 0.96)); +} + +.modal-title { + display: inline-flex; + align-items: center; + gap: 10px; + font-weight: 800; + color: var(--pg-text); +} + +.icon-bg { + width: 34px; + height: 34px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--pg-primary-soft); + color: var(--pg-primary-strong); +} + +.icon-bg.danger-soft { + background: var(--pg-danger-soft); + color: var(--pg-danger); +} + +.modal-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.btn-icon { + width: 34px; + height: 34px; + border: 1px solid var(--pg-border-strong); + border-radius: 10px; + background: #fff; + color: var(--pg-text-soft); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + color: var(--pg-primary-strong); + border-color: var(--pg-primary); + transform: translateY(-1px); + } + + &:focus-visible { + outline: none; + box-shadow: var(--pg-focus-ring); + } +} + +.modal-body { + padding: 18px; + background: linear-gradient(180deg, #f8fbff, #ffffff 80%); + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +.modal-footer { + padding: 14px 18px; + border-top: 1px solid var(--pg-border); + display: flex; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; + gap: 10px; + background: #fff; +} + +.detail-state { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + min-height: 180px; + color: var(--pg-text-muted); + font-weight: 700; +} + +.detail-state.error { + color: var(--pg-danger); +} + +.text-brand { + color: var(--pg-primary); +} + +.detail-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.detail-card { + border: 1px solid var(--pg-border); + border-radius: 12px; + padding: 11px 12px; + background: #fff; + display: grid; + gap: 6px; + color: var(--pg-text-muted); + + small { + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.66rem; + font-weight: 800; + } + + span { + color: var(--pg-text); + font-size: 0.88rem; + font-weight: 700; + } +} + +.detail-card.highlight { + grid-column: span 2; + background: linear-gradient(180deg, #ffffff, #f4f8ff); +} + +.detail-strong { + font-weight: 800; +} + +.text-blue { + color: var(--pg-primary-strong); +} + +.money-strong { + color: var(--pg-primary); +} + +.text-danger { + color: var(--pg-danger); +} + +.status-pill { + width: fit-content; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid var(--pg-primary-soft-2); + background: var(--pg-primary-soft); + color: var(--pg-primary-strong); + font-size: 0.74rem; + font-weight: 800; +} + +.annual-section { + margin-top: 16px; + border: 1px solid var(--pg-border); + border-radius: 14px; + padding: 12px; + background: #fff; + display: grid; + gap: 10px; +} + +.annual-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; +} + +.section-title { + display: inline-flex; + align-items: center; + gap: 7px; + font-weight: 800; + color: var(--pg-text); +} + +.annual-table-shell { + overflow-x: auto; + overflow-y: hidden; + border: 1px solid var(--pg-border); + border-radius: 12px; +} + +.annual-table { + width: max-content; + min-width: 1220px; + border-collapse: separate; + border-spacing: 0; + font-size: 12px; + + th, + td { + border-bottom: 1px solid #e7edf6; + padding: 8px 10px; + white-space: nowrap; + } + + thead th { + position: sticky; + top: 0; + z-index: 2; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--pg-text-soft); + background: #f6f9fe; + font-size: 10px; + font-weight: 800; + } +} + +.annual-section .sticky-col { + position: sticky; + left: 0; + z-index: 3; + background: #fff; + box-shadow: 2px 0 0 #eef3fb; +} + +.annual-section .col-1 { + left: 0; + min-width: 90px; +} + +.annual-section .col-2 { + left: 90px; + min-width: 128px; +} + +.annual-empty { + min-height: 92px; + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed var(--pg-border-strong); + border-radius: 10px; + font-size: 0.84rem; + font-weight: 700; + color: var(--pg-text-muted); + background: var(--pg-surface-alt); +} + +.confirm-delete { + min-height: 150px; + display: grid; + align-content: center; + justify-items: center; + gap: 10px; + text-align: center; + color: var(--pg-text-muted); + + p { + margin: 0; + color: var(--pg-text); + font-weight: 600; + } +} + +.confirm-icon { + width: 54px; + height: 54px; + border-radius: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--pg-danger); + border: 1px solid rgba(197, 41, 41, 0.22); + background: rgba(197, 41, 41, 0.1); + font-size: 1.15rem; +} + +@keyframes pop-up { + from { + opacity: 0; + transform: translateY(8px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@media (max-width: 1100px) { + .header-highlights { + grid-template-columns: 1fr; + } + + .detail-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 780px) { + .container-geral-responsive { + margin-top: 18px; + } + + .page-header, + .modal-body, + .modal-header, + .modal-footer { + padding-left: 14px; + padding-right: 14px; + } + + .header-actions { + width: 100%; + } + + .header-actions .btn-primary, + .header-actions .btn-ghost { + flex: 1; + } + + .detail-grid { + grid-template-columns: 1fr; + } + + .detail-card.highlight { + grid-column: span 1; + } + + .modal-footer { + justify-content: stretch; + } + + .modal-footer .btn-primary, + .modal-footer .btn-ghost, + .modal-footer .btn-danger { + flex: 1; + } +} diff --git a/src/app/pages/parcelamentos/parcelamentos.ts b/src/app/pages/parcelamentos/parcelamentos.ts new file mode 100644 index 0000000..d601b91 --- /dev/null +++ b/src/app/pages/parcelamentos/parcelamentos.ts @@ -0,0 +1,1091 @@ +import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { environment } from '../../../environments/environment'; +import { finalize, Subscription, timeout } from 'rxjs'; +import { AuthService } from '../../services/auth.service'; +import { + ParcelamentosService, + ParcelamentoListItem, + ParcelamentoDetail, + ParcelamentoDetailResponse, + ParcelamentoParcela, + ParcelamentoAnnualRow, + ParcelamentoAnnualMonth, + ParcelamentoUpsertRequest, + ParcelamentoMonthInput, +} from '../../services/parcelamentos.service'; +import { + ParcelamentosKpisComponent, + ParcelamentoKpi, +} from './components/parcelamentos-kpis/parcelamentos-kpis'; +import { + ParcelamentosFiltersComponent, + ParcelamentosFiltersModel, + FilterChip, +} from './components/parcelamentos-filters/parcelamentos-filters'; +import { + ParcelamentosTableComponent, + ParcelamentoSegment, + ParcelamentoViewItem, +} from './components/parcelamentos-table/parcelamentos-table'; +import { + ParcelamentoCreateModalComponent, + ParcelamentoCreateModel, +} from './components/parcelamento-create-modal/parcelamento-create-modal'; + +type MonthOption = { value: number; label: string }; +type ParcelamentoStatus = 'ativos' | 'futuros' | 'finalizados'; +type AnnualMonthValue = { month: number; label: string; value: number | null }; +type AnnualRow = { year: number; total: number; months: AnnualMonthValue[] }; + +@Component({ + selector: 'app-parcelamentos', + standalone: true, + imports: [ + CommonModule, + FormsModule, + ParcelamentosKpisComponent, + ParcelamentosFiltersComponent, + ParcelamentosTableComponent, + ParcelamentoCreateModalComponent, + ], + templateUrl: './parcelamentos.html', + styleUrls: ['./parcelamentos.scss'], +}) +export class Parcelamentos implements OnInit, OnDestroy { + loading = false; + errorMessage = ''; + + debugMode = !environment.production; + + items: ParcelamentoListItem[] = []; + total = 0; + page = 1; + pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; + + filters: ParcelamentosFiltersModel = { + anoRef: '', + linha: '', + cliente: '', + competenciaAno: '', + competenciaMes: '', + search: '', + }; + + activeSegment: ParcelamentoSegment = 'todos'; + segmentCounts: Record = { + todos: 0, + ativos: 0, + futuros: 0, + finalizados: 0, + }; + + viewItems: ParcelamentoViewItem[] = []; + kpiCards: ParcelamentoKpi[] = []; + activeChips: FilterChip[] = []; + + isAdmin = false; + + detailOpen = false; + detailLoading = false; + detailError = ''; + selectedDetail: ParcelamentoDetail | null = null; + + annualRows: AnnualRow[] = []; + readonly annualMonthHeaders = this.buildAnnualMonthHeaders(); + + private detailRequestSub?: Subscription; + private detailRequestToken = 0; + private detailGuardTimer?: ReturnType; + + debugYearGroups: { year: number; months: Array<{ label: string; value: string }> }[] = []; + + createOpen = false; + createSaving = false; + createError = ''; + createModel: ParcelamentoCreateModel = this.buildCreateModel(); + + editOpen = false; + editLoading = false; + editSaving = false; + editError = ''; + editModel: ParcelamentoCreateModel | null = null; + editId: string | null = null; + + deleteOpen = false; + deleteLoading = false; + deleteError = ''; + deleteTarget: ParcelamentoViewItem | null = null; + + readonly monthOptions: MonthOption[] = [ + { value: 1, label: '01 - Janeiro' }, + { value: 2, label: '02 - Fevereiro' }, + { value: 3, label: '03 - Marco' }, + { value: 4, label: '04 - Abril' }, + { value: 5, label: '05 - Maio' }, + { value: 6, label: '06 - Junho' }, + { value: 7, label: '07 - Julho' }, + { value: 8, label: '08 - Agosto' }, + { value: 9, label: '09 - Setembro' }, + { value: 10, label: '10 - Outubro' }, + { value: 11, label: '11 - Novembro' }, + { value: 12, label: '12 - Dezembro' }, + ]; + + constructor( + private parcelamentosService: ParcelamentosService, + private authService: AuthService + ) {} + + ngOnInit(): void { + this.syncPermissions(); + this.load(); + } + + ngOnDestroy(): void { + this.cancelDetailRequest(); + } + + @HostListener('document:keydown.escape') + onEscape(): void { + if (this.detailOpen) this.closeDetails(); + if (this.createOpen) this.closeCreateModal(); + if (this.editOpen) this.closeEditModal(); + if (this.deleteOpen) this.cancelDelete(); + } + + private syncPermissions(): void { + this.isAdmin = this.authService.hasRole('admin'); + } + + get totalPages(): number { + return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10))); + } + + get pageNumbers(): number[] { + const total = this.totalPages; + const current = this.page; + const max = 5; + let start = Math.max(1, current - 2); + let end = Math.min(total, start + (max - 1)); + start = Math.max(1, end - (max - 1)); + const pages: number[] = []; + for (let i = start; i <= end; i++) pages.push(i); + return pages; + } + + get pageStart(): number { + return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + } + + get pageEnd(): number { + return this.total === 0 ? 0 : Math.min(this.page * this.pageSize, this.total); + } + + get competenciaInvalid(): boolean { + const ano = this.parseNumber(this.filters.competenciaAno); + const mes = this.parseNumber(this.filters.competenciaMes); + const hasAno = ano !== null; + const hasMes = mes !== null; + return (hasAno || hasMes) && !(hasAno && hasMes); + } + + load(): void { + this.loading = true; + this.errorMessage = ''; + + const anoRef = this.parseNumber(this.filters.anoRef); + const competenciaAno = this.parseNumber(this.filters.competenciaAno); + const competenciaMes = this.parseNumber(this.filters.competenciaMes); + const sendCompetencia = competenciaAno !== null && competenciaMes !== null; + + this.parcelamentosService + .list({ + anoRef: anoRef ?? undefined, + linha: this.filters.linha?.trim() || undefined, + cliente: this.filters.cliente?.trim() || undefined, + competenciaAno: sendCompetencia ? competenciaAno ?? undefined : undefined, + competenciaMes: sendCompetencia ? competenciaMes ?? undefined : undefined, + page: this.page, + pageSize: this.pageSize, + }) + .subscribe({ + next: (res) => { + try { + const anyRes: any = res ?? {}; + const items = Array.isArray(anyRes.items) + ? anyRes.items.filter(Boolean) + : Array.isArray(anyRes.Items) + ? anyRes.Items.filter(Boolean) + : []; + this.items = items; + this.total = typeof anyRes.total === 'number' + ? anyRes.total + : (typeof anyRes.Total === 'number' ? anyRes.Total : 0); + this.loading = false; + this.updateDerived(); + } catch (e) { + console.error('Erro ao processar parcelamentos', e); + this.items = []; + this.total = 0; + this.loading = false; + this.errorMessage = 'Erro ao processar parcelamentos.'; + this.updateDerivedSafe(); + } + }, + error: () => { + this.items = []; + this.total = 0; + this.loading = false; + this.errorMessage = 'Erro ao carregar parcelamentos.'; + this.updateDerivedSafe(); + }, + }); + } + + applyFilters(): void { + if (this.competenciaInvalid) { + this.errorMessage = 'Informe ano e mes para filtrar competencia.'; + return; + } + this.page = 1; + this.load(); + } + + clearFilters(): void { + this.filters = { + anoRef: '', + linha: '', + cliente: '', + competenciaAno: '', + competenciaMes: '', + search: '', + }; + this.page = 1; + this.errorMessage = ''; + this.load(); + } + + refresh(): void { + this.load(); + } + + onPageSizeChange(size: number): void { + this.pageSize = size; + this.page = 1; + this.load(); + } + + goToPage(p: number): void { + this.page = Math.max(1, Math.min(this.totalPages, p)); + this.load(); + } + + setSegment(segment: ParcelamentoSegment): void { + this.activeSegment = segment; + this.updateDerivedSafe(); + } + + onSearchChange(term: string): void { + this.filters.search = term; + this.updateDerivedSafe(); + } + + openDetails(item: ParcelamentoListItem): void { + const id = this.getItemId(item); + if (!id) { + this.detailOpen = true; + this.detailLoading = false; + this.detailError = 'Registro sem identificador para carregar detalhes.'; + this.selectedDetail = null; + this.annualRows = []; + return; + } + + this.cancelDetailRequest(); + const currentToken = ++this.detailRequestToken; + + this.detailOpen = true; + this.detailLoading = true; + this.detailError = ''; + this.selectedDetail = null; + this.startDetailGuard(currentToken, item); + + this.detailRequestSub = this.parcelamentosService + .getById(id) + .pipe( + timeout(15000), + finalize(() => { + if (!this.isCurrentDetailRequest(currentToken)) return; + this.clearDetailGuard(); + this.detailLoading = false; + }) + ) + .subscribe({ + next: (res) => { + if (!this.isCurrentDetailRequest(currentToken)) return; + try { + this.selectedDetail = this.normalizeDetail(res); + this.prepareAnnual(this.selectedDetail); + this.debugYearGroups = this.buildDebugYearGroups(this.selectedDetail); + } catch { + this.applyDetailFallback(item); + } + this.detailLoading = false; + }, + error: () => { + if (!this.isCurrentDetailRequest(currentToken)) return; + this.applyDetailFallback(item); + this.detailLoading = false; + }, + }); + } + + closeDetails(): void { + this.cancelDetailRequest(); + + this.detailOpen = false; + this.detailLoading = false; + this.detailError = ''; + this.selectedDetail = null; + this.debugYearGroups = []; + this.annualRows = []; + } + + openCreateModal(): void { + this.createModel = this.buildCreateModel(); + this.createError = ''; + this.createOpen = true; + } + + closeCreateModal(): void { + this.createOpen = false; + this.createSaving = false; + this.createError = ''; + } + + saveNewParcelamento(model: ParcelamentoCreateModel): void { + if (this.createSaving) return; + this.createSaving = true; + this.createError = ''; + const payload = this.buildUpsertPayload(model); + this.parcelamentosService.create(payload) + .pipe(finalize(() => (this.createSaving = false))) + .subscribe({ + next: () => { + this.createOpen = false; + this.load(); + }, + error: () => { + this.createError = 'Erro ao salvar parcelamento.'; + }, + }); + } + + openEdit(item: ParcelamentoListItem): void { + const id = this.getItemId(item); + if (!id) return; + this.editOpen = true; + this.editLoading = true; + this.editError = ''; + this.editModel = this.buildCreateModel(); + this.editId = id; + + this.parcelamentosService + .getById(id) + .pipe( + timeout(15000), + finalize(() => (this.editLoading = false)) + ) + .subscribe({ + next: (res) => { + const detail = this.normalizeDetail(res); + this.editModel = this.buildEditModel(detail); + }, + error: () => { + this.editError = 'Erro ao carregar dados para editar.'; + }, + }); + } + + closeEditModal(): void { + this.editOpen = false; + this.editLoading = false; + this.editSaving = false; + this.editError = ''; + this.editModel = null; + this.editId = null; + } + + saveEditParcelamento(model: ParcelamentoCreateModel): void { + if (this.editSaving || !this.editModel || !this.editId) return; + this.editSaving = true; + this.editError = ''; + const payload = this.buildUpsertPayload(model); + this.parcelamentosService + .update(this.editId, payload) + .pipe(finalize(() => (this.editSaving = false))) + .subscribe({ + next: () => { + this.editOpen = false; + this.load(); + }, + error: () => { + this.editError = 'Erro ao atualizar parcelamento.'; + }, + }); + } + + openDelete(item: ParcelamentoViewItem): void { + if (!this.isAdmin) return; + this.deleteTarget = item; + this.deleteError = ''; + this.deleteOpen = true; + } + + cancelDelete(): void { + this.deleteOpen = false; + this.deleteLoading = false; + this.deleteError = ''; + this.deleteTarget = null; + } + + confirmDelete(): void { + if (!this.deleteTarget || this.deleteLoading) return; + const id = this.getItemId(this.deleteTarget); + if (!id) return; + this.deleteLoading = true; + this.deleteError = ''; + this.parcelamentosService + .delete(id) + .pipe(finalize(() => (this.deleteLoading = false))) + .subscribe({ + next: () => { + this.cancelDelete(); + this.load(); + }, + error: () => { + this.deleteError = 'Erro ao excluir parcelamento.'; + }, + }); + } + + displayQtParcelas(item: ParcelamentoListItem): string { + const atual = this.toNumber(item.parcelaAtual); + const total = this.toNumber(item.totalParcelas); + if (atual !== null && total !== null) return `${atual}/${total}`; + const raw = (item.qtParcelas ?? '').toString().trim(); + if (raw) return raw; + return '-'; + } + + formatMoney(value: any): string { + const n = this.toNumber(value); + if (n === null) return '-'; + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(n); + } + + formatNumber(value: any): string { + const n = this.toNumber(value); + if (n === null) return '-'; + return new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 0 }).format(n); + } + + formatCompetencia(value: any): string { + const date = this.parseCompetenciaDate(value); + if (!date) return value ? String(value) : '-'; + const label = new Intl.DateTimeFormat('pt-BR', { month: 'long' }).format(date); + const cap = label.charAt(0).toUpperCase() + label.slice(1); + return `${cap}/${date.getFullYear()}`; + } + + formatCompetenciaShort(value: any): string { + const date = this.parseCompetenciaDate(value); + if (!date) return value ? String(value) : '-'; + let label = new Intl.DateTimeFormat('pt-BR', { month: 'short' }).format(date); + label = label.replace('.', ''); + const cap = label.charAt(0).toUpperCase() + label.slice(1); + return `${cap}/${date.getFullYear()}`; + } + + private buildAnnualMonthHeaders(): Array<{ month: number; label: string }> { + return Array.from({ length: 12 }, (_, idx) => { + const date = new Date(2000, idx, 1); + let label = new Intl.DateTimeFormat('pt-BR', { month: 'short' }).format(date); + label = label.replace('.', ''); + label = label.charAt(0).toUpperCase() + label.slice(1); + return { month: idx + 1, label }; + }); + } + + get detailStatus(): string { + if (!this.selectedDetail) return '-'; + const atual = this.toNumber(this.selectedDetail.parcelaAtual); + const total = this.toNumber(this.selectedDetail.totalParcelas); + if (atual !== null && total !== null) return `${atual}/${total}`; + const raw = (this.selectedDetail.qtParcelas ?? '').toString().trim(); + return raw || '-'; + } + + private parseCompetenciaDate(value: any): Date | null { + if (!value) return null; + const raw = String(value); + const match = raw.match(/^(\d{4})-(\d{2})/); + if (match) { + const year = Number(match[1]); + const month = Number(match[2]); + return new Date(year, month - 1, 1); + } + const d = new Date(raw); + if (Number.isNaN(d.getTime())) return null; + return d; + } + + private updateDerived(): void { + const base = (this.items || []).map((item) => this.toViewItem(item)); + const searched = this.applySearch(base, this.filters.search); + + this.segmentCounts = { + todos: searched.length, + ativos: searched.filter((i) => i.status === 'ativos').length, + futuros: searched.filter((i) => i.status === 'futuros').length, + finalizados: searched.filter((i) => i.status === 'finalizados').length, + }; + + this.viewItems = + this.activeSegment === 'todos' + ? searched + : searched.filter((i) => i.status === this.activeSegment); + + this.kpiCards = this.buildKpis(searched); + this.activeChips = this.buildActiveChips(); + } + + private updateDerivedSafe(): void { + try { + this.updateDerived(); + } catch (e) { + console.error('Erro ao atualizar parcelamentos', e); + this.viewItems = []; + this.kpiCards = []; + this.activeChips = []; + this.segmentCounts = { + todos: 0, + ativos: 0, + futuros: 0, + finalizados: 0, + }; + } + } + + private buildKpis(list: ParcelamentoViewItem[]): ParcelamentoKpi[] { + const totalContratado = list.reduce((sum, item) => sum + (item.valorComDescontoNumber ?? item.valorCheioNumber ?? 0), 0); + const totalCheio = list.reduce((sum, item) => sum + (item.valorCheioNumber ?? 0), 0); + const totalDesconto = list.reduce((sum, item) => sum + (item.descontoNumber ?? 0), 0); + + const parcelasEmAberto = list.reduce((sum, item) => { + const total = this.toNumber(item.totalParcelas); + const atual = this.toNumber(item.parcelaAtual); + if (total === null || atual === null) return sum; + return sum + Math.max(0, total - atual); + }, 0); + + const competenciaAno = this.parseNumber(this.filters.competenciaAno); + const competenciaMes = this.parseNumber(this.filters.competenciaMes); + const totalMensalEstimado = + competenciaAno !== null && competenciaMes !== null + ? list.reduce((sum, item) => sum + (item.valorParcela ?? 0), 0) + : null; + + const anoRef = this.parseNumber(this.filters.anoRef); + const totalAnual = anoRef !== null || competenciaAno !== null ? totalContratado : null; + + return [ + { + label: 'Total mensal (estimado)', + value: totalMensalEstimado !== null ? this.formatMoney(totalMensalEstimado) : '-', + hint: competenciaAno && competenciaMes ? 'Baseado nas parcelas da lista' : 'Selecione competencia', + tone: 'brand', + }, + { + label: 'Total anual (ano selecionado)', + value: totalAnual !== null ? this.formatMoney(totalAnual) : '-', + hint: anoRef || competenciaAno ? 'Baseado nos contratos filtrados' : 'Selecione ano', + }, + { + label: 'Parcelamentos ativos', + value: this.formatNumber(this.segmentCounts.ativos), + hint: 'Status atual', + tone: 'success', + }, + { + label: 'Parcelas em aberto', + value: this.formatNumber(parcelasEmAberto), + hint: 'Estimado por parcela atual', + }, + { + label: 'Valor total contratado', + value: this.formatMoney(totalContratado || totalCheio), + hint: totalDesconto ? `Desconto total: ${this.formatMoney(totalDesconto)}` : undefined, + }, + ]; + } + + private buildActiveChips(): FilterChip[] { + const chips: FilterChip[] = []; + if (this.filters.anoRef) chips.push({ label: 'AnoRef', value: this.filters.anoRef }); + if (this.filters.linha) chips.push({ label: 'Linha', value: this.filters.linha }); + if (this.filters.cliente) chips.push({ label: 'Cliente', value: this.filters.cliente }); + const ano = this.filters.competenciaAno; + const mes = this.filters.competenciaMes; + if (ano && mes) { + chips.push({ label: 'Competencia', value: `${String(mes).padStart(2, '0')}/${ano}` }); + } + if (this.filters.search) chips.push({ label: 'Busca', value: this.filters.search }); + return chips; + } + + private toViewItem(item: ParcelamentoListItem): ParcelamentoViewItem { + const id = this.getItemId(item) ?? ''; + const status = this.resolveStatus(item); + const valorCheio = this.toNumber(item.valorCheio); + const desconto = this.toNumber(item.desconto); + const valorComDesconto = this.toNumber(item.valorComDesconto); + return { + ...item, + id, + status, + statusLabel: this.statusLabel(status), + progressLabel: this.displayQtParcelas(item), + valorParcela: this.computeParcelaValue(item), + valorCheioNumber: valorCheio, + descontoNumber: desconto, + valorComDescontoNumber: valorComDesconto ?? (valorCheio !== null && desconto !== null ? Math.max(0, valorCheio - desconto) : null), + }; + } + + private applySearch(list: ParcelamentoViewItem[], term: string): ParcelamentoViewItem[] { + const search = this.normalizeText(term); + if (!search) return list; + return list.filter((item) => { + const payload = [ + item.anoRef, + item.item, + item.linha, + item.cliente, + item.qtParcelas, + ] + .map((v) => (v ?? '').toString()) + .join(' '); + return this.normalizeText(payload).includes(search); + }); + } + + private resolveStatus(item: ParcelamentoListItem): ParcelamentoStatus { + const total = this.toNumber(item.totalParcelas); + const atual = this.toNumber(item.parcelaAtual); + if (total !== null && atual !== null) { + if (atual >= total) return 'finalizados'; + if (atual <= 0) return 'futuros'; + return 'ativos'; + } + + const parsed = this.parseQtParcelas(item.qtParcelas); + if (parsed) { + if (parsed.atual >= parsed.total) return 'finalizados'; + return 'ativos'; + } + + return 'ativos'; + } + + private statusLabel(status: ParcelamentoStatus): string { + if (status === 'finalizados') return 'Finalizado'; + if (status === 'futuros') return 'Futuro'; + return 'Ativo'; + } + + private computeParcelaValue(item: ParcelamentoListItem): number | null { + const totalParcelas = this.toNumber(item.totalParcelas) ?? this.parseQtParcelas(item.qtParcelas)?.total ?? null; + if (!totalParcelas) return null; + const total = this.toNumber(item.valorComDesconto) ?? this.toNumber(item.valorCheio); + if (total === null) return null; + return total / totalParcelas; + } + + private parseQtParcelas(value: any): { atual: number; total: number } | null { + if (!value) return null; + const raw = String(value); + const parts = raw.split('/'); + if (parts.length < 2) return null; + const atualStr = this.onlyDigits(parts[0]); + const totalStr = this.onlyDigits(parts[1]); + if (!atualStr || !totalStr) return null; + return { atual: Number(atualStr), total: Number(totalStr) }; + } + + private prepareAnnual(detail: ParcelamentoDetail): void { + this.annualRows = this.buildAnnualRows(detail); + } + + private buildAnnualRows(detail: ParcelamentoDetail): AnnualRow[] { + const fromApi = this.mapAnnualRows(detail.annualRows ?? []); + if (fromApi.length) return fromApi; + return this.buildAnnualRowsFromParcelas(detail.parcelasMensais ?? []); + } + + private mapAnnualRows(rawRows?: ParcelamentoAnnualRow[] | null): AnnualRow[] { + const rows = (rawRows ?? []) + .filter((row): row is ParcelamentoAnnualRow => !!row) + .map((row) => { + const year = this.parseNumber(row.year); + if (year === null) return null; + + const monthMap = new Map(); + (row.months ?? []).forEach((m) => { + const month = this.parseNumber(m.month); + if (month === null) return; + const value = this.toNumber(m.valor); + monthMap.set(month, value); + }); + + const months = this.annualMonthHeaders.map((h) => ({ + month: h.month, + label: h.label, + value: monthMap.get(h.month) ?? null, + })); + + const total = this.toNumber(row.total) ?? months.reduce((sum, m) => sum + (m.value ?? 0), 0); + return { year, total, months }; + }) + .filter((row): row is AnnualRow => !!row) + .sort((a, b) => a.year - b.year); + + return rows; + } + + private buildAnnualRowsFromParcelas(parcelas: ParcelamentoParcela[]): AnnualRow[] { + const map = new Map>(); + + (parcelas ?? []).forEach((p) => { + const parsed = this.parseCompetenciaParts(p.competencia); + if (!parsed) return; + const value = this.toNumber(p.valor) ?? 0; + if (!map.has(parsed.year)) map.set(parsed.year, new Map()); + const monthMap = map.get(parsed.year)!; + monthMap.set(parsed.month, (monthMap.get(parsed.month) ?? 0) + value); + }); + + return Array.from(map.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([year, monthMap]) => { + const months = this.annualMonthHeaders.map((h) => ({ + month: h.month, + label: h.label, + value: monthMap.get(h.month) ?? null, + })); + const total = months.reduce((sum, m) => sum + (m.value ?? 0), 0); + return { year, total, months }; + }); + } + + private parseCompetenciaParts(value: any): { year: number; month: number } | null { + const date = this.parseCompetenciaDate(value); + if (!date) return null; + return { year: date.getFullYear(), month: date.getMonth() + 1 }; + } + + private normalizeDetail(res: ParcelamentoDetailResponse): ParcelamentoDetail { + const payload = (res ?? {}) as any; + const parcelasRaw = + payload.parcelasMensais ?? + payload.ParcelasMensais ?? + payload.parcelas ?? + payload.Parcelas ?? + payload.monthValues ?? + payload.MonthValues ?? + []; + + const parcelasMensais = Array.isArray(parcelasRaw) + ? parcelasRaw + .filter((p) => !!p && typeof p === 'object') + .map((p: any) => ({ + competencia: (p.competencia ?? p.Competencia ?? '').toString(), + valor: p.valor ?? p.Valor ?? null, + })) + .filter((p) => !!p.competencia) + : []; + + const annualRowsRaw = + payload.annualRows ?? + payload.AnnualRows ?? + payload.detalhamentoAnual ?? + payload.DetalhamentoAnual ?? + []; + + const annualRows: ParcelamentoAnnualRow[] = []; + if (Array.isArray(annualRowsRaw)) { + annualRowsRaw.forEach((row: any) => { + if (!row || typeof row !== 'object') return; + const year = this.parseNumber(row.year ?? row.Year); + if (year === null) return; + const monthsRaw = row.months ?? row.Months ?? []; + const months: ParcelamentoAnnualMonth[] = []; + if (Array.isArray(monthsRaw)) { + monthsRaw.forEach((m: any) => { + if (!m || typeof m !== 'object') return; + const month = this.parseNumber(m.month ?? m.Month); + if (month === null) return; + const valor = (m.valor ?? m.Valor ?? m.value ?? m.Value ?? null) as number | string | null; + months.push({ + month, + valor, + }); + }); + } + const total = (row.total ?? row.Total ?? null) as number | string | null; + annualRows.push({ + year, + total, + months, + }); + }); + } + + return { + id: payload.id ?? payload.Id ?? '', + anoRef: payload.anoRef ?? payload.AnoRef ?? null, + item: payload.item ?? payload.Item ?? null, + linha: payload.linha ?? payload.Linha ?? null, + cliente: payload.cliente ?? payload.Cliente ?? null, + qtParcelas: payload.qtParcelas ?? payload.QtParcelas ?? null, + parcelaAtual: payload.parcelaAtual ?? payload.ParcelaAtual ?? null, + totalParcelas: payload.totalParcelas ?? payload.TotalParcelas ?? null, + valorCheio: payload.valorCheio ?? payload.ValorCheio ?? null, + desconto: payload.desconto ?? payload.Desconto ?? null, + valorComDesconto: payload.valorComDesconto ?? payload.ValorComDesconto ?? null, + parcelasMensais, + annualRows, + }; + } + + private buildCreateModel(): ParcelamentoCreateModel { + const now = new Date(); + return { + anoRef: now.getFullYear(), + linha: '', + cliente: '', + item: null, + qtParcelas: '', + parcelaAtual: null, + totalParcelas: 12, + valorCheio: '', + desconto: '', + valorComDesconto: '', + competenciaAno: now.getFullYear(), + competenciaMes: now.getMonth() + 1, + monthValues: [], + }; + } + + private buildEditModel(detail: ParcelamentoDetail): ParcelamentoCreateModel { + const parcelas = (detail.parcelasMensais ?? []) + .filter((p) => !!p && !!p.competencia) + .map((p) => ({ + competencia: p.competencia, + valor: this.normalizeInputValue(p.valor), + })) + .sort((a, b) => a.competencia.localeCompare(b.competencia)); + + const firstCompetencia = parcelas.length ? parcelas[0].competencia : ''; + const compParts = firstCompetencia.match(/^(\d{4})-(\d{2})/); + const competenciaAno = compParts ? Number(compParts[1]) : null; + const competenciaMes = compParts ? Number(compParts[2]) : null; + + return { + anoRef: detail.anoRef ?? null, + linha: detail.linha ?? '', + cliente: detail.cliente ?? '', + item: this.toNumber(detail.item) ?? null, + qtParcelas: detail.qtParcelas ?? '', + parcelaAtual: this.toNumber(detail.parcelaAtual), + totalParcelas: this.toNumber(detail.totalParcelas), + valorCheio: this.normalizeInputValue(detail.valorCheio), + desconto: this.normalizeInputValue(detail.desconto), + valorComDesconto: this.normalizeInputValue(detail.valorComDesconto), + competenciaAno, + competenciaMes, + monthValues: parcelas, + }; + } + + private buildUpsertPayload(model: ParcelamentoCreateModel): ParcelamentoUpsertRequest { + const monthValues: ParcelamentoMonthInput[] = (model.monthValues ?? []) + .filter((m) => !!m && !!m.competencia) + .map((m) => ({ + competencia: m.competencia, + valor: this.toNumber(m.valor), + })); + + return { + anoRef: this.toNumber(model.anoRef), + item: this.toNumber(model.item), + linha: model.linha?.trim() || null, + cliente: model.cliente?.trim() || null, + qtParcelas: model.qtParcelas?.trim() || null, + parcelaAtual: this.toNumber(model.parcelaAtual), + totalParcelas: this.toNumber(model.totalParcelas), + valorCheio: this.toNumber(model.valorCheio), + desconto: this.toNumber(model.desconto), + valorComDesconto: this.toNumber(model.valorComDesconto), + monthValues, + }; + } + + private normalizeInputValue(value: any): string { + const n = this.toNumber(value); + if (n === null) return ''; + return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); + } + + private parseNumber(value: any): number | null { + if (value === null || value === undefined || value === '') return null; + const n = Number(value); + return Number.isNaN(n) ? null : n; + } + + private toNumber(value: any): number | null { + if (value === null || value === undefined) return null; + if (typeof value === 'number') return Number.isFinite(value) ? value : null; + const raw = String(value).trim(); + if (!raw) return null; + + let cleaned = raw.replace(/[^\d,.-]/g, ''); + + if (cleaned.includes(',') && cleaned.includes('.')) { + if (cleaned.lastIndexOf(',') > cleaned.lastIndexOf('.')) { + cleaned = cleaned.replace(/\./g, '').replace(',', '.'); + } else { + cleaned = cleaned.replace(/,/g, ''); + } + } else if (cleaned.includes(',')) { + cleaned = cleaned.replace(/\./g, '').replace(',', '.'); + } else { + cleaned = cleaned.replace(/,/g, ''); + } + + const n = Number(cleaned); + return Number.isNaN(n) ? null : n; + } + + private normalizeText(value: any): string { + return (value ?? '') + .toString() + .trim() + .toUpperCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + } + + private onlyDigits(value: string): string { + let out = ''; + for (const ch of value ?? '') { + if (ch >= '0' && ch <= '9') out += ch; + } + return out; + } + + private getItemId(item: ParcelamentoListItem | null | undefined): string | null { + if (!item) return null; + const raw = (item as any).id ?? (item as any).Id ?? (item as any).parcelamentoId ?? (item as any).ParcelamentoId; + if (raw === null || raw === undefined) return null; + const value = String(raw).trim(); + return value ? value : null; + } + + private cancelDetailRequest(): void { + this.clearDetailGuard(); + this.detailRequestToken++; + if (this.detailRequestSub) { + this.detailRequestSub.unsubscribe(); + this.detailRequestSub = undefined; + } + } + + private isCurrentDetailRequest(token: number): boolean { + return token === this.detailRequestToken; + } + + private startDetailGuard(token: number, item: ParcelamentoListItem): void { + this.clearDetailGuard(); + this.detailGuardTimer = setTimeout(() => { + if (!this.isCurrentDetailRequest(token)) return; + this.cancelDetailRequest(); + this.applyDetailFallback(item); + this.detailLoading = false; + }, 20000); + } + + private clearDetailGuard(): void { + if (this.detailGuardTimer) { + clearTimeout(this.detailGuardTimer); + this.detailGuardTimer = undefined; + } + } + + private applyDetailFallback(item: ParcelamentoListItem): void { + this.detailError = ''; + this.selectedDetail = this.buildFallbackDetail(item); + this.prepareAnnual(this.selectedDetail); + this.debugYearGroups = this.buildDebugYearGroups(this.selectedDetail); + this.detailLoading = false; + } + + private buildFallbackDetail(item: ParcelamentoListItem): ParcelamentoDetail { + return { + id: this.getItemId(item) ?? '', + anoRef: item.anoRef ?? null, + item: item.item ?? null, + linha: item.linha ?? null, + cliente: item.cliente ?? null, + qtParcelas: item.qtParcelas ?? null, + parcelaAtual: item.parcelaAtual ?? null, + totalParcelas: item.totalParcelas ?? null, + valorCheio: item.valorCheio ?? null, + desconto: item.desconto ?? null, + valorComDesconto: item.valorComDesconto ?? null, + parcelasMensais: [], + }; + } + + private buildDebugYearGroups(detail: ParcelamentoDetail | null): { year: number; months: Array<{ label: string; value: string }> }[] { + if (!detail) return []; + const parcels = (detail.parcelasMensais ?? []).filter( + (p): p is ParcelamentoParcela => !!p && typeof p === 'object' + ); + const map = new Map>(); + parcels.forEach((p) => { + const parsed = this.parseCompetenciaParts(p.competencia); + if (!parsed) return; + if (!map.has(parsed.year)) map.set(parsed.year, new Map()); + map.get(parsed.year)!.set(parsed.month, p); + }); + + return Array.from(map.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([year, monthMap]) => ({ + year, + months: Array.from({ length: 12 }, (_, idx) => { + const month = idx + 1; + const item = monthMap.get(month); + return { + label: `${month.toString().padStart(2, '0')}/${year}`, + value: this.formatMoney(item?.valor), + }; + }), + })); + } + +} diff --git a/src/app/pages/perfil/perfil.html b/src/app/pages/perfil/perfil.html new file mode 100644 index 0000000..7052be6 --- /dev/null +++ b/src/app/pages/perfil/perfil.html @@ -0,0 +1,158 @@ +
+ + + + + +
+
+
+
+
+ PERFIL +
+ +
+
MEU PERFIL
+ Atualize seus dados e credenciais de acesso +
+ +
+
+
+ +
+
+
+
+

Informação de perfil

+
+ +
+ {{ profileError }} +
+
+ {{ profileSuccess }} +
+ +
+
+
+ + + + Nome é obrigatório. + + + Nome deve ter pelo menos 2 caracteres. + +
+ +
+ + + + Email é obrigatório. + + + Email inválido. + +
+
+ +
+ +
+
+
+ +
+
+

Atualizar senha

+
+ +
+ {{ passwordError }} +
+
+ {{ passwordSuccess }} +
+ +
+
+
+ + + + Credencial atual é obrigatória. + +
+ +
+ + + + Nova credencial é obrigatória. + + + Nova credencial deve ter pelo menos 8 caracteres. + +
+ +
+ + + + Confirmação da nova credencial é obrigatória. + + + A nova credencial e a confirmação precisam ser iguais. + +
+
+ +
+ +
+
+
+
+
+
+
+
diff --git a/src/app/pages/perfil/perfil.scss b/src/app/pages/perfil/perfil.scss new file mode 100644 index 0000000..5cbcc41 --- /dev/null +++ b/src/app/pages/perfil/perfil.scss @@ -0,0 +1,295 @@ +:host { + --brand: #E33DCF; + --blue: #030FAA; + --text: #111214; + --muted: rgba(17, 18, 20, 0.65); + --success-bg: rgba(25, 135, 84, 0.1); + --success-text: #198754; + --danger-bg: rgba(220, 53, 69, 0.1); + --danger-text: #dc3545; + --radius-xl: 22px; + --shadow-card: 0 22px 46px rgba(17, 18, 20, 0.1); + --glass-bg: rgba(255, 255, 255, 0.82); + --glass-border: 1px solid rgba(227, 61, 207, 0.16); + + display: block; + font-family: 'Inter', sans-serif; + color: var(--text); + box-sizing: border-box; +} + +.perfil-page { + min-height: 100vh; + padding: 0 12px; + display: flex; + justify-content: center; + position: relative; + overflow-y: auto; + background: + radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%), + radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%); +} + +.page-blob { + position: fixed; + pointer-events: none; + border-radius: 999px; + filter: blur(34px); + opacity: 0.55; + z-index: 0; + background: radial-gradient(circle at 30% 30%, rgba(227, 61, 207, 0.55), rgba(227, 61, 207, 0.06)); + animation: floaty 10s ease-in-out infinite; + + &.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; } + &.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; } + &.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; } + &.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: 0.45; } +} + +@keyframes floaty { + 0% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(18px, 10px) scale(1.03); } + 100% { transform: translate(0, 0) scale(1); } +} + +.container-geral-responsive { + width: 100%; + max-width: 1180px; + position: relative; + z-index: 1; + margin-top: 40px; + margin-bottom: 120px; +} + +.geral-card { + border-radius: var(--radius-xl); + overflow: hidden; + background: var(--glass-bg); + border: var(--glass-border); + backdrop-filter: blur(12px); + box-shadow: var(--shadow-card); + display: flex; + flex-direction: column; + min-height: 80vh; +} + +.geral-header { + padding: 16px 24px; + border-bottom: 1px solid rgba(17, 18, 20, 0.06); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.06), rgba(255, 255, 255, 0.2)); +} + +.header-row-top { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 12px; +} + +.title-badge { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(227, 61, 207, 0.22); + backdrop-filter: blur(10px); + color: var(--text); + font-size: 13px; + font-weight: 800; + + i { color: var(--brand); } +} + +.header-title { + justify-self: center; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.title { + font-size: 26px; + font-weight: 950; + letter-spacing: -0.3px; + color: var(--text); + margin-top: 10px; +} + +.subtitle { + color: var(--muted); + font-weight: 700; +} + +.header-actions { + justify-self: end; + min-height: 1px; +} + +.geral-body { + padding: 18px; +} + +.perfil-sections { + display: grid; + gap: 14px; +} + +.perfil-section-card { + background: #ffffff; + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 16px; + box-shadow: 0 4px 12px rgba(17, 18, 20, 0.06); + padding: 18px 20px; +} + +.section-header { + margin-bottom: 12px; + + h2 { + margin: 0; + font-size: 1rem; + font-weight: 900; + color: var(--text); + letter-spacing: 0.02em; + text-transform: uppercase; + } +} + +.form-alert { + border-radius: 10px; + padding: 10px 12px; + font-size: 13px; + font-weight: 700; + margin-bottom: 10px; + + &.error { + background: var(--danger-bg); + border: 1px solid rgba(220, 53, 69, 0.2); + color: var(--danger-text); + } + + &.success { + background: var(--success-bg); + border: 1px solid rgba(25, 135, 84, 0.2); + color: var(--success-text); + } +} + +.profile-form { + display: grid; + gap: 12px; +} + +.form-grid { + display: grid; + gap: 12px; +} + +.form-field { + display: grid; + gap: 6px; + + label { + font-size: 0.78rem; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); + } +} + +.form-control { + height: 40px; + border-radius: 10px; + border: 1px solid rgba(17, 18, 20, 0.16); + background: rgba(255, 255, 255, 0.86); + color: var(--text); + font-size: 14px; + padding: 0 12px; + transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; + + &:focus { + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); + background: #fff; + } +} + +.field-error { + color: var(--danger-text); + font-size: 12px; + font-weight: 700; + line-height: 1.2; +} + +.section-actions { + display: flex; + justify-content: flex-end; +} + +.btn-brand { + background-color: var(--brand); + border-color: var(--brand); + color: #fff; + font-weight: 900; + border-radius: 12px; + transition: transform 0.2s, box-shadow 0.2s; + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); + filter: brightness(1.05); + } + + &:disabled { + opacity: 0.72; + cursor: not-allowed; + transform: none; + } +} + +@media (max-width: 768px) { + .container-geral-responsive { + margin-top: 16px; + margin-bottom: 64px; + } + + .header-row-top { + grid-template-columns: 1fr; + text-align: center; + gap: 10px; + } + + .title-badge { + justify-self: center; + } + + .header-actions { + display: none; + } + + .geral-header { + padding: 14px 16px; + } + + .geral-body { + padding: 12px; + } + + .perfil-section-card { + padding: 14px; + } + + .section-actions { + justify-content: stretch; + } + + .section-actions .btn { + width: 100%; + } +} diff --git a/src/app/pages/perfil/perfil.ts b/src/app/pages/perfil/perfil.ts new file mode 100644 index 0000000..7907856 --- /dev/null +++ b/src/app/pages/perfil/perfil.ts @@ -0,0 +1,229 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { + AbstractControl, + FormBuilder, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + Validators +} from '@angular/forms'; + +import { ProfileService } from '../../services/profile.service'; +import { AuthService } from '../../services/auth.service'; + +@Component({ + selector: 'app-perfil', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './perfil.html', + styleUrls: ['./perfil.scss'], +}) +export class Perfil implements OnInit { + profileForm: FormGroup; + passwordForm: FormGroup; + + loadingProfile = false; + savingProfile = false; + savingPassword = false; + + profileSuccess = ''; + profileError = ''; + passwordSuccess = ''; + passwordError = ''; + + constructor( + private fb: FormBuilder, + private profileService: ProfileService, + private authService: AuthService + ) { + this.profileForm = this.fb.group({ + nome: ['', [Validators.required, Validators.minLength(2)]], + email: ['', [Validators.required, Validators.email]], + }); + + this.passwordForm = this.fb.group( + { + credencialAtual: ['', [Validators.required]], + novaCredencial: ['', [Validators.required, Validators.minLength(8)]], + confirmarNovaCredencial: ['', [Validators.required]], + }, + { validators: this.passwordsMatchValidator } + ); + } + + ngOnInit(): void { + this.loadProfile(); + } + + onSaveProfile(): void { + if (this.savingProfile) return; + this.profileSuccess = ''; + this.profileError = ''; + + if (this.profileForm.invalid) { + this.profileForm.markAllAsTouched(); + return; + } + + this.savingProfile = true; + this.setProfileFormDisabled(true); + const payload = { + nome: String(this.profileForm.get('nome')?.value ?? '').trim(), + email: String(this.profileForm.get('email')?.value ?? '').trim(), + }; + + this.profileService.updateProfile(payload).subscribe({ + next: (updated) => { + this.savingProfile = false; + this.setProfileFormDisabled(false); + this.profileSuccess = 'Perfil atualizado com sucesso.'; + this.profileForm.patchValue({ + nome: updated.nome ?? '', + email: updated.email ?? '', + }); + this.profileForm.markAsPristine(); + this.authService.updateUserProfile({ + nome: updated.nome ?? '', + email: updated.email ?? '', + }); + }, + error: (err: HttpErrorResponse) => { + this.savingProfile = false; + this.setProfileFormDisabled(false); + this.profileError = this.extractErrorMessage(err, 'Não foi possível atualizar o perfil.'); + }, + }); + } + + onChangePassword(): void { + if (this.savingPassword) return; + this.passwordSuccess = ''; + this.passwordError = ''; + + if (this.passwordForm.invalid) { + this.passwordForm.markAllAsTouched(); + return; + } + + this.savingPassword = true; + this.setPasswordFormDisabled(true); + const payload = { + credencialAtual: String(this.passwordForm.get('credencialAtual')?.value ?? ''), + novaCredencial: String(this.passwordForm.get('novaCredencial')?.value ?? ''), + confirmarNovaCredencial: String(this.passwordForm.get('confirmarNovaCredencial')?.value ?? ''), + }; + + this.profileService.changePassword(payload).subscribe({ + next: () => { + this.savingPassword = false; + this.setPasswordFormDisabled(false); + this.passwordSuccess = 'Credencial atualizada com sucesso.'; + this.passwordForm.reset(); + }, + error: (err: HttpErrorResponse) => { + this.savingPassword = false; + this.setPasswordFormDisabled(false); + this.passwordError = this.extractErrorMessage(err, 'Não foi possível atualizar a credencial.'); + }, + }); + } + + hasProfileFieldError(field: string, error?: string): boolean { + const control = this.profileForm.get(field); + if (!control) return false; + if (error) return !!(control.touched && control.hasError(error)); + return !!(control.touched && control.invalid); + } + + hasPasswordFieldError(field: string, error?: string): boolean { + const control = this.passwordForm.get(field); + if (!control) return false; + if (error) return !!(control.touched && control.hasError(error)); + return !!(control.touched && control.invalid); + } + + get passwordMismatch(): boolean { + const confirmTouched = this.passwordForm.get('confirmarNovaCredencial')?.touched; + return !!(confirmTouched && this.passwordForm.errors?.['passwordMismatch']); + } + + private loadProfile(): void { + this.loadingProfile = true; + this.profileSuccess = ''; + this.profileError = ''; + this.setProfileFormDisabled(true); + + this.profileService.getMe().subscribe({ + next: (me) => { + this.loadingProfile = false; + this.profileForm.patchValue({ + nome: me.nome ?? '', + email: me.email ?? '', + }); + this.setProfileFormDisabled(false); + }, + error: (err: HttpErrorResponse) => { + this.loadingProfile = false; + this.setProfileFormDisabled(false); + if (err.status === 404) { + const authProfile = this.authService.currentUserProfile; + if (authProfile) { + this.profileForm.patchValue({ + nome: authProfile.nome ?? '', + email: authProfile.email ?? '', + }); + } + } + this.profileError = this.extractErrorMessage(err, 'Não foi possível carregar os dados do perfil.'); + }, + }); + } + + private setProfileFormDisabled(disabled: boolean): void { + if (disabled) { + this.profileForm.disable({ emitEvent: false }); + return; + } + this.profileForm.enable({ emitEvent: false }); + } + + private setPasswordFormDisabled(disabled: boolean): void { + if (disabled) { + this.passwordForm.disable({ emitEvent: false }); + return; + } + this.passwordForm.enable({ emitEvent: false }); + } + + private extractErrorMessage(err: HttpErrorResponse, fallback: string): string { + if (err.status === 404) { + return 'API de perfil não encontrada (404). Reinicie/atualize o back-end com os novos endpoints de perfil.'; + } + + const apiError = err?.error; + + if (Array.isArray(apiError?.errors) && apiError.errors.length) { + const msg = apiError.errors[0]?.message; + if (msg) return String(msg); + } + + if (typeof apiError?.message === 'string' && apiError.message.trim()) { + return apiError.message.trim(); + } + + if (typeof apiError === 'string' && apiError.trim()) { + return apiError.trim(); + } + + return fallback; + } + + private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null { + const nova = group.get('novaCredencial')?.value; + const confirmar = group.get('confirmarNovaCredencial')?.value; + if (!nova || !confirmar) return null; + return nova === confirmar ? null : { passwordMismatch: true }; + } +} diff --git a/src/app/pages/register/register.ts b/src/app/pages/register/register.ts index ecd2601..7dfbf25 100644 --- a/src/app/pages/register/register.ts +++ b/src/app/pages/register/register.ts @@ -78,7 +78,7 @@ export class Register { this.isSubmitting = false; // Se você não quer manter "logado" após cadastrar: - localStorage.removeItem('token'); + this.authService.logout(); await this.showToast('Cadastro realizado com sucesso! Agora faça login para continuar.'); diff --git a/src/app/pages/resumo/resumo.html b/src/app/pages/resumo/resumo.html new file mode 100644 index 0000000..7464d04 --- /dev/null +++ b/src/app/pages/resumo/resumo.html @@ -0,0 +1,615 @@ +
+
+
+ +
+
+ Dashboard +
+

Resumo Gerencial

+
+
+ Atualizando dados... +
+
+ {{ errorMessage }} +
+
+ Atualizado +
+
+
+

Visão consolidada de performance, contratos e indicadores financeiros.

+
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+

Planos & Contratos

+

Performance financeira agrupada por modalidade de plano.

+
+
+
+ Total Linhas + {{ formatNumber(planosTotals?.totalLinhasTotal) }} +
+
+ Valor Total + {{ formatMoney(planosTotals?.valorTotal) }} +
+
+ Contratos + {{ formatMoney(contratosTotals?.valorTotal) }} +
+
+
+
+ +
+
+
+

Top Planos (Valor)

+

Os planos com maior representatividade financeira.

+
+
+ +
+
+
+
+

Top Planos (Volume)

+

Quantidade de linhas ativas por tipo de plano.

+
+
+ +
+
+
+ +
+ +
+

Macrophony - Planos

+ Detalhamento granular dos planos e suas variações. +
+
+
+
+
+ + +
+ +
+ + +
+
+ +
+
+ +

Nenhum dado encontrado.

+
+ +
+
+
+ +
+ +
+
{{ group.plano }}
+
+ GB {{ group.gbLabel }} + {{ formatNumber(group.totalLinhas) }} linhas +
+
+ +
+
+ Valor Total + {{ formatMoney(group.valorTotal) }} +
+
+ Média Un. + {{ formatMoney(group.valorUnitMedio) }} +
+
+ +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
Plano / VariaçãoFranquiaValor Un.LinhasTotal
+
+ {{ row.planoContrato || '-' }} + +
+
{{ formatGb(row.gb) }}{{ formatMoney(row.valorIndividualComSvas) }}{{ formatNumber(row.totalLinhas) }}{{ formatMoney(row.valorTotal) }}
+
+
+
+
+ + + +
+
+ Total de Linhas + {{ formatNumber(planosTotals.totalLinhasTotal) }} +
+
+ Valor Total Global + {{ formatMoney(planosTotals.valorTotal) }} +
+
+
+
+ +
+ +
+

Resumo de Contratos

+ Visão consolidada por tipo de contrato vigente. +
+
+
+ +
+
+ +
+
+
+
+

Clientes & Performance

+

Analise a rentabilidade e custos por cliente.

+
+
+
+ Total Linhas + {{ formatNumber(clientesTotals?.qtdLinhasTotal) }} +
+
+ Receita Line + {{ formatMoney(clientesTotals?.valorContratoLine) }} +
+
+ Lucro Total + {{ formatMoney(clientesTotals?.lucro) }} +
+
+
+
+ +
+
+
+

Top Clientes (Lucratividade)

+

Clientes ordenados pelo maior retorno financeiro.

+
+
+ +
+
+
+ +
+ +
+

Detalhamento Vivo x Line Móvel

+ Comparativo de custos, receitas e margem por cliente. +
+
+
+ +
+ +
+ +
+
+
+
+

Totais Line

+

Consolidado entre Pessoa Física (PF) e Jurídica (PJ).

+
+
+
+ PF Linhas + {{ formatNumber(findLineTotal(['PF','PESSOA FISICA'])?.qtdLinhas) }} +
+
+ PJ Linhas + {{ formatNumber(findLineTotal(['PJ','PESSOA JURIDICA'])?.qtdLinhas) }} +
+
+ Lucro Consolidado + {{ formatMoney(clientesTotals?.lucro) }} +
+
+
+
+ +
+
+
+

Distribuição PF vs PJ

+

Proporção da base de linhas ativas.

+
+
+ +
+
+
+ +
+ +
+

Detalhamento Totais

+ Tabela analítica dos totais processados. +
+
+
+ +
+ +
+ +
+

Distribuição por GB

+ Tabela GB / QTD / SOMA importada da aba RESUMO. +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
GBQTDSOMA
{{ formatGb(row.gb) }}{{ formatNumber(row.qtd) }}{{ formatMoney(row.soma) }}
Total{{ formatNumber(gbDistribuicaoTotalLinhas) }}{{ formatMoney(gbDistribuicaoSomaTotal) }}
+
+
+
+
+ +
+
+
+
+

Estoque de Reserva

+

Monitoramento de linhas disponíveis por DDD.

+
+
+
+ Linhas em Estoque + {{ formatNumber(reservaTotals?.qtdLinhasTotal) }} +
+
+ Custo de Reserva + {{ formatNumber(reservaTotals?.total) }} +
+
+
+
+ +
+
+
+

Concentração por DDD

+

Regiões com maior volume de linhas em reserva.

+
+
+ +
+
+
+ +
+ +
+

Detalhamento por DDD

+ Lista completa de estoque agrupada geograficamente. +
+
+
+ +
+
+
+
+ + +
+
+ + +
+ + +
+
+ +
+
+

Nenhum registro encontrado.

+
+
+
+
+ +
+
+
{{ item.title }}
+
{{ item.subtitle }}
+
+
+
+ {{ metric.label }} + {{ metric.value }} +
+
+
+ +
+
+ +
+
+ + + + + + + + + + + +
+ {{ col.label }} +
+ + {{ formatCell(col, row) }} + + {{ formatCell(col, row) }} + +
+
+
+
+
+ +
+
+ Receita Line + {{ formatMoney(clientesTotals.valorContratoLine) }} +
+
+ Custo Vivo + {{ formatMoney(clientesTotals.valorContratoVivo) }} +
+
+ Lucro Líquido + {{ formatMoney(clientesTotals.lucro) }} +
+
+ + +
+ +
+
+
+
+

{{ group.detailGroup?.title }}

+ +
+
+
+ + + + + + + + + + + +
+ {{ col.label }} +
+ {{ formatCell(col, row) }} +
+
+
+
+
+
+ +
+
+
+
+
+ Detalhes do Plano +

{{ macrophonyDetailGroup?.plano }}

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
VariaçãoGBValor Un.Total LinhasValor Total
{{ row.planoContrato || '-' }}{{ formatGb(row.gb) }}{{ formatMoney(row.valorIndividualComSvas) }}{{ formatNumber(row.totalLinhas) }}{{ formatMoney(row.valorTotal) }}
Total deste grupo{{ formatNumber(macrophonyDetailGroup.totalLinhas) }}{{ formatMoney(macrophonyDetailGroup.valorTotal) }}
+
+
+
+
+
diff --git a/src/app/pages/resumo/resumo.scss b/src/app/pages/resumo/resumo.scss new file mode 100644 index 0000000..f740f85 --- /dev/null +++ b/src/app/pages/resumo/resumo.scss @@ -0,0 +1,790 @@ +:host { + /* Tokens de Cor - Enterprise Palette */ + --brand: #e33dcf; + --brand-hover: #c92bb6; + --brand-soft: rgba(227, 61, 207, 0.08); + --brand-gradient: linear-gradient(135deg, #e33dcf 0%, #b0249d 100%); + + --blue: #030faa; + --blue-soft: rgba(3, 15, 170, 0.06); + + --text-main: #0f172a; + --text-sec: #64748b; + --text-light: #94a3b8; + + --border: #e2e8f0; + --surface: #ffffff; + --bg-page: #f8fafc; + + --success: #10b981; + --danger: #ef4444; + --warning: #f59e0b; + + /* Elevation & Depth */ + --shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02); + --shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); + --shadow-elevated: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04); + --shadow-modal: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + + --focus-ring: 0 0 0 3px rgba(227, 61, 207, 0.2); + + display: block; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color: var(--text-main); + background: var(--bg-page); +} + +.resumo-page { + min-height: 100vh; + padding-bottom: 80px; +} + +.wrap { padding-top: 32px; } + +.resumo-container { + width: 100%; + max-width: 1360px; /* Ligeiramente mais largo para dashboard */ + margin: 0 auto; + padding: 0 24px; + + @media (max-width: 768px) { padding: 0 16px; } +} + +/* Animações */ +[data-animate] { + opacity: 1; + transform: translateY(0); + transition: opacity 0.5s ease-out, transform 0.5s ease-out; +} +:host(.animate-ready) [data-animate] { + opacity: 0; + transform: translateY(15px); +} +:host(.animate-ready) [data-animate].is-visible { + opacity: 1; + transform: translateY(0); +} + +/* Header */ +.page-head { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 24px; + gap: 16px; + + @media (max-width: 768px) { + flex-direction: column; + align-items: flex-start; + } +} + +.title-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.badge-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 99px; + background: white; + border: 1px solid var(--border); + color: var(--brand); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + box-shadow: var(--shadow-sm); + width: fit-content; +} + +.flex-title { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.page-title { + margin: 0; + font-size: 28px; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--text-main); +} + +.page-subtitle { + margin: 0; + font-size: 14px; + color: var(--text-sec); + max-width: 600px; +} + +/* Status Indicators */ +.status-wrapper { + display: flex; + gap: 12px; +} +.status { + font-size: 12px; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 99px; + + &.loading { background: #eff6ff; color: var(--blue); } + &.error { background: #fef2f2; color: var(--danger); } + &.success { background: #f0fdf4; color: var(--success); } +} +.spin { animation: spin 0.8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +/* Botões */ +.btn-ghost { + border: 1px solid var(--border); + background: white; + color: var(--text-main); + padding: 8px 16px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: var(--shadow-sm); + + &:hover:not(:disabled) { + border-color: var(--brand); + color: var(--brand); + transform: translateY(-1px); + box-shadow: var(--shadow-card); + } + &:disabled { opacity: 0.6; cursor: wait; } +} + +.btn-icon-text { + @extend .btn-ghost; + padding: 6px 12px; + font-size: 12px; +} + +.btn-icon { + width: 32px; + height: 32px; + border-radius: 8px; + border: 1px solid transparent; + background: rgba(0,0,0,0.03); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: rgba(0,0,0,0.06); + color: var(--brand); + } +} + +.btn-mini { + padding: 4px 10px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + border: 1px solid var(--border); + background: white; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + color: var(--text-sec); + + &:hover { + border-color: var(--brand); + color: var(--brand); + } +} + +/* Tabs */ +.tab-bar { + display: flex; + gap: 6px; + padding: 4px; + border-radius: var(--radius-md); + background: white; + border: 1px solid var(--border); + margin-bottom: 24px; + width: fit-content; + box-shadow: var(--shadow-sm); + overflow-x: auto; +} + +.tab-btn { + border: none; + background: transparent; + padding: 8px 16px; + border-radius: 8px; + font-weight: 600; + font-size: 13px; + color: var(--text-sec); + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + + &:hover { color: var(--text-main); background: #f1f5f9; } + &.active { + background: var(--brand-soft); + color: var(--brand); + font-weight: 700; + } +} + +/* Hero Section */ +.section-hero { + background: white; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 24px; + margin-bottom: 24px; + box-shadow: var(--shadow-card); +} + +.hero-content { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + + @media (max-width: 900px) { + flex-direction: column; + align-items: flex-start; + } +} + +.hero-text h3 { + margin: 0 0 4px; + font-size: 18px; + font-weight: 700; +} +.hero-text p { + margin: 0; + color: var(--text-sec); + font-size: 13px; +} + +.hero-kpis { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 16px; + width: 100%; + max-width: 600px; +} + +.kpi-card { + background: #f8fafc; + border: 1px solid var(--border); + padding: 12px 16px; + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 4px; + transition: transform 0.2s; + + &:hover { + border-color: #cbd5e1; + background: white; + box-shadow: var(--shadow-sm); + } + + &.highlight { + background: linear-gradient(to bottom right, #f8fafc, #fff); + border-left: 3px solid var(--success); + } +} + +.kpi-lbl { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + color: var(--text-sec); + letter-spacing: 0.04em; +} + +.kpi-val { + font-size: 18px; + font-weight: 700; + color: var(--text-main); + font-feature-settings: "tnum"; +} + +/* Grids */ +.section-grid { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.planos-charts .chart-card { + grid-column: span 6; + @media (max-width: 960px) { grid-column: span 12; } +} + +.full-chart .chart-card { + grid-column: span 12; +} + +.chart-card { + background: white; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 20px; + box-shadow: var(--shadow-card); + display: flex; + flex-direction: column; + height: 100%; +} + +.chart-area { + position: relative; + height: 300px; + width: 100%; + margin-top: 16px; +} + +.card-header-clean h3 { + margin: 0; + font-size: 15px; + font-weight: 700; +} +.card-header-clean p { + margin: 2px 0 0; + font-size: 12px; + color: var(--text-sec); +} + +/* Details/Summary Sections */ +.section-card { + background: white; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow-card); + margin-bottom: 24px; + transition: box-shadow 0.2s; + + &:hover { box-shadow: var(--shadow-elevated); } +} + +summary { + list-style: none; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 24px; + background: white; + border-bottom: 1px solid transparent; + transition: background 0.2s; + + &:hover { background: #fcfcfc; } + &::-webkit-details-marker { display: none; } +} + +details[open] summary { + border-bottom-color: var(--border); + background: #f8fafc; +} + +.summary-content h4 { + margin: 0 0 2px; + font-size: 15px; + font-weight: 700; + color: var(--text-main); +} +.summary-content span { + font-size: 13px; + color: var(--text-sec); +} +.summary-icon { + color: var(--text-sec); + transition: transform 0.3s ease; +} +details[open] .summary-icon { transform: rotate(180deg); } + +/* Tabelas e Listas */ +.table-tools { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 24px; + border-bottom: 1px solid var(--border); + gap: 16px; + flex-wrap: wrap; + background: white; +} + +.search-box { + display: flex; + align-items: center; + gap: 10px; + background: #f1f5f9; + padding: 8px 12px; + border-radius: 8px; + width: 300px; + max-width: 100%; + border: 1px solid transparent; + transition: all 0.2s; + + &:focus-within { + background: white; + border-color: var(--brand); + box-shadow: 0 0 0 2px var(--brand-soft); + } + + input { + border: none; + background: transparent; + width: 100%; + outline: none; + font-size: 13px; + } + i { color: var(--text-sec); } +} + +.tools-right { + display: flex; + align-items: center; + gap: 12px; +} + +.select-label { + font-size: 12px; + font-weight: 600; + color: var(--text-sec); + display: flex; + align-items: center; + gap: 8px; + + select { + border: 1px solid var(--border); + border-radius: 6px; + padding: 4px 24px 4px 8px; + font-size: 12px; + background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 6px center / 10px; + appearance: none; + cursor: pointer; + + &:hover { border-color: var(--text-light); } + &:focus { outline: none; border-color: var(--brand); } + } +} + +.divider-v { + width: 1px; + height: 24px; + background: var(--border); +} + +/* Listas Agrupadas */ +.macrophony-row, .grouped-row { + display: grid; + grid-template-columns: 40px minmax(200px, 1.5fr) 2fr auto; + gap: 16px; + padding: 16px 24px; + align-items: center; + border-bottom: 1px solid var(--border); + background: white; + transition: background 0.1s; + cursor: pointer; + + &:hover { background: #f8fafc; } + + @media (max-width: 768px) { + grid-template-columns: 40px 1fr; + gap: 8px; + } +} + +.group-toggle { + width: 32px; + height: 32px; + border-radius: 6px; + border: 1px solid var(--border); + background: white; + color: var(--text-sec); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + &:hover { border-color: var(--brand); color: var(--brand); } +} + +.group-title { + font-weight: 700; + font-size: 14px; + color: var(--text-main); +} + +.group-meta { + display: flex; + gap: 6px; + margin-top: 4px; +} + +.group-metrics { + display: flex; + justify-content: flex-end; + gap: 24px; + + @media (max-width: 768px) { + grid-column: 2; + justify-content: flex-start; + margin-top: 8px; + } +} + +.metric { + display: flex; + flex-direction: column; + align-items: flex-end; + + @media (max-width: 768px) { align-items: flex-start; } +} + +.metric .lbl { + font-size: 10px; + text-transform: uppercase; + font-weight: 700; + color: var(--text-light); + letter-spacing: 0.05em; +} +.metric strong { + font-size: 14px; + color: var(--text-main); +} + +.badge-tag { + background: #f1f5f9; + color: var(--text-main); + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + border: 1px solid transparent; + + &.secondary { background: white; border-color: var(--border); color: var(--text-sec); } +} + +/* Tabelas Internas */ +.macrophony-details, .grouped-details { + background: #f8fafc; + border-bottom: 1px solid var(--border); + box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); + animation: slideDown 0.3s ease; +} + +@keyframes slideDown { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.table-wrap { + overflow-x: auto; + + &.is-nested { padding: 16px 24px; } +} + +.data-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 13px; + + &.compact td, &.compact th { padding: 8px 12px; } + + th { + text-align: center; + font-size: 11px; + text-transform: uppercase; + font-weight: 700; + color: var(--text-sec); + padding: 12px 16px; + border-bottom: 1px solid var(--border); + background: rgba(248, 250, 252, 0.95); + backdrop-filter: blur(8px); + position: sticky; + top: 0; + z-index: 10; + } + + td { + text-align: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + color: var(--text-main); + transition: background 0.1s; + } + + tr:last-child td { border-bottom: none; } + tr:hover td { background: white; } + + /* Auxiliares */ + .text-right { text-align: center; } + .text-center { text-align: center; } + .num-font { font-family: 'Roboto Mono', monospace; font-size: 12px; } + .font-bold { font-weight: 700; } + .text-brand { color: var(--brand); } + .text-success { color: var(--success); } + .text-danger { color: var(--danger); } +} + +.diff-row td { background: #fff5f9; } + +/* Footers */ +.table-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 24px; + background: white; + border-top: 1px solid var(--border); + + @media (max-width: 600px) { + flex-direction: column; + gap: 12px; + } +} + +.table-count { + font-size: 12px; + color: var(--text-sec); +} + +.pagination { + display: flex; + gap: 4px; +} + +.page-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + border: 1px solid var(--border); + background: white; + cursor: pointer; + font-size: 12px; + font-weight: 600; + transition: all 0.2s; + + &:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); } + &.active { background: var(--brand); color: white; border-color: var(--brand); } + &:disabled { opacity: 0.5; cursor: default; } +} + +/* Resumo Bars (Totais) */ +.macrophony-summary, .grouped-summary-bar { + display: flex; + gap: 24px; + padding: 16px 24px; + background: #f8fafc; + border-top: 1px solid var(--border); + flex-wrap: wrap; +} + +.summary-item, .sum-col { + display: flex; + flex-direction: column; + + span { font-size: 11px; text-transform: uppercase; color: var(--text-sec); font-weight: 700; } + strong { font-size: 16px; color: var(--text-main); } + + &.highlight strong { color: var(--brand); } +} + +/* Modais */ +.macrophony-modal, .grouped-modal { + position: fixed; + inset: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +.macrophony-backdrop, .grouped-backdrop { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(4px); + z-index: 90; +} + +.macrophony-card, .grouped-card { + background: white; + width: 100%; + max-width: 800px; + max-height: 85vh; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-modal); + display: flex; + flex-direction: column; + z-index: 101; + overflow: hidden; + animation: modalUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes modalUp { + from { opacity: 0; transform: translateY(20px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +.detail-head { + padding: 20px 24px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: flex-start; + background: white; + + h4 { margin: 0; font-size: 18px; font-weight: 700; } + .detail-super { display: block; font-size: 11px; text-transform: uppercase; color: var(--text-sec); font-weight: 700; margin-bottom: 4px; } +} + +.macrophony-modal-body, .grouped-modal-body { + overflow-y: auto; + padding: 0; +} + +/* Utils */ +.hide-mobile { + @media (max-width: 600px) { display: none; } +} + +.empty-state { + padding: 40px; + text-align: center; + color: var(--text-sec); + + i { font-size: 32px; margin-bottom: 12px; display: block; opacity: 0.5; } + p { margin: 0; font-size: 14px; } +} diff --git a/src/app/pages/resumo/resumo.ts b/src/app/pages/resumo/resumo.ts new file mode 100644 index 0000000..6901ad5 --- /dev/null +++ b/src/app/pages/resumo/resumo.ts @@ -0,0 +1,1120 @@ +import { + Component, + ChangeDetectorRef, + Inject, + PLATFORM_ID, + AfterViewInit, + OnInit, + ViewChild, + ElementRef, + OnDestroy, + HostBinding +} from '@angular/core'; +import { isPlatformBrowser, CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import Chart from 'chart.js/auto'; +import type { ChartConfiguration, ChartData, ScriptableContext, TooltipItem } from 'chart.js'; +import { + ResumoService, + ResumoResponse, + MacrophonyPlan, + MacrophonyTotals, + VivoLineResumo, + VivoLineTotals, + ClienteEspecial, + PlanoContratoResumo, + PlanoContratoTotal, + LineTotal, + GbDistribuicao, + ReservaLine, + ReservaPorDdd, + ReservaTotal +} from '../../services/resumo.service'; + +type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva'; + +// Configurações de UI +const ANIMATION_DELAY = 60; +const CHART_THEME = { + brand: '#e33dcf', + brandLight: '#fce7f9', + blue: '#030faa', + blueLight: '#eef2ff', + success: '#10b981', + fontFamily: "'Inter', sans-serif" +}; + +// ... (Interfaces e Types mantidos sem alteração na estrutura de dados) ... +// Para brevidade, mantive as definições de Tipos iguais ao original, focando nas mudanças visuais da classe. +type TableColumn = { + key: string; + label: string; + type?: 'text' | 'number' | 'money' | 'gb'; + align?: 'left' | 'right' | 'center'; + sortable?: boolean; + value: (row: T) => any; + display?: (value: any, row: T) => string; + tone?: boolean; + badge?: boolean; + icon?: (row: T) => string | null; + title?: (row: T) => string; +}; +// ... (Demais tipos TableView, TableState, GroupedTableState mantidos) ... +type TableView = { rows: T[]; total: number; totalPages: number; pageStart: number; pageEnd: number; pageNumbers: number[]; sorted: T[]; }; +type TableState = { key: string; label: string; data: T[]; columns: TableColumn[]; search: string; page: number; pageSize: number; pageSizeOptions: number[]; sortKey: string; sortDir: 'asc' | 'desc'; compact: boolean; view: TableView | null; }; +type MacrophonyGroup = { key: string; plano: string; gbLabel: string; totalLinhas: number; valorTotal: number; valorUnitMedio: number | null; rows: MacrophonyPlan[]; }; +type GroupMetric = { label: string; value: string; tone?: string }; +type GroupItem = { key: string; title: string; subtitle?: string; rows: T[]; metrics: GroupMetric[]; }; +type GroupedTableState = { key: string; label: string; table: TableState; groupBy: (row: T) => string; groupTitle: (rows: T[]) => string; groupSubtitle?: (rows: T[]) => string | undefined; groupMetrics: (rows: T[]) => GroupMetric[]; groupSort?: (a: GroupItem, b: GroupItem) => number; search: string; page: number; pageSize: number; pageSizeOptions: number[]; compact: boolean; open: Set; groups: GroupItem[]; filtered: GroupItem[]; view: GroupItem[]; detailOpen: boolean; detailGroup: GroupItem | null; }; + +@Component({ + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './resumo.html', + styleUrls: ['./resumo.scss'] +}) +export class Resumo implements OnInit, AfterViewInit, OnDestroy { + @HostBinding('class.animate-ready') animateReady = false; + + loading = false; + errorMessage = ''; + resumo: ResumoResponse | null = null; + + activeTab: ResumoTab = 'planos'; + readonly tabs = [ + { key: 'planos' as ResumoTab, label: 'Planos', icon: 'bi bi-collection' }, + { key: 'clientes' as ResumoTab, label: 'Clientes', icon: 'bi bi-people' }, + { key: 'totais' as ResumoTab, label: 'Totais Line', icon: 'bi bi-bar-chart' }, + { key: 'reserva' as ResumoTab, label: 'Reserva', icon: 'bi bi-box-seam' }, + ]; + + @ViewChild('chartPlanos') chartPlanosRef?: ElementRef; + @ViewChild('chartPlanosLinhas') chartPlanosLinhasRef?: ElementRef; + @ViewChild('chartClientes') chartClientesRef?: ElementRef; + @ViewChild('chartTotais') chartTotaisRef?: ElementRef; + @ViewChild('chartReserva') chartReservaRef?: ElementRef; + + private charts: { [key: string]: Chart | undefined } = {}; + private viewReady = false; + private dataReady = false; + + // Estados de Tabela e Grupo + macrophonyGroups: MacrophonyGroup[] = []; + macrophonyView: MacrophonyGroup[] = []; + macrophonySearch = ''; + macrophonyPage = 1; + macrophonyPageSize = 5; + macrophonyPageOptions = [5, 10, 20]; + macrophonyCompact = false; + macrophonyOpen: Set = new Set(); + macrophonyDetailOpen = false; + macrophonyDetailGroup: MacrophonyGroup | null = null; + private macrophonyFiltered: MacrophonyGroup[] = []; + + tableMacrophony!: TableState; + tablePlanoContrato!: TableState; + tableClientes!: TableState; + tableClientesEspeciais!: TableState; + tableTotaisLine!: TableState; + tableReserva!: TableState; + + groupPlanoContrato!: GroupedTableState; + groupClientes!: GroupedTableState; + groupClientesEspeciais!: GroupedTableState; + groupTotaisLine!: GroupedTableState; + groupReserva!: GroupedTableState; + + constructor( + @Inject(PLATFORM_ID) private platformId: object, + private resumoService: ResumoService, + private route: ActivatedRoute, + private router: Router, + private cdr: ChangeDetectorRef + ) { + this.initTables(); + this.initGroupTables(); + // Default chart configuration for Enterprise look + Chart.defaults.font.family = CHART_THEME.fontFamily; + Chart.defaults.color = '#64748b'; + Chart.defaults.scale.grid.color = '#f1f5f9'; + } + + ngOnInit(): void { + this.route.queryParamMap.subscribe((params) => { + const tab = params.get('tab'); + if (this.isValidTab(tab)) { + this.activeTab = tab as ResumoTab; + this.deferChartBuild(); + this.deferAnimate(); + } + }); + this.loadResumo(); + } + + ngAfterViewInit(): void { + this.viewReady = true; + this.animateReady = isPlatformBrowser(this.platformId); + if (this.animateReady) this.animateIn(); + this.tryBuildCharts(); + } + + ngOnDestroy(): void { + Object.values(this.charts).forEach(c => c?.destroy()); + } + + setTab(tab: ResumoTab): void { + if (this.activeTab === tab) return; + this.activeTab = tab; + this.router.navigate([], { queryParams: { tab }, queryParamsHandling: 'merge' }); + this.deferChartBuild(); + this.deferAnimate(); + } + + refresh(): void { + this.loadResumo(); + } + + // --- Helpers de UI --- + trackByIndex(index: number) { return index; } + + // Util para criar Gradiente no Chart.js + private createGradient(ctx: CanvasRenderingContext2D, colorStart: string, colorEnd: string) { + const gradient = ctx.createLinearGradient(0, 0, 0, 400); + gradient.addColorStop(0, colorStart); + gradient.addColorStop(1, colorEnd); + return gradient; + } + + // --- Lógica de Gráficos (Visual) --- + private buildChartsForActiveTab(): void { + if (!this.resumo) return; + this.destroyCharts(); + + if (this.activeTab === 'planos') { + this.buildChartPlanos(); + this.buildChartPlanosLinhas(); + } else if (this.activeTab === 'clientes') { + this.buildChartClientes(); + } else if (this.activeTab === 'totais') { + this.buildChartTotais(); + } else if (this.activeTab === 'reserva') { + this.buildChartReserva(); + } + } + + private buildChartPlanos() { + const canvas = this.chartPlanosRef?.nativeElement; + if (!canvas) return; + + const data = this.macrophonyGroups + .map(g => ({ label: g.plano, valor: g.valorTotal })) + .sort((a, b) => b.valor - a.valor) + .slice(0, 10); + + const ctx = canvas.getContext('2d'); + const bg = ctx ? this.createGradient(ctx, CHART_THEME.brand, '#b0249d') : CHART_THEME.brand; + + this.charts['planos'] = new Chart(canvas, { + type: 'bar', + data: { + labels: data.map(d => d.label.length > 20 ? d.label.slice(0, 20) + '...' : d.label), + datasets: [{ + label: 'Valor Total', + data: data.map(d => d.valor), + backgroundColor: bg, + borderRadius: 4, + barPercentage: 0.6, + minBarLength: 8, + }] + }, + options: this.getCommonChartOptions('currency') + }); + } + + private buildChartPlanosLinhas() { + const canvas = this.chartPlanosLinhasRef?.nativeElement; + if (!canvas) return; + + const data = this.macrophonyGroups + .map(g => ({ label: g.plano, linhas: g.totalLinhas })) + .sort((a, b) => b.linhas - a.linhas) + .slice(0, 10); + + const ctx = canvas.getContext('2d'); + const bg = ctx ? this.createGradient(ctx, CHART_THEME.blue, '#2563eb') : CHART_THEME.blue; + + this.charts['planosLinhas'] = new Chart(canvas, { + type: 'bar', + data: { + labels: data.map(d => d.label.length > 20 ? d.label.slice(0, 20) + '...' : d.label), + datasets: [{ + label: 'Total Linhas', + data: data.map(d => d.linhas), + backgroundColor: bg, + borderRadius: 4, + barPercentage: 0.6, + minBarLength: 8, + }] + }, + options: this.getCommonChartOptions('number') + }); + } + + private buildChartClientes() { + const canvas = this.chartClientesRef?.nativeElement; + if (!canvas) return; + + const data = (this.resumo?.vivoLineResumos ?? []) + .map(c => ({ label: c.cliente, lucro: this.toNumber(c.lucro) ?? 0 })) + .sort((a, b) => b.lucro - a.lucro) + .slice(0, 10); + + this.charts['clientes'] = new Chart(canvas, { + type: 'bar', + data: { + labels: data.map(d => (d.label?.length ?? 0) > 25 ? (d.label ?? '').slice(0, 25) + '...' : d.label), + datasets: [{ + label: 'Lucro Estimado', + data: data.map(d => d.lucro), + backgroundColor: CHART_THEME.success, + borderRadius: 4, + barPercentage: 0.7, + }] + }, + options: { + ...this.getCommonChartOptions('currency'), + indexAxis: 'y' + } + }); + } + + private buildChartTotais() { + const canvas = this.chartTotaisRef?.nativeElement; + if (!canvas) return; + + const pf = this.toNumber(this.findLineTotal(['PF', 'PESSOA FISICA'])?.qtdLinhas) ?? 0; + const pj = this.toNumber(this.findLineTotal(['PJ', 'PESSOA JURIDICA'])?.qtdLinhas) ?? 0; + + this.charts['totais'] = new Chart(canvas, { + type: 'doughnut', + data: { + labels: ['Pessoa Física', 'Pessoa Jurídica'], + datasets: [{ + data: [pf, pj], + backgroundColor: [CHART_THEME.brand, CHART_THEME.blue], + borderWidth: 0, + hoverOffset: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + cutout: '75%', + plugins: { + legend: { position: 'bottom', labels: { usePointStyle: true, padding: 20 } }, + tooltip: { + backgroundColor: '#1e293b', + padding: 12, + callbacks: { label: (c) => ` ${c.label}: ${this.formatNumber(c.raw)} linhas` } + } + } + } + }); + } + + private buildChartReserva() { + const canvas = this.chartReservaRef?.nativeElement; + if (!canvas) return; + + const data = this.getReservaPorDddChartData() + .sort((a, b) => b.totalLinhas - a.totalLinhas) + .slice(0, 15); + + this.charts['reserva'] = new Chart(canvas, { + type: 'bar', + data: { + labels: data.map(d => d.label), + datasets: [{ + label: 'Linhas em Estoque', + data: data.map(d => d.totalLinhas), + backgroundColor: CHART_THEME.blue, + borderRadius: 2, + minBarLength: 10 + }] + }, + options: { + ...this.getCommonChartOptions('number'), + plugins: { + ...this.getCommonChartOptions('number')?.plugins, + tooltip: { + ...this.getCommonChartOptions('number')?.plugins?.tooltip, + callbacks: { + label: (ctx: TooltipItem<'bar'>) => ` ${this.formatNumber(ctx.raw)} linhas` + } + } + } + } + }); + } + + private getCommonChartOptions(format: 'currency' | 'number'): ChartConfiguration['options'] { + return { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'nearest', intersect: true }, + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: '#1e293b', + titleFont: { size: 13 }, + bodyFont: { size: 13, weight: 'bold' }, + padding: 12, + cornerRadius: 8, + displayColors: false, + callbacks: { + label: (ctx: TooltipItem<'bar'>) => { + const val = ctx.raw as number; + return format === 'currency' ? this.formatMoney(val) : this.formatNumber(val); + } + } + } + }, + scales: { + y: { + beginAtZero: true, + grid: { color: '#f1f5f9', drawBorder: false } as any, + ticks: { font: { size: 11 }, maxTicksLimit: 6 } + }, + x: { + grid: { display: false } as any, + ticks: { font: { size: 11 } } + } + } + } as any; + } + + // --- Demais Métodos Funcionais (Mantidos para garantir contrato) --- + + private destroyCharts(): void { + Object.keys(this.charts).forEach(key => { + this.charts[key]?.destroy(); + this.charts[key] = undefined; + }); + } + + // (Mantendo lógica de carga de dados original para não quebrar regras de negócio) + private loadResumo(): void { + this.loading = true; + this.errorMessage = ''; + this.dataReady = false; + this.resumoService.getResumo().subscribe({ + next: (data) => { + this.resumo = data ? this.normalizeResumo(data) : null; + this.loading = false; + this.dataReady = true; + this.bindTables(); + this.cdr.detectChanges(); + this.tryBuildCharts(); + }, + error: () => { + this.loading = false; + this.errorMessage = 'Não foi possível carregar os dados.'; + this.cdr.detectChanges(); + } + }); + } + + // Animação de entrada + private animateIn(): void { + if (!isPlatformBrowser(this.platformId)) return; + setTimeout(() => { + document.querySelectorAll('[data-animate]').forEach((el, i) => { + setTimeout(() => el.classList.add('is-visible'), i * ANIMATION_DELAY); + }); + }, 100); + } + + // ... (Restante da lógica de Macrophony, Tabelas e Formatações mantidas exatamente como no original para segurança funcional) ... + // Apenas garantindo que métodos usados no HTML (formatMoney, toggleMacrophonyCompact, etc.) existam. + + // Exemplo de Formatação Enterprise + formatMoney(value: any): string { + const n = this.toNumber(value); + if (n === null) return '-'; + // Usando narrowSymbol para R$ sem espaço extra se desejado, mas padrão pt-BR é seguro + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(n); + } + + formatNumber(value: any): string { + const n = this.toNumber(value); + if (n === null) return '-'; + return new Intl.NumberFormat('pt-BR').format(n); + } + + formatGb(value: any): string { + if (value == null) return '-'; + const n = this.toNumber(value); + return n == null ? String(value) : `${new Intl.NumberFormat('pt-BR').format(n)} GB`; + } + + formatCell(column: TableColumn, row: T): string { + const value = column.value(row); + if (column.display) return column.display(value, row); + if (value === null || value === undefined || value === '') return '-'; + + switch (column.type) { + case 'money': + return this.formatMoney(value); + case 'number': + return this.formatNumber(value); + case 'gb': + return this.formatGb(value); + default: + return String(value); + } + } + + // (Omitindo repetição de código boilerplate de tabelas/paginação que não mudou a lógica, apenas a view consome diferente) + // ... [Inclua aqui todo o restante dos métodos: initTables, buildMacrophonyGroups, updateMacrophonyView, exportCsv, etc.] ... + + // Lógica crítica de retry do Chart + private tryBuildCharts(): void { + if (!isPlatformBrowser(this.platformId) || !this.viewReady || !this.dataReady) return; + requestAnimationFrame(() => this.buildChartsForActiveTab()); + } + + private deferChartBuild(): void { + if (!this.viewReady || !this.dataReady) return; + setTimeout(() => this.tryBuildCharts(), 50); + } + + private deferAnimate(): void { + if (!this.animateReady) return; + setTimeout(() => this.animateIn(), 50); + } + + private isValidTab(tab: string | null): tab is ResumoTab { + return ['planos', 'clientes', 'totais', 'reserva'].includes(tab || ''); + } + + // Métodos obrigatórios para o template funcionar + toggleMacrophonyCompact() { this.macrophonyCompact = !this.macrophonyCompact; } + onMacrophonySearch() { this.macrophonyPage = 1; this.updateMacrophonyView(); } + onMacrophonyPageSizeChange() { this.macrophonyPage = 1; this.updateMacrophonyView(); } + isMacrophonyOpen(key: string) { return this.macrophonyOpen.has(key); } + toggleMacrophonyGroup(key: string) { if (this.macrophonyOpen.has(key)) this.macrophonyOpen.delete(key); else this.macrophonyOpen.add(key); } + openMacrophonyDetail(g: MacrophonyGroup) { this.macrophonyDetailGroup = g; this.macrophonyDetailOpen = true; } + closeMacrophonyDetail() { this.macrophonyDetailOpen = false; this.macrophonyDetailGroup = null; } + goToMacrophonyPage(p: number) { this.macrophonyPage = p; this.updateMacrophonyView(); } + + onGroupedSearch(g: GroupedTableState) { g.page = 1; this.updateGroupView(g); } + toggleGroupedCompact(g: GroupedTableState) { g.compact = !g.compact; } + exportGroupedCsv(g: GroupedTableState, file: string) { this.exportCsv(g.table, file); } + isGroupedOpen(g: GroupedTableState, key: string) { return g.open.has(key); } + toggleGroupedOpen(g: GroupedTableState, key: string) { if (g.open.has(key)) g.open.delete(key); else g.open.add(key); } + openGroupedDetail(g: GroupedTableState, item: GroupItem) { g.detailGroup = item; g.detailOpen = true; } + closeGroupedDetail(g: GroupedTableState) { g.detailOpen = false; g.detailGroup = null; } + getGroupedPageStart(g: GroupedTableState) { return g.filtered.length ? ((g.page - 1) * g.pageSize + 1) : 0; } + getGroupedPageEnd(g: GroupedTableState) { return g.filtered.length ? Math.min(g.page * g.pageSize, g.filtered.length) : 0; } + getGroupedPageNumbers(g: GroupedTableState) { + const total = this.getGroupedTotalPages(g); + if (total <= 1) return [1]; + const current = Math.min(Math.max(g.page, 1), total); + const start = Math.max(1, current - 2); + const end = Math.min(total, start + 4); + const adjustedStart = Math.max(1, end - 4); + return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i); + } + getGroupedTotalPages(g: GroupedTableState) { return Math.max(1, Math.ceil(g.filtered.length / g.pageSize)); } + goToGroupedPage(g: GroupedTableState, p: number) { + g.page = Math.min(this.getGroupedTotalPages(g), Math.max(1, p)); + this.updateGroupView(g); + } + getTableRowClass(_: TableState, __: T) { return false; } + getToneClass(v: any) { + const n = this.toNumber(v); + if (n === null || n === 0) return ''; + return n > 0 ? 'text-success' : 'text-danger'; + } + isVivoTravel(v: any) { + if (v === true || v === 1) return true; + const normalized = String(v ?? '').trim().toLowerCase(); + return normalized === 'true' || normalized === '1' || normalized === 'sim'; + } + + private initTables() { + this.tableMacrophony = { + key: 'macrophony', + label: 'Macrophony', + data: [], + columns: [ + { key: 'planoContrato', label: 'Plano', type: 'text', value: (r) => r.planoContrato ?? '-' }, + { key: 'gb', label: 'Franquia', type: 'gb', value: (r) => r.gb }, + { key: 'valorIndividualComSvas', label: 'Valor Un.', type: 'money', align: 'right', value: (r) => r.valorIndividualComSvas }, + { key: 'totalLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.totalLinhas }, + { key: 'valorTotal', label: 'Valor Total', type: 'money', align: 'right', value: (r) => r.valorTotal }, + ], + search: '', + page: 1, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + sortKey: 'valorTotal', + sortDir: 'desc', + compact: false, + view: null, + }; + + this.tablePlanoContrato = { + key: 'planoContrato', + label: 'Plano Contrato', + data: [], + columns: [ + { key: 'planoContrato', label: 'Plano', type: 'text', value: (r) => r.planoContrato ?? '-' }, + { key: 'gb', label: 'Franquia', type: 'gb', value: (r) => r.gb }, + { key: 'valorIndividualComSvas', label: 'Valor Un.', type: 'money', align: 'right', value: (r) => r.valorIndividualComSvas }, + { key: 'totalLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.totalLinhas }, + { key: 'valorTotal', label: 'Valor Total', type: 'money', align: 'right', value: (r) => r.valorTotal }, + ], + search: '', + page: 1, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + sortKey: 'valorTotal', + sortDir: 'desc', + compact: false, + view: null, + }; + + this.tableClientes = { + key: 'clientes', + label: 'Clientes', + data: [], + columns: [ + { key: 'cliente', label: 'Cliente', type: 'text', value: (r) => r.cliente ?? '-' }, + { key: 'qtdLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.qtdLinhas }, + { key: 'franquiaTotal', label: 'Franquia Vivo', type: 'gb', align: 'right', value: (r) => r.franquiaTotal }, + { key: 'valorContratoVivo', label: 'Custo Vivo', type: 'money', align: 'right', value: (r) => r.valorContratoVivo }, + { key: 'franquiaLine', label: 'Franquia Line', type: 'gb', align: 'right', value: (r) => r.franquiaLine }, + { key: 'valorContratoLine', label: 'Receita Line', type: 'money', align: 'right', value: (r) => r.valorContratoLine }, + { key: 'lucro', label: 'Lucro', type: 'money', align: 'right', value: (r) => r.lucro, tone: true }, + ], + search: '', + page: 1, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + sortKey: 'lucro', + sortDir: 'desc', + compact: false, + view: null, + }; + + this.tableClientesEspeciais = { + key: 'clientesEspeciais', + label: 'Clientes Especiais', + data: [], + columns: [ + { key: 'nome', label: 'Nome', type: 'text', value: (r) => r.nome ?? '-' }, + { key: 'valor', label: 'Valor', type: 'money', align: 'right', value: (r) => r.valor, tone: true }, + ], + search: '', + page: 1, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + sortKey: 'valor', + sortDir: 'desc', + compact: false, + view: null, + }; + + this.tableTotaisLine = { + key: 'totaisLine', + label: 'Totais Line', + data: [], + columns: [ + { key: 'tipo', label: 'Tipo', type: 'text', value: (r) => r.tipo ?? '-' }, + { key: 'qtdLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.qtdLinhas }, + { key: 'valorTotalLine', label: 'Valor', type: 'money', align: 'right', value: (r) => r.valorTotalLine }, + { key: 'lucroTotalLine', label: 'Lucro', type: 'money', align: 'right', value: (r) => r.lucroTotalLine, tone: true }, + ], + search: '', + page: 1, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + sortKey: 'qtdLinhas', + sortDir: 'desc', + compact: false, + view: null, + }; + + this.tableReserva = { + key: 'reserva', + label: 'Reserva', + data: [], + columns: [ + { key: 'ddd', label: 'DDD', type: 'text', value: (r) => r.ddd ?? '-' }, + { key: 'franquiaGb', label: 'Franquia', type: 'gb', value: (r) => r.franquiaGb }, + { key: 'qtdLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.qtdLinhas }, + ], + search: '', + page: 1, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + sortKey: 'qtdLinhas', + sortDir: 'desc', + compact: false, + view: null, + }; + } + + private initGroupTables() { + this.groupPlanoContrato = this.createGroupedTableState( + 'grupoPlanoContrato', + 'Resumo de Contratos', + this.tablePlanoContrato, + (row) => (row.planoContrato ?? '-').toString(), + (rows) => (rows[0]?.planoContrato ?? '-').toString(), + (rows) => `Franquia ${this.formatGb(rows[0]?.gb)}`, + (rows) => [ + { label: 'Linhas', value: this.formatNumber(this.sumGroup(rows, (r) => r.totalLinhas)) }, + { label: 'Valor', value: this.formatMoney(this.sumGroup(rows, (r) => r.valorTotal)) }, + ], + (a, b) => this.sumGroup(b.rows, (r) => r.valorTotal) - this.sumGroup(a.rows, (r) => r.valorTotal) + ); + + this.groupClientes = this.createGroupedTableState( + 'grupoClientes', + 'Clientes', + this.tableClientes, + (row) => (row.cliente ?? '-').toString(), + (rows) => (rows[0]?.cliente ?? '-').toString(), + (rows) => `${this.formatNumber(this.sumGroup(rows, (r) => r.qtdLinhas))} linhas`, + (rows) => { + const receita = this.sumGroup(rows, (r) => r.valorContratoLine); + const custo = this.sumGroup(rows, (r) => r.valorContratoVivo); + const lucro = this.sumGroup(rows, (r) => r.lucro); + return [ + { label: 'Receita', value: this.formatMoney(receita) }, + { label: 'Custo', value: this.formatMoney(custo) }, + { label: 'Lucro', value: this.formatMoney(lucro), tone: this.getToneClass(lucro) }, + ]; + }, + (a, b) => this.sumGroup(b.rows, (r) => r.lucro) - this.sumGroup(a.rows, (r) => r.lucro) + ); + + this.groupClientesEspeciais = this.createGroupedTableState( + 'grupoClientesEspeciais', + 'Clientes Especiais', + this.tableClientesEspeciais, + (row) => (row.nome ?? '-').toString(), + (rows) => (rows[0]?.nome ?? '-').toString(), + undefined, + (rows) => { + const total = this.sumGroup(rows, (r) => r.valor); + return [{ label: 'Valor', value: this.formatMoney(total), tone: this.getToneClass(total) }]; + }, + (a, b) => this.sumGroup(b.rows, (r) => r.valor) - this.sumGroup(a.rows, (r) => r.valor) + ); + + this.groupTotaisLine = this.createGroupedTableState( + 'grupoTotaisLine', + 'Totais Line', + this.tableTotaisLine, + (row) => (row.tipo ?? '-').toString(), + (rows) => (rows[0]?.tipo ?? '-').toString(), + undefined, + (rows) => { + const linhas = this.sumGroup(rows, (r) => r.qtdLinhas); + const valor = this.sumGroup(rows, (r) => r.valorTotalLine); + const lucro = this.sumGroup(rows, (r) => r.lucroTotalLine); + return [ + { label: 'Linhas', value: this.formatNumber(linhas) }, + { label: 'Valor', value: this.formatMoney(valor) }, + { label: 'Lucro', value: this.formatMoney(lucro), tone: this.getToneClass(lucro) }, + ]; + }, + (a, b) => this.sumGroup(b.rows, (r) => r.qtdLinhas) - this.sumGroup(a.rows, (r) => r.qtdLinhas) + ); + + this.groupReserva = this.createGroupedTableState( + 'grupoReserva', + 'Reserva', + this.tableReserva, + (row) => String(row.ddd ?? '-'), + (rows) => `DDD ${rows[0]?.ddd ?? '-'}`, + undefined, + (rows) => { + const linhas = this.sumGroup(rows, (r) => r.qtdLinhas); + return [ + { label: 'Total de Linhas', value: this.formatNumber(linhas) }, + ]; + }, + (a, b) => this.sumGroup(b.rows, (r) => r.qtdLinhas) - this.sumGroup(a.rows, (r) => r.qtdLinhas) + ); + } + + private createGroupedTableState( + key: string, + label: string, + table: TableState, + groupBy: (row: T) => string, + groupTitle: (rows: T[]) => string, + groupSubtitle: ((rows: T[]) => string | undefined) | undefined, + groupMetrics: (rows: T[]) => GroupMetric[], + groupSort?: (a: GroupItem, b: GroupItem) => number, + ): GroupedTableState { + return { + key, + label, + table, + groupBy, + groupTitle, + groupSubtitle, + groupMetrics, + groupSort, + search: '', + page: 1, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + compact: false, + open: new Set(), + groups: [], + filtered: [], + view: [], + detailOpen: false, + detailGroup: null, + }; + } + + private normalizeResumo(data: ResumoResponse): ResumoResponse { + return { + ...data, + macrophonyPlans: Array.isArray(data.macrophonyPlans) ? data.macrophonyPlans : [], + vivoLineResumos: Array.isArray(data.vivoLineResumos) ? data.vivoLineResumos : [], + clienteEspeciais: Array.isArray(data.clienteEspeciais) ? data.clienteEspeciais : [], + planoContratoResumos: Array.isArray(data.planoContratoResumos) ? data.planoContratoResumos : [], + lineTotais: Array.isArray(data.lineTotais) ? data.lineTotais : [], + gbDistribuicao: Array.isArray(data.gbDistribuicao) ? data.gbDistribuicao : [], + reservaLines: Array.isArray(data.reservaLines) ? data.reservaLines : [], + reservaPorDdd: Array.isArray(data.reservaPorDdd) ? data.reservaPorDdd : [], + }; + } + + private bindTables() { + const resumo = this.resumo; + if (!resumo) { + this.tableMacrophony.data = []; + this.tablePlanoContrato.data = []; + this.tableClientes.data = []; + this.tableClientesEspeciais.data = []; + this.tableTotaisLine.data = []; + this.tableReserva.data = []; + this.macrophonyGroups = []; + this.macrophonyFiltered = []; + this.macrophonyView = []; + this.updateGroupView(this.groupPlanoContrato); + this.updateGroupView(this.groupClientes); + this.updateGroupView(this.groupClientesEspeciais); + this.updateGroupView(this.groupTotaisLine); + this.updateGroupView(this.groupReserva); + return; + } + + this.tableMacrophony.data = resumo.macrophonyPlans ?? []; + this.tablePlanoContrato.data = this.buildPlanoContratoResumoConsolidado(resumo.planoContratoResumos ?? []); + this.tableClientes.data = resumo.vivoLineResumos ?? []; + this.tableClientesEspeciais.data = resumo.clienteEspeciais ?? []; + this.tableTotaisLine.data = resumo.lineTotais ?? []; + this.tableReserva.data = resumo.reservaLines ?? []; + + this.updateMacrophonyView(); + this.updateGroupView(this.groupPlanoContrato); + this.updateGroupView(this.groupClientes); + this.updateGroupView(this.groupClientesEspeciais); + this.updateGroupView(this.groupTotaisLine); + this.updateGroupView(this.groupReserva); + } + + private updateMacrophonyView() { + const rows = this.tableMacrophony.data ?? []; + const byPlano = new Map(); + for (const row of rows) { + const key = (row.planoContrato ?? 'Sem Plano').toString(); + const list = byPlano.get(key) ?? []; + list.push(row); + byPlano.set(key, list); + } + + const groups: MacrophonyGroup[] = Array.from(byPlano.entries()).map(([key, items]) => { + const totalLinhas = this.sumGroup(items, (r) => r.totalLinhas); + const valorTotal = this.sumGroup(items, (r) => r.valorTotal); + const valorUnit = totalLinhas > 0 ? (valorTotal / totalLinhas) : null; + const gbLabel = (items.find((i) => (i.gb ?? '').toString().trim())?.gb ?? '-').toString(); + return { + key, + plano: key, + gbLabel, + totalLinhas, + valorTotal, + valorUnitMedio: valorUnit, + rows: [...items].sort((a, b) => (this.toNumber(b.valorTotal) ?? 0) - (this.toNumber(a.valorTotal) ?? 0)), + }; + }); + + groups.sort((a, b) => b.valorTotal - a.valorTotal); + this.macrophonyGroups = groups; + + const search = this.normalizeText(this.macrophonySearch); + this.macrophonyFiltered = !search + ? groups + : groups.filter((group) => + this.normalizeText(group.plano).includes(search) || + this.normalizeText(group.gbLabel).includes(search) + ); + + const totalPages = Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize)); + this.macrophonyPage = Math.min(totalPages, Math.max(1, this.macrophonyPage)); + const start = (this.macrophonyPage - 1) * this.macrophonyPageSize; + const end = start + this.macrophonyPageSize; + this.macrophonyView = this.macrophonyFiltered.slice(start, end); + + if (this.macrophonyDetailGroup && !this.macrophonyFiltered.some((g) => g.key === this.macrophonyDetailGroup?.key)) { + this.closeMacrophonyDetail(); + } + } + + private updateGroupView(group: GroupedTableState) { + const data = group.table.data ?? []; + const map = new Map(); + + data.forEach((row) => { + const key = group.groupBy(row) || 'Sem agrupamento'; + const rows = map.get(key) ?? []; + rows.push(row); + map.set(key, rows); + }); + + const groups = Array.from(map.entries()).map(([key, rows]) => ({ + key, + title: group.groupTitle(rows), + subtitle: group.groupSubtitle ? group.groupSubtitle(rows) : undefined, + rows, + metrics: group.groupMetrics(rows), + })) as GroupItem[]; + + if (group.groupSort) groups.sort(group.groupSort); + group.groups = groups; + + const search = this.normalizeText(group.search); + group.filtered = !search + ? groups + : groups.filter((g) => + this.normalizeText(g.title).includes(search) || + this.normalizeText(g.subtitle).includes(search) + ); + + const totalPages = Math.max(1, Math.ceil(group.filtered.length / group.pageSize)); + group.page = Math.min(totalPages, Math.max(1, group.page)); + const start = (group.page - 1) * group.pageSize; + const end = start + group.pageSize; + group.view = group.filtered.slice(start, end); + + if (group.detailGroup && !group.filtered.some((g) => g.key === group.detailGroup?.key)) { + group.detailGroup = null; + group.detailOpen = false; + } + } + + private normalizeText(value: any): string { + return (value ?? '') + .toString() + .trim() + .toUpperCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + } + + private sumGroup(rows: T[], getter: (row: T) => any): number { + return rows.reduce((acc, row) => acc + (this.toNumber(getter(row)) ?? 0), 0); + } + + private buildPlanoContratoResumoConsolidado(rows: PlanoContratoResumo[]): PlanoContratoResumo[] { + const grouped = new Map< + string, + { + planoContrato: string; + gb: number | null; + totalLinhas: number; + valorTotal: number; + valorIndividualFallback: number | null; + } + >(); + + rows.forEach((row) => { + const planoContrato = (row.planoContrato ?? '-').toString().trim() || '-'; + const key = this.normalizeText(planoContrato); + const gb = + this.extractGbFromPlanName(planoContrato) ?? + this.toNumber(row.gb ?? row.franquiaGb); + const totalLinhas = this.toNumber(row.totalLinhas) ?? 0; + const valorTotal = this.toNumber(row.valorTotal) ?? 0; + const valorIndividual = this.toNumber(row.valorIndividualComSvas); + + const current = grouped.get(key); + if (!current) { + grouped.set(key, { + planoContrato, + gb, + totalLinhas, + valorTotal, + valorIndividualFallback: valorIndividual, + }); + return; + } + + current.totalLinhas += totalLinhas; + current.valorTotal += valorTotal; + current.gb ??= gb; + current.valorIndividualFallback ??= valorIndividual; + }); + + return Array.from(grouped.values()) + .map((item) => { + const valorIndividualComSvas = + item.totalLinhas > 0 + ? item.valorTotal / item.totalLinhas + : item.valorIndividualFallback; + + return { + planoContrato: item.planoContrato, + gb: item.gb, + franquiaGb: item.gb, + valorIndividualComSvas, + totalLinhas: item.totalLinhas, + valorTotal: item.valorTotal, + } satisfies PlanoContratoResumo; + }) + .sort((a, b) => (this.toNumber(b.valorTotal) ?? 0) - (this.toNumber(a.valorTotal) ?? 0)); + } + + private extractGbFromPlanName(planoContrato: string | null | undefined): number | null { + if (!planoContrato) return null; + + const match = planoContrato + .toUpperCase() + .match(/(\d+(?:[.,]\d+)?)\s*(GB|MB)\b/); + + if (!match) return null; + + const value = Number(match[1].replace(',', '.')); + if (!Number.isFinite(value)) return null; + + return match[2] === 'MB' ? value / 1000 : value; + } + + private toNumber(value: any): number | null { + if (value === null || value === undefined || value === '') return null; + if (typeof value === 'number') return Number.isFinite(value) ? value : null; + const raw = String(value).trim(); + if (!raw) return null; + + let normalized = raw.replace(/[^\d,.-]/g, ''); + if (normalized.includes(',') && normalized.includes('.')) { + if (normalized.lastIndexOf(',') > normalized.lastIndexOf('.')) { + normalized = normalized.replace(/\./g, '').replace(',', '.'); + } else { + normalized = normalized.replace(/,/g, ''); + } + } else if (normalized.includes(',')) { + normalized = normalized.replace(/\./g, '').replace(',', '.'); + } + + const parsed = Number(normalized); + return Number.isNaN(parsed) ? null : parsed; + } + + private exportCsv(table: TableState, filename: string) { + if (!isPlatformBrowser(this.platformId)) return; + const rows = table.data ?? []; + const columns = table.columns ?? []; + const header = columns.map((c) => c.label); + const body = rows.map((row) => + columns.map((column) => { + const value = this.formatCell(column, row); + const escaped = String(value).replace(/"/g, '""'); + return `"${escaped}"`; + }) + ); + + const csv = [header.join(';'), ...body.map((line) => line.join(';'))].join('\n'); + const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${filename}.csv`; + a.click(); + URL.revokeObjectURL(url); + } + + private getReservaPorDddChartData(): Array<{ label: string; totalLinhas: number }> { + const resumo = this.resumo; + if (!resumo) return []; + + const porDdd = resumo.reservaPorDdd ?? []; + if (porDdd.length) { + return porDdd.map((item) => ({ + label: String(item.ddd ?? '-'), + totalLinhas: this.toNumber(item.totalLinhas) ?? 0, + })); + } + + const fromLines = resumo.reservaLines ?? []; + const map = new Map(); + fromLines.forEach((row) => { + const key = String(row.ddd ?? '-'); + const qtd = this.toNumber(row.qtdLinhas) ?? 0; + map.set(key, (map.get(key) ?? 0) + qtd); + }); + return Array.from(map.entries()).map(([label, totalLinhas]) => ({ label, totalLinhas })); + } + + exportMacrophonyCsv() { this.exportCsv(this.tableMacrophony, 'macrophony-planos'); } + findLineTotal(k: string[]): LineTotal | null { + const keys = k.map((item) => item.toUpperCase()); + const list = Array.isArray(this.resumo?.lineTotais) ? this.resumo?.lineTotais : []; + for (const item of list) { + const tipo = (item?.tipo ?? '').toString().toUpperCase(); + if (keys.some((key) => tipo.includes(key))) return item; + } + return null; + } + + get macrophonyPageStart() { return (this.macrophonyPage - 1) * this.macrophonyPageSize + 1; } + get macrophonyPageEnd() { return Math.min(this.macrophonyPage * this.macrophonyPageSize, this.macrophonyFilteredGroups.length); } + get macrophonyFilteredGroups() { return this.macrophonyFiltered; } + get macrophonyPageNumbers() { + const total = this.macrophonyTotalPages; + if (total <= 1) return [1]; + const current = Math.min(Math.max(this.macrophonyPage, 1), total); + const start = Math.max(1, current - 2); + const end = Math.min(total, start + 4); + const adjustedStart = Math.max(1, end - 4); + return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i); + } + get macrophonyTotalPages() { return Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize)); } + get planosTotals() { return this.resumo?.macrophonyTotals; } + get contratosTotals() { return this.resumo?.planoContratoTotal; } + get clientesTotals() { return this.resumo?.vivoLineTotals; } + get gbDistribuicaoRows(): GbDistribuicao[] { return this.resumo?.gbDistribuicao ?? []; } + get gbDistribuicaoTotalLinhas(): number { + const fromTotal = this.toNumber(this.resumo?.gbDistribuicaoTotal?.totalLinhas); + if (fromTotal !== null) return fromTotal; + return this.gbDistribuicaoRows.reduce((acc, row) => acc + (this.toNumber(row.qtd) ?? 0), 0); + } + get gbDistribuicaoSomaTotal(): number { + const fromTotal = this.toNumber(this.resumo?.gbDistribuicaoTotal?.somaTotal); + if (fromTotal !== null) return fromTotal; + return this.gbDistribuicaoRows.reduce((acc, row) => acc + (this.toNumber(row.soma) ?? 0), 0); + } + get reservaTotals() { return this.resumo?.reservaTotal; } +} diff --git a/src/app/pages/troca-numero/troca-numero.html b/src/app/pages/troca-numero/troca-numero.html index 0430e7a..6e31ffb 100644 --- a/src/app/pages/troca-numero/troca-numero.html +++ b/src/app/pages/troca-numero/troca-numero.html @@ -76,7 +76,7 @@ - + diff --git a/src/app/pages/troca-numero/troca-numero.scss b/src/app/pages/troca-numero/troca-numero.scss index a0a3175..8c6fa2e 100644 --- a/src/app/pages/troca-numero/troca-numero.scss +++ b/src/app/pages/troca-numero/troca-numero.scss @@ -250,7 +250,7 @@ .controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; } .search-group { - max-width: 380px; + max-width: 270px; border-radius: 12px; overflow: hidden; display: flex; diff --git a/src/app/pages/vigencia/vigencia.html b/src/app/pages/vigencia/vigencia.html index 357cd5b..1ad0ba4 100644 --- a/src/app/pages/vigencia/vigencia.html +++ b/src/app/pages/vigencia/vigencia.html @@ -23,7 +23,11 @@
GESTÃO DE VIGÊNCIA
Controle de contratos e fidelização -
+
+ +
@@ -39,19 +43,25 @@ Total Vencidos {{ kpiTotalVencidos }}
-
- Valor Total - {{ kpiValorTotal | currency:'BRL' }} -
-
-
- - - -
+
+ + + + +
@@ -109,7 +119,7 @@ EFETIVAÇÃO VENCIMENTO TOTAL - AÇÕES + AÇÕES @@ -135,6 +145,8 @@
+ +
@@ -164,52 +176,258 @@
-
+
+
-
- + + +
+ +
+ + +
+ +
+ + +
+
diff --git a/src/app/pages/vigencia/vigencia.scss b/src/app/pages/vigencia/vigencia.scss index 1a9a7f4..e382393 100644 --- a/src/app/pages/vigencia/vigencia.scss +++ b/src/app/pages/vigencia/vigencia.scss @@ -6,6 +6,9 @@ --blue: #030FAA; --text: #111214; --muted: rgba(17, 18, 20, 0.65); + --surface-soft: rgba(255, 255, 255, 0.7); + --surface-hover: rgba(255, 255, 255, 0.94); + --focus-ring: 0 0 0 3px rgba(227, 61, 207, 0.16); --success-bg: rgba(25, 135, 84, 0.1); --success-text: #198754; @@ -75,42 +78,107 @@ .title-badge { display: inline-flex; align-items: center; gap: 10px; padding: 6px 12px; - border-radius: 999px; background: rgba(255, 255, 255, 0.78); + border-radius: 999px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.7)); border: 1px solid rgba(227, 61, 207, 0.22); font-size: 13px; font-weight: 800; + box-shadow: 0 8px 20px rgba(17, 18, 20, 0.06); i { color: var(--brand); } } .header-title { text-align: center; } .title { font-size: 1.5rem; font-weight: 950; margin: 0; letter-spacing: -0.5px; } .subtitle { color: var(--muted); font-weight: 700; } +.header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + white-space: nowrap; + min-height: 38px; + border-width: 1px; + } +} /* KPIs */ .mureg-kpis { - display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; + display: grid; + grid-template-columns: repeat(3, minmax(158px, 205px)); + justify-content: center; + gap: 8px; .kpi { background: rgba(255,255,255,0.7); border: 1px solid rgba(17,18,20,0.08); - border-radius: 16px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; + border-radius: 14px; + padding: 8px 10px; + min-height: 58px; + display: flex; + justify-content: space-between; + align-items: center; transition: transform 0.2s; &:hover { transform: translateY(-2px); border-color: var(--brand); background: #fff; } - .lbl { font-size: 0.72rem; font-weight: 900; text-transform: uppercase; color: var(--muted); } - .val { font-size: 1.25rem; font-weight: 950; color: var(--text); } + .lbl { font-size: 0.64rem; font-weight: 900; text-transform: uppercase; color: var(--muted); } + .val { font-size: 1.02rem; font-weight: 950; color: var(--text); } .text-brand { color: var(--brand) !important; } } - - .kpi.kpi-stack { - flex-direction: column; - align-items: center; - justify-content: center; - gap: 6px; - text-align: center; - } } /* Controls */ .search-group { - border-radius: 12px; background: #fff; border: 1px solid rgba(17,18,20,0.15); display: flex; align-items: center; - &:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); } - .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; &:focus { outline: none; } } + max-width: 270px; + border-radius: 12px; + overflow: hidden; + display: flex; + align-items: stretch; + background: #fff; + border: 1px solid rgba(17,18,20,0.15); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); + + &:focus-within { + border-color: var(--brand); + box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); + transform: translateY(-1px); + } + + .input-group-text { + background: transparent; + border: none; + color: var(--muted); + padding-left: 14px; + padding-right: 8px; + display: flex; + align-items: center; + } + + .form-control { + border: none; + background: transparent; + padding: 10px 0; + font-size: 0.9rem; + color: var(--text); + box-shadow: none; + + &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } + &:focus { outline: none; } + } + + .btn-clear { + background: transparent; + border: none; + color: var(--muted); + padding: 0 12px; + display: flex; + align-items: center; + cursor: pointer; + transition: color 0.2s; + + &:hover { color: #dc3545; } + } } .select-glass { @@ -118,6 +186,75 @@ color: var(--blue); font-weight: 800; } +.btn-brand, +.btn-glass, +.btn-primary, +.btn-danger { + border-radius: 12px; + font-weight: 900; + transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s, filter 0.2s; + + &:hover { + transform: translateY(-1px); + } + + &:focus-visible { + outline: none; + box-shadow: var(--focus-ring); + } + + &:disabled { + opacity: 0.72; + cursor: not-allowed; + transform: none; + box-shadow: none; + } +} + +.btn-brand { + background-color: var(--brand); + border-color: var(--brand); + color: #fff; + box-shadow: 0 10px 22px rgba(227, 61, 207, 0.22); + + &:hover { + box-shadow: 0 12px 24px rgba(227, 61, 207, 0.28); + filter: brightness(1.05); + } +} + +.btn-glass { + background: var(--surface-soft); + border: 1px solid rgba(3, 15, 170, 0.24); + color: var(--blue); + box-shadow: 0 6px 16px rgba(3, 15, 170, 0.1); + + &:hover { + background: var(--surface-hover); + border-color: var(--brand); + color: var(--brand); + box-shadow: 0 8px 18px rgba(227, 61, 207, 0.16); + } +} + +.btn-primary { + background: linear-gradient(135deg, #1543ff, #030faa); + border-color: #030faa; + color: #fff; + box-shadow: 0 10px 22px rgba(3, 15, 170, 0.28); + + &:hover { + box-shadow: 0 12px 24px rgba(3, 15, 170, 0.3); + filter: brightness(1.05); + } +} + +.btn-danger { + background: linear-gradient(135deg, #ef4444, #dc2626); + border-color: #dc2626; + box-shadow: 0 10px 22px rgba(220, 38, 38, 0.26); +} + /* BODY E GRUPOS */ .geral-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; } .groups-container { padding: 16px; overflow-y: auto; height: 100%; } @@ -166,10 +303,23 @@ .text-blue { color: var(--blue) !important; } .td-clip { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.actions-col { min-width: 152px; } + +.action-group { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + flex-wrap: nowrap; + white-space: nowrap; +} + .btn-icon { - width: 32px; height: 32px; border: none; background: transparent; border-radius: 8px; color: rgba(17,18,20,0.5); + width: 32px; height: 32px; border: none; background: rgba(17,18,20,0.04); border-radius: 8px; color: rgba(17,18,20,0.6); display: flex; align-items: center; justify-content: center; transition: all 0.2s; - &:hover { background: rgba(3,15,170,0.1); color: var(--blue); } + &:hover { background: rgba(17,18,20,0.08); color: var(--text); transform: translateY(-1px); } + &.primary:hover { background: rgba(3,15,170,0.1); color: var(--blue); } + &.danger:hover { background: rgba(220, 53, 69, 0.12); color: #dc3545; } } /* FOOTER */ @@ -177,10 +327,309 @@ padding: 14px 24px; border-top: 1px solid rgba(17, 18, 20, 0.06); display: flex; justify-content: space-between; align-items: center; } .pagination-modern .page-link { color: var(--blue); font-weight: 900; border-radius: 10px; border: 1px solid rgba(17,18,20,0.1); background: #fff; margin: 0 2px; } +.pagination-modern .page-link:hover { border-color: var(--brand); color: var(--brand); background: rgba(255, 255, 255, 0.98); } .pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; } /* MODAL */ -.lg-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } +.lg-backdrop { + position: fixed; + inset: 0; + background: radial-gradient(circle at 20% 0%, rgba(227, 61, 207, 0.18), rgba(0, 0, 0, 0.55) 45%); + z-index: 9990; + backdrop-filter: blur(5px); +} .lg-modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } -.lg-modal-card { background: #ffffff; border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); width: 600px; overflow: hidden; animation: popUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } +.lg-modal-card { + background: #ffffff; + border: 1px solid rgba(255,255,255,0.86); + border-radius: 20px; + box-shadow: 0 30px 60px -18px rgba(0, 0, 0, 0.4); + width: min(860px, 96vw); + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; + animation: popUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.lg-modal-card.modal-lg { width: min(760px, 94vw); } +.lg-modal-card.modal-xl { width: min(1040px, 95vw); max-height: 86vh; } + +.lg-modal-card .modal-header { + padding: 16px 24px; + border-bottom: 1px solid rgba(0,0,0,0.06); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.09), rgba(255, 255, 255, 0.95) 70%); + display: flex; + justify-content: space-between; + align-items: center; +} + +.lg-modal-card .modal-title { + font-size: 1.08rem; + font-weight: 900; + color: var(--text); + display: flex; + align-items: center; + gap: 12px; +} + +.lg-modal-card .icon-bg { + width: 32px; + height: 32px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 16px; + background: rgba(3, 15, 170, 0.1); + color: var(--blue); +} + +.lg-modal-card .icon-bg.primary-soft { background: rgba(3, 15, 170, 0.1); color: var(--blue); } +.lg-modal-card .icon-bg.danger-soft { background: rgba(220, 53, 69, 0.12); color: #dc3545; } + +.lg-modal-card .modal-body { flex: 1; min-height: 0; overflow-y: auto; } +.lg-modal-card .modal-footer { + flex-shrink: 0; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + background: linear-gradient(180deg, #ffffff, rgba(248, 249, 251, 0.96)); +} +.lg-modal-card.create-modal { width: min(1080px, 95vw); max-height: 86vh; } +.lg-modal-card.create-modal .modal-header { background: linear-gradient(180deg, rgba(227, 61, 207, 0.08), #ffffff 70%); } +.lg-modal-card.create-modal .modal-body { background: linear-gradient(180deg, rgba(248, 249, 250, 0.96), rgba(255, 255, 255, 0.98)); } +.lg-modal-card.create-modal .edit-sections { gap: 14px; } +.lg-modal-card.create-modal .detail-box { border: 1px solid rgba(227, 61, 207, 0.14); box-shadow: 0 10px 24px rgba(17, 18, 20, 0.06); } +.lg-modal-card.create-modal .box-header { color: var(--brand); background: linear-gradient(135deg, rgba(227, 61, 207, 0.1), rgba(3, 15, 170, 0.07)); } +.lg-modal-card.create-modal .box-body { background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(250, 250, 252, 0.96)); } +.lg-modal-card.create-modal .form-field label { color: rgba(17, 18, 20, 0.68); } +.lg-modal-card.create-modal .form-control, +.lg-modal-card.create-modal .form-select { min-height: 40px; } +.lg-modal-card.create-modal .modal-footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; + padding: 14px 20px !important; + background: linear-gradient(180deg, #ffffff, rgba(248, 249, 251, 0.95)); +} +.lg-modal-card.create-modal .modal-footer .btn { + border-radius: 12px; + font-weight: 900; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 120px; +} +.lg-modal-card.create-modal .modal-footer .btn.me-2 { margin-right: 0 !important; } +.bg-light-gray { background-color: #f8f9fa; } + +.lg-modal-card .btn-icon { + width: 32px; + height: 32px; + border: none; + border-radius: 8px; + background: rgba(17, 18, 20, 0.04); + color: var(--muted); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: rgba(17, 18, 20, 0.08); + color: var(--brand); + transform: translateY(-1px); + } +} + @keyframes popUp { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } + +.details-dashboard { display: grid; grid-template-columns: 1fr; gap: 16px; } +.detail-box { background: #fff; border-radius: 16px; border: 1px solid rgba(0,0,0,0.06); box-shadow: 0 2px 10px rgba(0,0,0,0.03); overflow: hidden; } + +.box-header { + padding: 10px 16px; + font-size: 0.76rem; + font-weight: 900; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); + border-bottom: 1px solid rgba(0,0,0,0.05); + background: #fdfdfd; + display: flex; + align-items: center; +} + +.box-header.justify-content-center { + justify-content: center; + text-align: center; + color: var(--brand); + background: linear-gradient(135deg, rgba(227, 61, 207, 0.08), rgba(59, 130, 246, 0.08)); +} + +.box-body { padding: 16px; } + +.info-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.info-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 8px; + background: rgba(245, 245, 247, 0.55); + border-radius: 12px; + border: 1px solid rgba(0,0,0,0.04); + + &.span-2 { grid-column: span 2; } + + .lbl { + font-size: 0.64rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; + color: var(--muted); + margin-bottom: 2px; + } + + .val { + font-size: 0.92rem; + font-weight: 700; + color: var(--text); + word-break: break-word; + line-height: 1.25; + } +} + +.status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 84px; + padding: 5px 12px; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.03em; + background: rgba(25, 135, 84, 0.12); + color: #157347; + border: 1px solid rgba(25, 135, 84, 0.24); +} + +.status-pill.is-danger { + background: rgba(220, 53, 69, 0.12); + color: #b02a37; + border-color: rgba(220, 53, 69, 0.24); +} + +.edit-sections { display: grid; gap: 12px; } +.edit-sections .detail-box { border: 1px solid rgba(17, 18, 20, 0.08); box-shadow: 0 8px 22px rgba(17, 18, 20, 0.06); } + +summary.box-header { + cursor: pointer; + list-style: none; + user-select: none; + + i:not(.transition-icon) { color: var(--brand); margin-right: 6px; } + &::-webkit-details-marker { display: none; } +} + +.transition-icon { color: var(--muted); transition: transform 0.25s ease, color 0.25s ease; } +details[open] .transition-icon { transform: rotate(180deg); color: var(--brand); } + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.form-field { + display: flex; + flex-direction: column; + gap: 6px; + + &.span-2 { grid-column: span 2; } + + label { + font-size: 0.72rem; + font-weight: 900; + letter-spacing: 0.04em; + text-transform: uppercase; + color: rgba(17, 18, 20, 0.64); + } +} + +.form-control, +.form-select { + border-radius: 10px; + border: 1px solid rgba(17,18,20,0.15); + background: #fff; + font-size: 0.9rem; + font-weight: 600; + color: var(--text); + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; + + &:hover { border-color: rgba(17, 18, 20, 0.36); } + &:focus { + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(227,61,207,0.15); + outline: none; + transform: translateY(-1px); + } +} + +.confirm-delete { + border: 1px solid rgba(220, 53, 69, 0.16); + background: #fff; + border-radius: 14px; + padding: 18px 16px; + display: flex; + align-items: center; + gap: 12px; + + p { font-weight: 700; color: rgba(17, 18, 20, 0.85); } +} + +.confirm-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(220, 53, 69, 0.12); + color: #dc3545; + flex-shrink: 0; +} + +@media (max-width: 992px) { + .mureg-kpis { + grid-template-columns: repeat(2, minmax(150px, 198px)); + } +} + +@media (max-width: 700px) { + .mureg-kpis { + grid-template-columns: minmax(0, 1fr); + justify-content: stretch; + } + .lg-modal-card { border-radius: 16px; } + .lg-modal-card .modal-header { padding: 12px 16px; } + .lg-modal-card .modal-body { padding: 16px !important; } + .lg-modal-card.create-modal .modal-footer { flex-direction: column-reverse; } + .lg-modal-card.create-modal .modal-footer .btn { width: 100%; min-width: 0; } + .form-grid, + .info-grid { grid-template-columns: 1fr; } + .info-item.span-2, + .form-field.span-2 { grid-column: span 1; } +} diff --git a/src/app/pages/vigencia/vigencia.ts b/src/app/pages/vigencia/vigencia.ts index d5dd528..bda08c7 100644 --- a/src/app/pages/vigencia/vigencia.ts +++ b/src/app/pages/vigencia/vigencia.ts @@ -1,14 +1,25 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; -import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult } from '../../services/vigencia.service'; +import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult, UpdateVigenciaRequest } from '../../services/vigencia.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { AuthService } from '../../services/auth.service'; +import { LinesService, MobileLineDetail } from '../../services/lines.service'; +import { PlanAutoFillService } from '../../services/plan-autofill.service'; type SortDir = 'asc' | 'desc'; type ToastType = 'success' | 'danger'; type ViewMode = 'lines' | 'groups'; +interface LineOptionDto { + id: string; + item: number; + linha: string | null; + usuario: string | null; + label?: string; +} + @Component({ selector: 'app-vigencia', standalone: true, @@ -16,7 +27,7 @@ type ViewMode = 'lines' | 'groups'; templateUrl: './vigencia.html', styleUrls: ['./vigencia.scss'], }) -export class VigenciaComponent implements OnInit { +export class VigenciaComponent implements OnInit, OnDestroy { loading = false; errorMsg = ''; @@ -46,7 +57,6 @@ export class VigenciaComponent implements OnInit { kpiTotalClientes = 0; kpiTotalLinhas = 0; kpiTotalVencidos = 0; - kpiValorTotal = 0; // === ACORDEÃO === expandedGroup: string | null = null; @@ -56,18 +66,63 @@ export class VigenciaComponent implements OnInit { // UI detailsOpen = false; selectedRow: VigenciaRow | null = null; + editOpen = false; + editSaving = false; + editModel: VigenciaRow | null = null; + editEfetivacao = ''; + editTermino = ''; + editingId: string | null = null; + deleteOpen = false; + deleteTarget: VigenciaRow | null = null; + + createOpen = false; + createSaving = false; + createModel: any = { + selectedClient: '', + mobileLineId: '', + item: '', + conta: '', + linha: '', + cliente: '', + usuario: '', + planoContrato: '', + total: null + }; + createEfetivacao = ''; + createTermino = ''; + + lineOptionsCreate: LineOptionDto[] = []; + createClientsLoading = false; + createLinesLoading = false; + clientsFromGeral: string[] = []; + planOptions: string[] = []; + + isAdmin = false; toastOpen = false; toastMessage = ''; toastType: ToastType = 'success'; private toastTimer: any = null; + private searchTimer: any = null; - constructor(private vigenciaService: VigenciaService) {} + constructor( + private vigenciaService: VigenciaService, + private authService: AuthService, + private linesService: LinesService, + private planAutoFill: PlanAutoFillService + ) {} ngOnInit(): void { + this.isAdmin = this.authService.hasRole('admin'); this.loadClients(); + this.loadPlanRules(); this.fetch(1); } + ngOnDestroy(): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + if (this.toastTimer) clearTimeout(this.toastTimer); + } + setView(mode: ViewMode): void { if (this.viewMode === mode) return; this.viewMode = mode; @@ -85,6 +140,15 @@ export class VigenciaComponent implements OnInit { }); } + private async loadPlanRules() { + try { + await this.planAutoFill.load(); + this.planOptions = this.planAutoFill.getPlanOptions(); + } catch { + this.planOptions = []; + } + } + get totalPages(): number { return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10))); } @@ -120,7 +184,6 @@ export class VigenciaComponent implements OnInit { this.kpiTotalClientes = res.kpis.totalClientes; this.kpiTotalLinhas = res.kpis.totalLinhas; this.kpiTotalVencidos = res.kpis.totalVencidos; - this.kpiValorTotal = res.kpis.valorTotal; this.loading = false; }, @@ -199,10 +262,298 @@ export class VigenciaComponent implements OnInit { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); } - clearFilters() { this.search = ''; this.fetch(1); } + onSearchChange() { + if (this.searchTimer) clearTimeout(this.searchTimer); + this.searchTimer = setTimeout(() => this.fetch(1), 300); + } + + clearFilters() { + this.search = ''; + if (this.searchTimer) clearTimeout(this.searchTimer); + this.fetch(1); + } openDetails(r: VigenciaRow) { this.selectedRow = r; this.detailsOpen = true; } closeDetails() { this.detailsOpen = false; } + openEdit(r: VigenciaRow) { + if (!this.isAdmin) return; + this.editingId = r.id; + this.editModel = { ...r }; + this.editEfetivacao = this.toDateInput(r.dtEfetivacaoServico); + this.editTermino = this.toDateInput(r.dtTerminoFidelizacao); + this.editOpen = true; + } + + closeEdit() { + this.editOpen = false; + this.editSaving = false; + this.editModel = null; + this.editEfetivacao = ''; + this.editTermino = ''; + this.editingId = null; + } + + saveEdit() { + if (!this.editModel || !this.editingId) return; + this.editSaving = true; + + const payload: UpdateVigenciaRequest = { + item: this.toNullableNumber(this.editModel.item), + conta: this.editModel.conta, + linha: this.editModel.linha, + cliente: this.editModel.cliente, + usuario: this.editModel.usuario, + planoContrato: this.editModel.planoContrato, + dtEfetivacaoServico: this.dateInputToIso(this.editEfetivacao), + dtTerminoFidelizacao: this.dateInputToIso(this.editTermino), + total: this.toNullableNumber(this.editModel.total) + }; + + this.vigenciaService.update(this.editingId, payload).subscribe({ + next: () => { + this.editSaving = false; + this.closeEdit(); + this.fetch(); + this.showToast('Registro atualizado!', 'success'); + }, + error: () => { + this.editSaving = false; + this.showToast('Erro ao salvar.', 'danger'); + } + }); + } + + // ========================== + // CREATE + // ========================== + openCreate() { + if (!this.isAdmin) return; + this.resetCreateModel(); + this.createOpen = true; + this.preloadGeralClients(); + } + + closeCreate() { + this.createOpen = false; + this.createSaving = false; + this.createModel = null; + } + + private resetCreateModel() { + this.createModel = { + selectedClient: '', + mobileLineId: '', + item: '', + conta: '', + linha: '', + cliente: '', + usuario: '', + planoContrato: '', + total: null + }; + this.createEfetivacao = ''; + this.createTermino = ''; + this.lineOptionsCreate = []; + this.createLinesLoading = false; + this.createClientsLoading = false; + } + + private preloadGeralClients() { + this.createClientsLoading = true; + this.linesService.getClients().subscribe({ + next: (list) => { + this.clientsFromGeral = list ?? []; + this.createClientsLoading = false; + }, + error: () => { + this.clientsFromGeral = []; + this.createClientsLoading = false; + } + }); + } + + onCreateClientChange() { + const c = (this.createModel.selectedClient ?? '').trim(); + this.createModel.mobileLineId = ''; + this.createModel.linha = ''; + this.createModel.conta = ''; + this.createModel.usuario = ''; + this.createModel.planoContrato = ''; + this.createModel.total = null; + this.createModel.cliente = c; + this.lineOptionsCreate = []; + + if (c) this.loadLinesForClient(c); + } + + private loadLinesForClient(cliente: string) { + const c = (cliente ?? '').trim(); + if (!c) return; + + this.createLinesLoading = true; + this.linesService.getLinesByClient(c).subscribe({ + next: (items: any[]) => { + const mapped: LineOptionDto[] = (items ?? []) + .filter(x => !!String(x?.id ?? '').trim()) + .map(x => ({ + id: String(x.id), + item: Number(x.item ?? 0), + linha: x.linha ?? null, + usuario: x.usuario ?? null, + label: `${x.item ?? ''} • ${x.linha ?? '-'} • ${x.usuario ?? 'SEM USUÁRIO'}` + })) + .filter(x => !!String(x.linha ?? '').trim()); + + this.lineOptionsCreate = mapped; + this.createLinesLoading = false; + }, + error: () => { + this.lineOptionsCreate = []; + this.createLinesLoading = false; + this.showToast('Erro ao carregar linhas da GERAL.', 'danger'); + } + }); + } + + onCreateLineChange() { + const id = String(this.createModel.mobileLineId ?? '').trim(); + if (!id) return; + + this.linesService.getById(id).subscribe({ + next: (d: MobileLineDetail) => this.applyLineDetailToCreate(d), + error: () => this.showToast('Erro ao carregar dados da linha.', 'danger') + }); + } + + private applyLineDetailToCreate(d: MobileLineDetail) { + this.createModel.linha = d.linha ?? ''; + this.createModel.conta = d.conta ?? ''; + this.createModel.cliente = d.cliente ?? this.createModel.cliente ?? ''; + this.createModel.usuario = d.usuario ?? ''; + this.createModel.planoContrato = d.planoContrato ?? ''; + this.createEfetivacao = this.toDateInput(d.dtEfetivacaoServico ?? null); + this.createTermino = this.toDateInput(d.dtTerminoFidelizacao ?? null); + + this.ensurePlanOption(this.createModel.planoContrato); + + if (!String(this.createModel.item ?? '').trim() && d.item) { + this.createModel.item = String(d.item); + } + + this.onCreatePlanChange(); + } + + onCreatePlanChange() { + this.ensurePlanOption(this.createModel?.planoContrato); + this.applyPlanSuggestion(this.createModel); + } + + onEditPlanChange() { + if (!this.editModel) return; + this.ensurePlanOption(this.editModel?.planoContrato); + this.applyPlanSuggestion(this.editModel); + } + + private applyPlanSuggestion(model: any) { + const plan = (model?.planoContrato ?? '').toString().trim(); + if (!plan) return; + + const suggestion = this.planAutoFill.suggest(plan); + if (!suggestion) return; + + if (suggestion.valorPlano != null) { + model.total = suggestion.valorPlano; + } + } + + private ensurePlanOption(plan: any) { + const p = (plan ?? '').toString().trim(); + if (!p) return; + if (!this.planOptions.includes(p)) { + this.planOptions = [p, ...this.planOptions]; + } + } + + saveCreate() { + if (!this.createModel) return; + this.applyPlanSuggestion(this.createModel); + + const payload = { + item: this.toNullableNumber(this.createModel.item), + conta: this.createModel.conta, + linha: this.createModel.linha, + cliente: this.createModel.cliente, + usuario: this.createModel.usuario, + planoContrato: this.createModel.planoContrato, + dtEfetivacaoServico: this.dateInputToIso(this.createEfetivacao), + dtTerminoFidelizacao: this.dateInputToIso(this.createTermino), + total: this.toNullableNumber(this.createModel.total) + }; + + this.createSaving = true; + this.vigenciaService.create(payload).subscribe({ + next: () => { + this.createSaving = false; + this.closeCreate(); + this.fetch(); + this.showToast('Vigência criada com sucesso!', 'success'); + }, + error: () => { + this.createSaving = false; + this.showToast('Erro ao criar vigência.', 'danger'); + } + }); + } + + openDelete(r: VigenciaRow) { + if (!this.isAdmin) return; + this.deleteTarget = r; + this.deleteOpen = true; + } + + cancelDelete() { + this.deleteOpen = false; + this.deleteTarget = null; + } + + confirmDelete() { + if (!this.deleteTarget) return; + const id = this.deleteTarget.id; + this.vigenciaService.remove(id).subscribe({ + next: () => { + this.deleteOpen = false; + this.deleteTarget = null; + this.fetch(); + this.showToast('Registro removido.', 'success'); + }, + error: () => { + this.deleteOpen = false; + this.deleteTarget = null; + this.showToast('Erro ao remover.', 'danger'); + } + }); + } + + private toDateInput(value: string | null): string { + if (!value) return ''; + const d = new Date(value); + if (isNaN(d.getTime())) return ''; + return d.toISOString().slice(0, 10); + } + + private dateInputToIso(value: string): string | null { + if (!value) return null; + const d = new Date(`${value}T00:00:00`); + if (isNaN(d.getTime())) return null; + return d.toISOString(); + } + + private toNullableNumber(value: any): number | null { + if (value === undefined || value === null || value === '') return null; + const n = Number(value); + return Number.isNaN(n) ? null : n; + } + handleError(err: HttpErrorResponse, msg: string) { this.loading = false; this.expandedLoading = false; diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index dbeda6a..89a9b5a 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from '../../environments/environment'; +import { BehaviorSubject } from 'rxjs'; import { tap } from 'rxjs/operators'; export interface RegisterPayload { @@ -16,35 +17,112 @@ export interface LoginPayload { password: string; } +export interface LoginOptions { + rememberMe?: boolean; +} + +export interface LoginResponse { + token?: string; + accessToken?: string; +} + +export interface AuthUserProfile { + id: string; + nome: string; + email: string; + tenantId: string; + roles: string[]; +} + @Injectable({ providedIn: 'root' }) export class AuthService { private baseUrl = `${environment.apiUrl}/auth`; + private userProfileSubject = new BehaviorSubject(null); + readonly userProfile$ = this.userProfileSubject.asObservable(); + private readonly tokenStorageKey = 'token'; + private readonly tokenExpiresAtKey = 'tokenExpiresAt'; + private readonly rememberMeHours = 6; - constructor(private http: HttpClient) {} + constructor(private http: HttpClient) { + this.syncUserProfileFromToken(); + } register(payload: RegisterPayload) { return this.http.post<{ token: string }>(`${this.baseUrl}/register`, payload) - .pipe(tap(r => localStorage.setItem('token', r.token))); + .pipe(tap(r => this.setToken(r.token))); } - login(payload: LoginPayload) { - return this.http.post<{ token: string }>(`${this.baseUrl}/login`, payload) - .pipe(tap(r => localStorage.setItem('token', r.token))); + login(payload: LoginPayload, options?: LoginOptions) { + return this.http.post(`${this.baseUrl}/login`, payload) + .pipe( + tap((r) => { + const token = this.resolveLoginToken(r); + if (!token) return; + this.setToken(token, options?.rememberMe ?? false); + }) + ); } logout() { - localStorage.removeItem('token'); + if (typeof window === 'undefined') { + this.userProfileSubject.next(null); + return; + } + + this.clearTokenStorage(localStorage); + this.clearTokenStorage(sessionStorage); + this.userProfileSubject.next(null); + } + + setToken(token: string, rememberMe = false) { + if (typeof window === 'undefined') return; + this.clearTokenStorage(localStorage); + this.clearTokenStorage(sessionStorage); + + if (rememberMe) { + const expiresAt = Date.now() + this.rememberMeHours * 60 * 60 * 1000; + localStorage.setItem(this.tokenStorageKey, token); + localStorage.setItem(this.tokenExpiresAtKey, String(expiresAt)); + } else { + sessionStorage.setItem(this.tokenStorageKey, token); + } + + this.syncUserProfileFromToken(); } get token(): string | null { if (typeof window === 'undefined') return null; - return localStorage.getItem('token'); + this.cleanupExpiredRememberSession(); + + const sessionToken = sessionStorage.getItem(this.tokenStorageKey); + if (sessionToken) return sessionToken; + + return localStorage.getItem(this.tokenStorageKey); } isLoggedIn(): boolean { return !!this.token; } + get currentUserProfile(): AuthUserProfile | null { + return this.userProfileSubject.value; + } + + syncUserProfileFromToken() { + this.userProfileSubject.next(this.buildProfileFromToken()); + } + + updateUserProfile(profile: Pick) { + const current = this.userProfileSubject.value; + if (!current) return; + + this.userProfileSubject.next({ + ...current, + nome: profile.nome.trim(), + email: profile.email.trim().toLowerCase(), + }); + } + getTokenPayload(): Record | null { const token = this.token; if (!token) return null; @@ -66,6 +144,10 @@ export class AuthService { getRoles(): string[] { const payload = this.getTokenPayload(); if (!payload) return []; + return this.extractRoles(payload); + } + + private extractRoles(payload: Record): string[] { const possibleKeys = [ 'role', 'roles', @@ -81,9 +163,74 @@ export class AuthService { return roles.map(r => r.toLowerCase()); } + private buildProfileFromToken(): AuthUserProfile | null { + const payload = this.getTokenPayload(); + if (!payload) return null; + + const id = String( + payload['sub'] ?? + payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'] ?? + '' + ).trim(); + const nome = String(payload['name'] ?? '').trim(); + const email = String( + payload['email'] ?? + payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] ?? + '' + ).trim().toLowerCase(); + const tenantId = String( + payload['tenantId'] ?? + payload['tenant'] ?? + payload['TenantId'] ?? + '' + ).trim(); + + if (!id || !tenantId) return null; + + return { + id, + nome, + email, + tenantId, + roles: this.extractRoles(payload), + }; + } + hasRole(role: string): boolean { const target = (role || '').toLowerCase(); if (!target) return false; return this.getRoles().includes(target); } + + private cleanupExpiredRememberSession() { + const token = localStorage.getItem(this.tokenStorageKey); + if (!token) return; + + const expiresAtRaw = localStorage.getItem(this.tokenExpiresAtKey); + if (!expiresAtRaw) { + this.clearTokenStorage(localStorage); + return; + } + + const expiresAt = Number(expiresAtRaw); + if (!Number.isFinite(expiresAt)) { + this.clearTokenStorage(localStorage); + return; + } + + if (Date.now() > expiresAt) { + this.clearTokenStorage(localStorage); + } + } + + private clearTokenStorage(storage: Storage) { + storage.removeItem(this.tokenStorageKey); + storage.removeItem(this.tokenExpiresAtKey); + } + + private resolveLoginToken(response: LoginResponse | null | undefined): string | null { + const raw = response?.token ?? response?.accessToken ?? null; + const token = (raw ?? '').toString().trim(); + return token || null; + } } diff --git a/src/app/services/billing.ts b/src/app/services/billing.ts index 1638710..adcd886 100644 --- a/src/app/services/billing.ts +++ b/src/app/services/billing.ts @@ -34,6 +34,22 @@ export interface BillingItem { aparelho?: string | null; formaPagamento?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} + +export interface BillingUpdateRequest { + tipo?: string; + item?: number | null; + cliente?: string | null; + qtdLinhas?: number | null; + franquiaVivo?: number | null; + valorContratoVivo?: number | null; + franquiaLine?: number | null; + valorContratoLine?: number | null; + lucro?: number | null; + aparelho?: string | null; + formaPagamento?: string | null; } export interface BillingQuery { @@ -84,4 +100,16 @@ export class BillingService { return this.getPaged(q).pipe(map((res) => res.items ?? [])); } + + getById(id: string): Observable { + return this.http.get(`${this.baseUrl}/${id}`); + } + + update(id: string, payload: BillingUpdateRequest): Observable { + return this.http.put(`${this.baseUrl}/${id}`, payload); + } + + remove(id: string): Observable { + return this.http.delete(`${this.baseUrl}/${id}`); + } } diff --git a/src/app/services/chips-controle.service.ts b/src/app/services/chips-controle.service.ts index 87374b7..d21be34 100644 --- a/src/app/services/chips-controle.service.ts +++ b/src/app/services/chips-controle.service.ts @@ -17,8 +17,18 @@ export interface ChipVirgemListDto { item: number; numeroDoChip: string | null; observacoes: string | null; + createdAt?: string | null; + updatedAt?: string | null; } +export interface UpdateChipVirgemRequest { + item?: number | null; + numeroDoChip?: string | null; + observacoes?: string | null; +} + +export interface CreateChipVirgemRequest extends UpdateChipVirgemRequest {} + export interface ControleRecebidoListDto { id: string; ano: number | null; @@ -34,8 +44,28 @@ export interface ControleRecebidoListDto { dataDoRecebimento: string | null; quantidade: number | null; isResumo: boolean | null; + createdAt?: string | null; + updatedAt?: string | null; } +export interface UpdateControleRecebidoRequest { + ano?: number | null; + item?: number | null; + notaFiscal?: string | null; + chip?: string | null; + serial?: string | null; + conteudoDaNf?: string | null; + numeroDaLinha?: string | null; + valorUnit?: number | null; + valorDaNf?: number | null; + dataDaNf?: string | null; + dataDoRecebimento?: string | null; + quantidade?: number | null; + isResumo?: boolean | null; +} + +export interface CreateControleRecebidoRequest extends UpdateControleRecebidoRequest {} + @Injectable({ providedIn: 'root' }) export class ChipsControleService { private readonly baseApi: string; @@ -67,6 +97,18 @@ export class ChipsControleService { return this.http.get(`${this.baseApi}/chips-virgens/${id}`); } + updateChipVirgem(id: string, payload: UpdateChipVirgemRequest): Observable { + return this.http.put(`${this.baseApi}/chips-virgens/${id}`, payload); + } + + createChipVirgem(payload: CreateChipVirgemRequest): Observable { + return this.http.post(`${this.baseApi}/chips-virgens`, payload); + } + + removeChipVirgem(id: string): Observable { + return this.http.delete(`${this.baseApi}/chips-virgens/${id}`); + } + getControleRecebidos(opts: { ano?: number | string | null; isResumo?: boolean | string | null; @@ -95,4 +137,16 @@ export class ChipsControleService { getControleRecebidoById(id: string): Observable { return this.http.get(`${this.baseApi}/controle-recebidos/${id}`); } + + updateControleRecebido(id: string, payload: UpdateControleRecebidoRequest): Observable { + return this.http.put(`${this.baseApi}/controle-recebidos/${id}`, payload); + } + + createControleRecebido(payload: CreateControleRecebidoRequest): Observable { + return this.http.post(`${this.baseApi}/controle-recebidos`, payload); + } + + removeControleRecebido(id: string): Observable { + return this.http.delete(`${this.baseApi}/controle-recebidos/${id}`); + } } diff --git a/src/app/services/dados-usuarios.service.ts b/src/app/services/dados-usuarios.service.ts index 88e69d6..d39a627 100644 --- a/src/app/services/dados-usuarios.service.ts +++ b/src/app/services/dados-usuarios.service.ts @@ -17,6 +17,10 @@ export interface UserDataRow { item: number; linha: string | null; cliente: string | null; + tipoPessoa?: string | null; + nome?: string | null; + razaoSocial?: string | null; + cnpj?: string | null; cpf: string | null; email: string | null; celular: string | null; @@ -26,10 +30,30 @@ export interface UserDataRow { dataNascimento: string | null; } +export interface UpdateUserDataRequest { + item?: number | null; + linha?: string | null; + cliente?: string | null; + tipoPessoa?: string | null; + nome?: string | null; + razaoSocial?: string | null; + cnpj?: string | null; + cpf?: string | null; + rg?: string | null; + dataNascimento?: string | null; + email?: string | null; + endereco?: string | null; + celular?: string | null; + telefoneFixo?: string | null; +} + +export interface CreateUserDataRequest extends UpdateUserDataRequest {} + export interface UserDataClientGroup { cliente: string; totalRegistros: number; comCpf: number; + comCnpj: number; comEmail: number; } @@ -37,6 +61,7 @@ export interface UserDataKpis { totalRegistros: number; clientesUnicos: number; comCpf: number; + comCnpj: number; comEmail: number; } @@ -56,6 +81,7 @@ export class DadosUsuariosService { getGroups(opts: { search?: string; + tipo?: string; page?: number; pageSize?: number; sortBy?: string; @@ -63,6 +89,7 @@ export class DadosUsuariosService { }): Observable { let params = new HttpParams(); if (opts.search) params = params.set('search', opts.search); + if (opts.tipo) params = params.set('tipo', opts.tipo); params = params.set('page', String(opts.page || 1)); params = params.set('pageSize', String(opts.pageSize || 10)); @@ -75,6 +102,7 @@ export class DadosUsuariosService { getRows(opts: { search?: string; client?: string; + tipo?: string; page?: number; pageSize?: number; sortBy?: string; @@ -83,6 +111,7 @@ export class DadosUsuariosService { let params = new HttpParams(); if (opts.search) params = params.set('search', opts.search); if (opts.client) params = params.set('client', opts.client); + if (opts.tipo) params = params.set('tipo', opts.tipo); params = params.set('page', String(opts.page || 1)); params = params.set('pageSize', String(opts.pageSize || 20)); @@ -92,11 +121,25 @@ export class DadosUsuariosService { return this.http.get>(`${this.baseApi}/user-data`, { params }); } - getClients(): Observable { - return this.http.get(`${this.baseApi}/user-data/clients`); + getClients(tipo?: string): Observable { + let params = new HttpParams(); + if (tipo) params = params.set('tipo', tipo); + return this.http.get(`${this.baseApi}/user-data/clients`, { params }); } getById(id: string): Observable { return this.http.get(`${this.baseApi}/user-data/${id}`); } -} \ No newline at end of file + + update(id: string, payload: UpdateUserDataRequest): Observable { + return this.http.put(`${this.baseApi}/user-data/${id}`, payload); + } + + create(payload: CreateUserDataRequest): Observable { + return this.http.post(`${this.baseApi}/user-data`, payload); + } + + remove(id: string): Observable { + return this.http.delete(`${this.baseApi}/user-data/${id}`); + } +} diff --git a/src/app/services/historico.service.ts b/src/app/services/historico.service.ts new file mode 100644 index 0000000..69e9636 --- /dev/null +++ b/src/app/services/historico.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { environment } from '../../environments/environment'; + +export type AuditAction = 'CREATE' | 'UPDATE' | 'DELETE'; +export type AuditChangeType = 'added' | 'modified' | 'removed'; + +export interface AuditFieldChangeDto { + field: string; + changeType: AuditChangeType; + oldValue?: string | null; + newValue?: string | null; +} + +export interface AuditLogDto { + id: string; + occurredAtUtc: string; + action: AuditAction | string; + page: string; + entityName: string; + entityId?: string | null; + entityLabel?: string | null; + userId?: string | null; + userName?: string | null; + userEmail?: string | null; + requestPath?: string | null; + requestMethod?: string | null; + ipAddress?: string | null; + changes: AuditFieldChangeDto[]; +} + +export interface PagedResult { + page: number; + pageSize: number; + total: number; + items: T[]; +} + +export interface HistoricoQuery { + pageName?: string; + action?: AuditAction | string; + entity?: string; + userId?: string; + search?: string; + dateFrom?: string; + dateTo?: string; + page?: number; + pageSize?: number; +} + +@Injectable({ providedIn: 'root' }) +export class HistoricoService { + private readonly baseApi: string; + + constructor(private http: HttpClient) { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + } + + list(params: HistoricoQuery): Observable> { + let httpParams = new HttpParams(); + if (params.pageName) httpParams = httpParams.set('pageName', params.pageName); + if (params.action) httpParams = httpParams.set('action', params.action); + if (params.entity) httpParams = httpParams.set('entity', params.entity); + if (params.userId) httpParams = httpParams.set('userId', params.userId); + if (params.search) httpParams = httpParams.set('search', params.search); + if (params.dateFrom) httpParams = httpParams.set('dateFrom', params.dateFrom); + if (params.dateTo) httpParams = httpParams.set('dateTo', params.dateTo); + + httpParams = httpParams.set('page', String(params.page || 1)); + httpParams = httpParams.set('pageSize', String(params.pageSize || 10)); + + return this.http.get>(`${this.baseApi}/historico`, { params: httpParams }); + } +} diff --git a/src/app/services/lines.service.ts b/src/app/services/lines.service.ts index 3483a2e..d65f616 100644 --- a/src/app/services/lines.service.ts +++ b/src/app/services/lines.service.ts @@ -47,6 +47,8 @@ export interface MobileLineDetail extends MobileLineList { solicitante?: string | null; dataEntregaOpera?: string | null; dataEntregaCliente?: string | null; + dtEfetivacaoServico?: string | null; + dtTerminoFidelizacao?: string | null; } export interface LineOption { diff --git a/src/app/services/notifications.service.ts b/src/app/services/notifications.service.ts index 07e16e8..174fbc9 100644 --- a/src/app/services/notifications.service.ts +++ b/src/app/services/notifications.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; @@ -20,6 +20,10 @@ export type NotificationDto = { cliente?: string | null; linha?: string | null; usuario?: string | null; + conta?: string | null; + planoContrato?: string | null; + dtEfetivacaoServico?: string | null; + dtTerminoFidelizacao?: string | null; }; @Injectable({ providedIn: 'root' }) @@ -38,4 +42,29 @@ export class NotificationsService { markAsRead(id: string): Observable { return this.http.patch(`${this.baseApi}/notifications/${id}/read`, {}); } + + markAllAsRead(filter?: string, notificationIds?: string[]): Observable { + let params = new HttpParams(); + if (filter) params = params.set('filter', filter); + const body = notificationIds && notificationIds.length ? { notificationIds } : {}; + return this.http.patch(`${this.baseApi}/notifications/read-all`, body, { params }); + } + + export(filter?: string, notificationIds?: string[]): Observable> { + let params = new HttpParams(); + if (filter) params = params.set('filter', filter); + if (notificationIds && notificationIds.length) { + return this.http.post(`${this.baseApi}/notifications/export`, { notificationIds }, { + params, + observe: 'response', + responseType: 'blob' + }); + } + + return this.http.get(`${this.baseApi}/notifications/export`, { + params, + observe: 'response', + responseType: 'blob' + }); + } } diff --git a/src/app/services/parcelamentos.service.ts b/src/app/services/parcelamentos.service.ts new file mode 100644 index 0000000..1a3a9b2 --- /dev/null +++ b/src/app/services/parcelamentos.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +export interface PagedResult { + page: number; + pageSize: number; + total: number; + items: T[]; +} + +export interface ParcelamentoListItem { + id: string; + anoRef?: number | null; + item?: number | null; + linha?: string | null; + cliente?: string | null; + qtParcelas?: string | null; + parcelaAtual?: number | null; + totalParcelas?: number | null; + valorCheio?: number | string | null; + desconto?: number | string | null; + valorComDesconto?: number | string | null; +} + +export interface ParcelamentoParcela { + competencia: string; + valor?: number | string | null; +} + +export interface ParcelamentoAnnualMonth { + month: number; + valor?: number | string | null; +} + +export interface ParcelamentoAnnualRow { + year: number; + total?: number | string | null; + months?: ParcelamentoAnnualMonth[]; +} + +export interface ParcelamentoMonthInput { + competencia: string; + valor?: number | string | null; +} + +export interface ParcelamentoUpsertRequest { + anoRef?: number | null; + item?: number | null; + linha?: string | null; + cliente?: string | null; + qtParcelas?: string | null; + parcelaAtual?: number | null; + totalParcelas?: number | null; + valorCheio?: number | string | null; + desconto?: number | string | null; + valorComDesconto?: number | string | null; + monthValues?: ParcelamentoMonthInput[] | null; +} + +export interface ParcelamentoDetail extends ParcelamentoListItem { + parcelasMensais?: ParcelamentoParcela[]; + annualRows?: ParcelamentoAnnualRow[]; +} + +export interface ParcelamentoDetailResponse extends ParcelamentoListItem { + parcelasMensais?: ParcelamentoParcela[]; + parcelas?: ParcelamentoParcela[]; + monthValues?: ParcelamentoParcela[]; + annualRows?: ParcelamentoAnnualRow[]; +} + +@Injectable({ providedIn: 'root' }) +export class ParcelamentosService { + private readonly baseApi: string; + + constructor(private http: HttpClient) { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + } + + list(filters: { + anoRef?: number; + linha?: string; + cliente?: string; + competenciaAno?: number; + competenciaMes?: number; + page?: number; + pageSize?: number; + }): Observable> { + let params = new HttpParams(); + if (filters.anoRef !== undefined) params = params.set('anoRef', String(filters.anoRef)); + if (filters.linha && filters.linha.trim()) params = params.set('linha', filters.linha.trim()); + if (filters.cliente && filters.cliente.trim()) params = params.set('cliente', filters.cliente.trim()); + if (filters.competenciaAno !== undefined) params = params.set('competenciaAno', String(filters.competenciaAno)); + if (filters.competenciaMes !== undefined) params = params.set('competenciaMes', String(filters.competenciaMes)); + params = params.set('page', String(filters.page ?? 1)); + params = params.set('pageSize', String(filters.pageSize ?? 10)); + + return this.http.get>(`${this.baseApi}/parcelamentos`, { params }); + } + + getById(id: string): Observable { + return this.http.get(`${this.baseApi}/parcelamentos/${id}`); + } + + create(payload: ParcelamentoUpsertRequest): Observable { + return this.http.post(`${this.baseApi}/parcelamentos`, payload); + } + + update(id: string, payload: ParcelamentoUpsertRequest): Observable { + return this.http.put(`${this.baseApi}/parcelamentos/${id}`, payload); + } + + delete(id: string): Observable { + return this.http.delete(`${this.baseApi}/parcelamentos/${id}`); + } +} diff --git a/src/app/services/plan-autofill.service.ts b/src/app/services/plan-autofill.service.ts new file mode 100644 index 0000000..2955b32 --- /dev/null +++ b/src/app/services/plan-autofill.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { ResumoService, PlanoContratoResumo, MacrophonyPlan } from './resumo.service'; + +export type PlanSuggestion = { + franquiaGb?: number | null; + valorPlano?: number | null; +}; + +@Injectable({ providedIn: 'root' }) +export class PlanAutoFillService { + private loaded = false; + private loadingPromise: Promise | null = null; + private planMap = new Map(); + private planOptions: string[] = []; + + constructor(private resumoService: ResumoService) {} + + async load(): Promise { + if (this.loaded) return; + if (this.loadingPromise) return this.loadingPromise; + + this.loadingPromise = firstValueFrom(this.resumoService.getResumo()) + .then((res) => { + const items: Array = [ + ...(res?.planoContratoResumos ?? []), + ...(res?.macrophonyPlans ?? []) + ]; + + items.forEach((row) => this.addPlanRule(row)); + this.planOptions = Array.from(new Set(this.planOptions)).sort((a, b) => + a.localeCompare(b, 'pt-BR', { sensitivity: 'base' }) + ); + + this.loaded = true; + }) + .catch(() => { + this.loaded = true; + }) + .finally(() => { + this.loadingPromise = null; + }); + + return this.loadingPromise; + } + + getPlanOptions(): string[] { + return [...this.planOptions]; + } + + suggest(planName: string | null | undefined): PlanSuggestion | null { + const plan = (planName ?? '').trim(); + if (!plan) return null; + + const key = this.normalizePlan(plan); + const fromMap = this.planMap.get(key); + + const franquia = fromMap?.franquiaGb ?? this.parseFranquiaFromPlan(plan); + const valorPlano = fromMap?.valorPlano ?? null; + + if (franquia == null && valorPlano == null) return null; + return { franquiaGb: franquia ?? null, valorPlano }; + } + + private addPlanRule(row: PlanoContratoResumo | MacrophonyPlan) { + const plano = (row?.planoContrato ?? '').toString().trim(); + if (!plano) return; + + const key = this.normalizePlan(plano); + const current = this.planMap.get(key) || {}; + + const franquia = this.toNumber((row as any).franquiaGb ?? (row as any).gb); + const valorPlano = this.toNumber((row as any).valorIndividualComSvas); + + const next: PlanSuggestion = { + franquiaGb: current.franquiaGb ?? franquia ?? null, + valorPlano: current.valorPlano ?? valorPlano ?? null + }; + + this.planMap.set(key, next); + this.planOptions.push(plano); + } + + private normalizePlan(plan: string): string { + return plan.trim().replace(/\s+/g, ' ').toUpperCase(); + } + + private toNumber(value: any): number | null { + if (value === null || value === undefined || value === '') return null; + if (typeof value === 'number') return Number.isFinite(value) ? value : null; + + const raw = String(value).trim(); + if (!raw) return null; + + const cleaned = raw.replace(/[^0-9,.-]/g, '').replace(',', '.'); + const n = parseFloat(cleaned); + return Number.isFinite(n) ? n : null; + } + + private parseFranquiaFromPlan(plan: string): number | null { + const match = plan.match(/(\d+(?:[.,]\d+)?)\s*(GB|MB)/i); + if (!match) return null; + + const raw = match[1].replace(',', '.'); + const unit = match[2].toUpperCase(); + + const value = parseFloat(raw); + if (!Number.isFinite(value)) return null; + + if (unit === 'MB') { + return Number((value / 1000).toFixed(4)); + } + + return value; + } +} diff --git a/src/app/services/profile.service.ts b/src/app/services/profile.service.ts new file mode 100644 index 0000000..3879844 --- /dev/null +++ b/src/app/services/profile.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { environment } from '../../environments/environment'; + +export type ProfileMeDto = { + id: string; + nome: string; + email: string; +}; + +export type UpdateProfilePayload = { + nome: string; + email: string; +}; + +export type ChangePasswordPayload = { + credencialAtual: string; + novaCredencial: string; + confirmarNovaCredencial: string; +}; + +@Injectable({ providedIn: 'root' }) +export class ProfileService { + private readonly baseApi: string; + + constructor(private http: HttpClient) { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + } + + getMe(): Observable { + return this.http.get(`${this.baseApi}/profile/me`); + } + + updateProfile(payload: UpdateProfilePayload): Observable { + return this.http.patch(`${this.baseApi}/profile`, payload); + } + + changePassword(payload: ChangePasswordPayload): Observable { + return this.http.post(`${this.baseApi}/profile/change-password`, payload); + } +} diff --git a/src/app/services/resumo.service.ts b/src/app/services/resumo.service.ts new file mode 100644 index 0000000..16be4b3 --- /dev/null +++ b/src/app/services/resumo.service.ts @@ -0,0 +1,129 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../environments/environment'; + +export interface MacrophonyPlan { + planoContrato?: string | null; + gb?: string | number | null; + valorIndividualComSvas?: string | number | null; + franquiaGb?: string | number | null; + totalLinhas?: number | string | null; + valorTotal?: string | number | null; + vivoTravel?: boolean | string | number | null; +} + +export interface MacrophonyTotals { + franquiaGbTotal?: string | number | null; + totalLinhasTotal?: number | string | null; + valorTotal?: string | number | null; +} + +export interface VivoLineResumo { + skil?: string | null; + cliente?: string | null; + qtdLinhas?: number | string | null; + franquiaTotal?: string | number | null; + valorContratoVivo?: string | number | null; + franquiaLine?: string | number | null; + valorContratoLine?: string | number | null; + lucro?: string | number | null; +} + +export interface VivoLineTotals { + qtdLinhasTotal?: number | string | null; + franquiaTotal?: string | number | null; + valorContratoVivo?: string | number | null; + franquiaLine?: string | number | null; + valorContratoLine?: string | number | null; + lucro?: string | number | null; +} + +export interface ClienteEspecial { + nome?: string | null; + valor?: string | number | null; +} + +export interface PlanoContratoResumo { + planoContrato?: string | null; + gb?: string | number | null; + valorIndividualComSvas?: string | number | null; + franquiaGb?: string | number | null; + totalLinhas?: number | string | null; + valorTotal?: string | number | null; +} + +export interface PlanoContratoTotal { + valorTotal?: string | number | null; +} + +export interface LineTotal { + tipo?: string | null; + valorTotalLine?: string | number | null; + lucroTotalLine?: string | number | null; + qtdLinhas?: number | string | null; +} + +export interface GbDistribuicao { + gb?: string | number | null; + qtd?: number | string | null; + soma?: string | number | null; +} + +export interface GbDistribuicaoTotal { + totalLinhas?: number | string | null; + somaTotal?: string | number | null; +} + +export interface ReservaLine { + ddd?: string | number | null; + franquiaGb?: string | number | null; + qtdLinhas?: number | string | null; + total?: string | number | null; +} + +export interface ReservaPorFranquia { + franquiaGb?: string | number | null; + totalLinhas?: number | string | null; +} + +export interface ReservaPorDdd { + ddd?: string | number | null; + totalLinhas?: number | string | null; + porFranquia?: ReservaPorFranquia[]; +} + +export interface ReservaTotal { + qtdLinhasTotal?: number | string | null; + total?: string | number | null; +} + +export interface ResumoResponse { + macrophonyPlans?: MacrophonyPlan[]; + macrophonyTotals?: MacrophonyTotals; + vivoLineResumos?: VivoLineResumo[]; + vivoLineTotals?: VivoLineTotals; + clienteEspeciais?: ClienteEspecial[]; + planoContratoResumos?: PlanoContratoResumo[]; + planoContratoTotal?: PlanoContratoTotal; + lineTotais?: LineTotal[]; + gbDistribuicao?: GbDistribuicao[]; + gbDistribuicaoTotal?: GbDistribuicaoTotal; + reservaLines?: ReservaLine[]; + reservaPorDdd?: ReservaPorDdd[]; + totalGeralLinhasReserva?: number | string | null; + reservaTotal?: ReservaTotal; +} + +@Injectable({ providedIn: 'root' }) +export class ResumoService { + private readonly apiBase: string; + + constructor(private http: HttpClient) { + const raw = (environment.apiUrl || 'https://localhost:7205').replace(/\/+$/, ''); + this.apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + } + + getResumo() { + return this.http.get(`${this.apiBase}/resumo`); + } +} diff --git a/src/app/services/session-notice.service.ts b/src/app/services/session-notice.service.ts new file mode 100644 index 0000000..5f9f9df --- /dev/null +++ b/src/app/services/session-notice.service.ts @@ -0,0 +1,110 @@ +import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { Router } from '@angular/router'; +import { AuthService } from './auth.service'; + +@Injectable({ providedIn: 'root' }) +export class SessionNoticeService { + private toastEl: HTMLElement | null = null; + private toastBodyEl: HTMLElement | null = null; + private toastHeaderEl: HTMLElement | null = null; + private handling401 = false; + private last401At = 0; + + constructor( + private authService: AuthService, + private router: Router, + @Inject(PLATFORM_ID) private platformId: object + ) {} + + async handleUnauthorized(): Promise { + if (!isPlatformBrowser(this.platformId)) return; + + const now = Date.now(); + if (this.handling401 && now - this.last401At < 3000) return; + this.handling401 = true; + this.last401At = now; + + await this.showToast('Sua sessão expirou. Faça login novamente.', 'danger'); + this.authService.logout(); + this.router.navigateByUrl('/login'); + + setTimeout(() => { + this.handling401 = false; + }, 3000); + } + + async handleForbidden(): Promise { + if (!isPlatformBrowser(this.platformId)) return; + await this.showToast('Acesso restrito.', 'warning'); + } + + private ensureToast(): void { + if (!isPlatformBrowser(this.platformId)) return; + if (this.toastEl && this.toastBodyEl && this.toastHeaderEl) return; + + const doc = document; + let container = doc.getElementById('lg-global-toast-container'); + if (!container) { + container = doc.createElement('div'); + container.id = 'lg-global-toast-container'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '10000'; + doc.body.appendChild(container); + } + + const toast = doc.createElement('div'); + toast.className = 'toast text-bg-danger border-0 shadow'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + + const header = doc.createElement('div'); + header.className = 'toast-header border-bottom-0'; + + const title = doc.createElement('strong'); + title.className = 'me-auto text-primary'; + title.textContent = 'LineGestão'; + + const closeBtn = doc.createElement('button'); + closeBtn.type = 'button'; + closeBtn.className = 'btn-close'; + closeBtn.setAttribute('data-bs-dismiss', 'toast'); + closeBtn.setAttribute('aria-label', 'Fechar'); + + header.appendChild(title); + header.appendChild(closeBtn); + + const body = doc.createElement('div'); + body.className = 'toast-body bg-white rounded-bottom text-dark'; + + toast.appendChild(header); + toast.appendChild(body); + container.appendChild(toast); + + this.toastEl = toast; + this.toastBodyEl = body; + this.toastHeaderEl = header; + } + + private async showToast(message: string, variant: 'danger' | 'warning'): Promise { + if (!isPlatformBrowser(this.platformId)) return; + this.ensureToast(); + if (!this.toastEl || !this.toastBodyEl || !this.toastHeaderEl) return; + + this.toastBodyEl.textContent = message; + this.toastEl.classList.remove('text-bg-danger', 'text-bg-warning'); + this.toastEl.classList.add(variant === 'warning' ? 'text-bg-warning' : 'text-bg-danger'); + + try { + const bs = await import('bootstrap'); + const toastInstance = bs.Toast.getOrCreateInstance(this.toastEl, { + autohide: true, + delay: 3000 + }); + toastInstance.show(); + } catch (error) { + console.error(error); + } + } +} diff --git a/src/app/services/vigencia.service.ts b/src/app/services/vigencia.service.ts index b062c74..8840fcf 100644 --- a/src/app/services/vigencia.service.ts +++ b/src/app/services/vigencia.service.ts @@ -23,8 +23,24 @@ export interface VigenciaRow { dtEfetivacaoServico: string | null; dtTerminoFidelizacao: string | null; total: number | null; + createdAt?: string | null; + updatedAt?: string | null; } +export interface UpdateVigenciaRequest { + item?: number | null; + conta?: string | null; + linha?: string | null; + cliente?: string | null; + usuario?: string | null; + planoContrato?: string | null; + dtEfetivacaoServico?: string | null; + dtTerminoFidelizacao?: string | null; + total?: number | null; +} + +export interface CreateVigenciaRequest extends UpdateVigenciaRequest {} + export interface VigenciaClientGroup { cliente: string; linhas: number; @@ -86,4 +102,20 @@ export class VigenciaService { getClients(): Observable { return this.http.get(`${this.baseApi}/lines/vigencia/clients`); } -} \ No newline at end of file + + getById(id: string): Observable { + return this.http.get(`${this.baseApi}/lines/vigencia/${id}`); + } + + update(id: string, payload: UpdateVigenciaRequest): Observable { + return this.http.put(`${this.baseApi}/lines/vigencia/${id}`, payload); + } + + create(payload: CreateVigenciaRequest): Observable { + return this.http.post(`${this.baseApi}/lines/vigencia`, payload); + } + + remove(id: string): Observable { + return this.http.delete(`${this.baseApi}/lines/vigencia/${id}`); + } +} diff --git a/src/index.html b/src/index.html index f2bb223..cc6e397 100644 --- a/src/index.html +++ b/src/index.html @@ -1,11 +1,11 @@ - + - LineGestaoFrontend + LineGestão - + diff --git a/src/main.server.ts b/src/main.server.ts index 723e001..4a25c51 100644 --- a/src/main.server.ts +++ b/src/main.server.ts @@ -1,7 +1,11 @@ import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser'; +import { registerLocaleData } from '@angular/common'; +import localePt from '@angular/common/locales/pt'; import { App } from './app/app'; import { config } from './app/app.config.server'; +registerLocaleData(localePt, 'pt-BR'); + const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); diff --git a/src/main.ts b/src/main.ts index f4c0acf..2a8b0e7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,11 @@ import { bootstrapApplication } from '@angular/platform-browser'; +import { registerLocaleData } from '@angular/common'; +import localePt from '@angular/common/locales/pt'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app'; import 'bootstrap/dist/js/bootstrap.bundle.min.js'; +registerLocaleData(localePt, 'pt-BR'); + bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); diff --git a/src/styles.scss b/src/styles.scss index 8cc0a47..f4c8f3e 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -83,7 +83,12 @@ select.form-control-sm { /* Empurra o conteúdo pra baixo do header fixo */ .app-main.has-header { + position: relative; padding-top: 84px; /* altura segura p/ header (mobile/desktop) */ + background: + radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.1), transparent 60%), + radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.06), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%); } @media (max-width: 600px) { @@ -92,6 +97,21 @@ select.form-control-sm { } } +/* Ajuste para monitores grandes: elimina o "vão" visual entre header e corpo. */ +@media (min-width: 1400px) { + .app-main.has-header { + padding-top: 72px; + } + + .container-geral, + .container-geral-responsive, + .container-fat, + .container-mureg, + .container-troca { + margin-top: 14px !important; + } +} + /* ========================================================== */ /* 🚀 GLOBAL FIX: Proporção Horizontal e Vertical */ /* ========================================================== */ @@ -143,7 +163,14 @@ select.form-control-sm { .users-page, .fat-page, .mureg-page, -.troca-page { +.troca-page, +.historico-page, +.perfil-page, +.dashboard-page, +.chips-page, +.parcelamentos-page, +.resumo-page, +.create-user-page { overflow-y: auto !important; height: auto !important; display: block !important; @@ -280,3 +307,23 @@ app-header .modal-card .btn-secondary:hover { } } +/* Remove separators inside search inputs (icon / text / clear button). */ +.input-group.search-group { + > .input-group-text, + > .form-control, + > .btn, + > .btn-clear { + border: 0 !important; + box-shadow: none !important; + background: transparent !important; + } + + > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.invalid-tooltip) { + margin-left: 0 !important; + } + + > .form-control:focus { + box-shadow: none !important; + } +} +