From 79d372d67b0d61d103fca81349adf5232324c0a1 Mon Sep 17 00:00:00 2001 From: Leon Date: Thu, 5 Mar 2026 18:30:45 -0300 Subject: [PATCH] feat: Cliente poder alterar e solicitar alteracoes --- src/app/app.routes.ts | 2 + src/app/app.ts | 1 + src/app/components/header/header.html | 3 + src/app/components/header/header.ts | 1 + src/app/pages/geral/geral.html | 46 ++- src/app/pages/geral/geral.ts | 87 ++++- .../solicitacoes-linhas.html | 138 +++++++ .../solicitacoes-linhas.scss | 368 ++++++++++++++++++ .../solicitacoes-linhas.ts | 189 +++++++++ .../services/solicitacoes-linhas.service.ts | 61 +++ 10 files changed, 881 insertions(+), 15 deletions(-) create mode 100644 src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html create mode 100644 src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss create mode 100644 src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts create mode 100644 src/app/services/solicitacoes-linhas.service.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 3c39d3d..0b219bf 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -21,6 +21,7 @@ import { Parcelamentos } from './pages/parcelamentos/parcelamentos'; import { Historico } from './pages/historico/historico'; import { Perfil } from './pages/perfil/perfil'; import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user'; +import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas'; export const routes: Routes = [ { path: '', component: Home }, @@ -38,6 +39,7 @@ export const routes: Routes = [ { path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' }, { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Parcelamentos' }, { path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' }, + { path: 'solicitacoes', component: SolicitacoesLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Solicitações' }, { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, { path: 'system/fornecer-usuario', diff --git a/src/app/app.ts b/src/app/app.ts index 11b8f61..0d6c4dc 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -29,6 +29,7 @@ export class AppComponent { // ✅ rotas internas (LOGADO) que devem esconder footer private readonly loggedPrefixes = [ '/geral', + '/solicitacoes-linhas', '/mureg', '/faturamento', '/dadosusuarios', diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index cc76c2f..4c1289c 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -548,6 +548,9 @@ Histórico + + Solicitações + Dados PF/PJ diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index 0d78ae7..ef45da7 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -93,6 +93,7 @@ export class Header implements AfterViewInit, OnDestroy { '/resumo', '/parcelamentos', '/historico', + '/solicitacoes', '/perfil', '/system', ]; diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index 6ba0955..8e06caa 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -1975,12 +1975,24 @@
- - + + + + + + +
@@ -1994,22 +2006,35 @@
-
- - -
- +
- + + +
+
+
+
+ + +
+
+ + +
@@ -2062,6 +2087,7 @@ +
diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 7e78101..bc3f3df 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -22,6 +22,7 @@ import { CustomSelectComponent } from '../../components/custom-select/custom-sel import { PlanAutoFillService } from '../../services/plan-autofill.service'; import { AuthService } from '../../services/auth.service'; import { TenantSyncService } from '../../services/tenant-sync.service'; +import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; import { firstValueFrom, Subscription, filter } from 'rxjs'; import { environment } from '../../../environments/environment'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; @@ -289,7 +290,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { private planAutoFill: PlanAutoFillService, private authService: AuthService, private router: Router, - private tenantSyncService: TenantSyncService + private tenantSyncService: TenantSyncService, + private solicitacoesLinhasService: SolicitacoesLinhasService ) {} private readonly apiBase = (() => { @@ -347,6 +349,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { financeOpen = false; editOpen = false; editSaving = false; + requestSaving = false; createOpen = false; createSaving = false; @@ -858,6 +861,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.aparelhoReciboFile = null; this.editSaving = false; + this.requestSaving = false; this.createSaving = false; this.editModel = null; @@ -1865,6 +1869,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { async onEditar(r: LineRow) { this.editOpen = true; this.editSaving = false; + this.requestSaving = false; this.editModel = null; this.aparelhoNotaFiscalFile = null; this.aparelhoReciboFile = null; @@ -1942,11 +1947,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async saveEdit() { - if (!this.editingId || !this.editModel) return; + if (!this.editingId || !this.editModel || this.requestSaving) return; this.editSaving = true; const editingId = this.editingId; const shouldUploadAttachments = !!(this.aparelhoNotaFiscalFile || this.aparelhoReciboFile); + const franquiaLineAtual = this.toNullableNumber(this.editModel.franquiaLine); + const franquiaLineSolicitada = this.toNullableNumber(this.editModel.franquiaLineSolicitada); + const shouldRequestFranquia = + this.isClientRestricted && !this.nullableNumberEquals(franquiaLineAtual, franquiaLineSolicitada); let payload: UpdateMobileLineRequest; if (this.isClientRestricted) { @@ -1954,15 +1963,21 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { item: this.toInt(this.editModel.item), usuario: (this.editModel.usuario ?? '').toString(), centroDeCustos: (this.editModel.centroDeCustos ?? '').toString(), + setorNome: (this.editModel.setorNome ?? '').toString(), aparelhoId: (this.editModel.aparelhoId ?? null) as string | null, aparelhoNome: (this.editModel.aparelhoNome ?? '').toString(), aparelhoCor: (this.editModel.aparelhoCor ?? '').toString(), - aparelhoImei: (this.editModel.aparelhoImei ?? '').toString() + aparelhoImei: (this.editModel.aparelhoImei ?? '').toString(), + franquiaLine: franquiaLineAtual }; } else { this.calculateFinancials(this.editModel); - const { contaEmpresa: _contaEmpresa, ...editModelPayload } = this.editModel; + const { + contaEmpresa: _contaEmpresa, + franquiaLineSolicitada: _franquiaLineSolicitada, + ...editModelPayload + } = this.editModel; payload = { ...editModelPayload, @@ -2004,12 +2019,48 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return; } + if (shouldRequestFranquia) { + try { + await firstValueFrom(this.solicitacoesLinhasService.create({ + lineId: editingId, + tipoSolicitacao: 'alteracao-franquia', + franquiaLineNova: franquiaLineSolicitada + })); + } catch (err) { + this.editSaving = false; + this.closeAllModals(); + const msg = + (err as HttpErrorResponse)?.error?.message + || 'Registro atualizado, mas falhou ao enviar a solicitação de franquia.'; + await this.showToast(msg); + + if (this.isGroupMode && this.expandedGroup) { + const term = (this.searchTerm ?? '').trim(); + const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined; + this.fetchGroupLines(this.expandedGroup, useTerm); + this.loadGroups(); + this.loadKpis(); + } else { + this.refreshData(); + } + return; + } + } + this.editSaving = false; // fecha e limpa overlay SEMPRE this.closeAllModals(); - await this.showToast(shouldUploadAttachments ? 'Registro e anexos atualizados!' : 'Registro atualizado!'); + if (shouldRequestFranquia) { + await this.showToast( + shouldUploadAttachments + ? 'Registro e anexos atualizados! Solicitação de franquia enviada.' + : 'Registro atualizado! Solicitação de franquia enviada.' + ); + } else { + await this.showToast(shouldUploadAttachments ? 'Registro e anexos atualizados!' : 'Registro atualizado!'); + } if (this.isGroupMode && this.expandedGroup) { const term = (this.searchTerm ?? '').trim(); @@ -2029,6 +2080,26 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { }); } + async requestLineBlock() { + if (!this.editingId || this.requestSaving) return; + + this.requestSaving = true; + try { + await firstValueFrom(this.solicitacoesLinhasService.create({ + lineId: this.editingId, + tipoSolicitacao: 'bloqueio' + })); + + this.requestSaving = false; + this.closeAllModals(); + await this.showToast('Solicitação de bloqueio enviada.'); + } catch (err) { + this.requestSaving = false; + const msg = (err as HttpErrorResponse)?.error?.message || 'Erro ao enviar solicitação de bloqueio.'; + await this.showToast(msg); + } + } + onAparelhoNotaFiscalSelected(event: Event) { const input = event.target as HTMLInputElement | null; this.aparelhoNotaFiscalFile = input?.files && input.files.length > 0 ? input.files[0] : null; @@ -3618,6 +3689,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { valorContratoVivo: d.valorContratoVivo ?? null, franquiaLine: d.franquiaLine ?? null, + franquiaLineSolicitada: d.franquiaLine ?? null, franquiaGestao: d.franquiaGestao ?? null, locacaoAp: d.locacaoAp ?? null, valorContratoLine: d.valorContratoLine ?? null, @@ -3660,6 +3732,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return Number.isNaN(n) ? null : n; } + private nullableNumberEquals(a: number | null, b: number | null): boolean { + if (a === null || b === null) return a === b; + return Math.abs(a - b) < 0.000001; + } + private mergeOption(current: any, list: string[]): string[] { const v = (current ?? '').toString().trim(); if (!v) return list; diff --git a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html new file mode 100644 index 0000000..73098c7 --- /dev/null +++ b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.html @@ -0,0 +1,138 @@ +
+
+
+
+
+
+ Administração +
+ +
+
Solicitações
+ Pedidos de alteração de franquia e bloqueio enviados pelos usuários. +
+ +
+ +
+
+ +
+
+ + + + + +
+ +
+ + Itens por pág: + +
+ +
+
+ +
+ Mostrando {{ pageStart }}-{{ pageEnd }} de {{ total }} +
+
+
+ +
+
+
+ +
+ +
+ {{ errorMsg }} +
+ +
+ Nenhuma solicitação encontrada. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DATACLIENTELINHAUSUARIO LINHATIPOFRANQUIA ANTESFRANQUIA DEPOISDESCRICAO
+
+ {{ formatDate(item.createdAt) }} + {{ formatTime(item.createdAt) }} +
+
{{ item.tenantNome || '-' }}{{ item.linha || '-' }}{{ item.usuarioLinha || '-' }} + {{ tipoLabel(item.tipoSolicitacao) }} + {{ formatFranquia(item.franquiaLineAtual) }}{{ formatFranquia(item.franquiaLineNova) }} + {{ descricao(item) }} +
+
+
+ + +
+
+
diff --git a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss new file mode 100644 index 0000000..f4e16fb --- /dev/null +++ b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.scss @@ -0,0 +1,368 @@ +:host { + --brand: #e33dcf; + --text: #111214; + --card-bg: rgba(255, 255, 255, 0.88); + --card-border: 1px solid rgba(227, 61, 207, 0.14); + --stroke: rgba(16, 24, 40, 0.08); + --soft: rgba(17, 18, 20, 0.64); + + display: block; + color: var(--text); +} + +.solicitacoes-page { + min-height: 100vh; + padding: 0 0 18px; + background: + radial-gradient(920px 420px at 15% 8%, rgba(227, 61, 207, 0.1), transparent 60%), + radial-gradient(860px 420px at 85% 20%, rgba(3, 15, 170, 0.07), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f6fb 70%); +} + +.container-geral-responsive { + width: calc(100vw - 2px); + max-width: none; + margin: 16px auto 24px; + position: relative; + z-index: 1; +} + +.geral-card { + border-radius: 22px; + overflow: hidden; + background: var(--card-bg); + border: var(--card-border); + backdrop-filter: blur(8px); + box-shadow: 0 18px 40px rgba(17, 18, 20, 0.1); + display: flex; + flex-direction: column; + min-height: 64vh; +} + +.geral-header { + padding: 16px 22px 14px; + border-bottom: 1px solid var(--stroke); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.05), rgba(255, 255, 255, 0.16)); +} + +.header-row-top { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 12px; + align-items: center; +} + +.title-badge { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 12px; + border-radius: 999px; + border: 1px solid rgba(227, 61, 207, 0.2); + background: rgba(255, 255, 255, 0.8); + font-weight: 800; + font-size: 12px; +} + +.header-title { + justify-self: center; + text-align: center; +} + +.title { + font-size: 24px; + font-weight: 900; + letter-spacing: -0.01em; +} + +.subtitle { + color: var(--soft); + font-weight: 600; +} + +.header-actions { + justify-self: end; +} + +.btn-brand { + background: var(--brand); + border-color: var(--brand); + color: #fff; + font-weight: 800; +} + +.controls { + display: grid; + grid-template-columns: minmax(320px, 1.3fr) auto auto; + align-items: center; + gap: 14px; +} + +.search-group { + max-width: 700px; + width: 100%; + border-radius: 13px; + border: 1px solid rgba(16, 24, 40, 0.16); + overflow: hidden; + box-shadow: 0 4px 10px rgba(16, 24, 40, 0.05); + background: #fff; + + .input-group-text, + .form-control, + .btn-clear { + border: none; + } + + .input-group-text { + color: rgba(17, 18, 20, 0.55); + } + + .form-control { + font-size: 13px; + } +} + +.geral-body { + flex: 1; + padding: 0 4px 10px; + overflow: hidden; +} + +.table-wrap { + width: 100%; + min-width: 0; + max-width: 100%; + min-height: clamp(260px, 44vh, 500px); + max-height: 60vh; + border: 1px solid rgba(16, 24, 40, 0.1); + border-radius: 14px; + overflow-x: hidden; + overflow-y: auto; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 251, 255, 0.96) 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); +} + +.table-modern { + width: 100%; + min-width: 0; + table-layout: fixed; +} + +.table-modern col.col-date { width: 9%; } +.table-modern col.col-cliente { width: 14%; } +.table-modern col.col-linha { width: 10%; } +.table-modern col.col-usuario { width: 15%; } +.table-modern col.col-tipo { width: 14%; } +.table-modern col.col-franquia { width: 12%; } +.table-modern col.col-descricao { width: 14%; } + +.table-modern thead th { + position: sticky; + top: 0; + z-index: 2; + background: #f6f8fc; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgba(17, 18, 20, 0.74); + padding: 10px 12px; + border-bottom: 1px solid #e4e9f3; + white-space: normal; + word-break: break-word; + line-height: 1.2; +} + +.table-modern thead th:nth-child(6), +.table-modern thead th:nth-child(7), +.table-modern tbody td:nth-child(6), +.table-modern tbody td:nth-child(7) { + min-width: 0; + text-align: center; +} + +.table-modern tbody td { + font-size: 13px; + padding: 11px 12px; + border-bottom: 1px solid #ebeff6; + vertical-align: middle; + white-space: normal; + word-break: break-word; + color: #1a1c20; +} + +.table-modern tbody tr:nth-child(even) { + background: rgba(243, 247, 253, 0.56); +} + +.table-modern tbody tr:hover { + background: #eef5ff; +} + +.date-cell { + display: flex; + flex-direction: column; + line-height: 1.15; + gap: 3px; +} + +.date-main { + font-weight: 700; + color: #1a1c20; +} + +.date-sub { + font-size: 11px; + color: rgba(17, 18, 20, 0.55); +} + +.mono-cell { + font-family: "JetBrains Mono", "Consolas", "Monaco", monospace; + font-variant-numeric: tabular-nums; + font-size: 12px; + font-weight: 700; + white-space: normal; + word-break: break-all; +} + +.cell-ellipsis { + overflow: visible; + text-overflow: unset; + white-space: normal; +} + +.type-badge { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 5px 10px; + font-size: 11px; + font-weight: 800; + line-height: 1; + border: 1px solid transparent; + white-space: nowrap; +} + +.type-badge--franquia { + color: #1e4e8f; + background: rgba(44, 121, 232, 0.14); + border-color: rgba(44, 121, 232, 0.24); +} + +.type-badge--bloqueio { + color: #8f2f2f; + background: rgba(221, 74, 74, 0.14); + border-color: rgba(221, 74, 74, 0.25); +} + +.type-badge--default { + color: #5b6173; + background: rgba(118, 127, 154, 0.14); + border-color: rgba(118, 127, 154, 0.24); +} + +.franquia-cell { + text-align: center; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.message-cell { + white-space: normal; +} + +.message-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + line-height: 1.32; + color: rgba(17, 18, 20, 0.8); + word-break: break-word; +} + +.table-summary { + justify-self: end; + font-size: 12px; + color: rgba(17, 18, 20, 0.64); + font-weight: 600; + + strong { + color: #111214; + font-weight: 800; + } +} + +.empty-group { + padding: 36px 18px; + text-align: center; + color: rgba(17, 18, 20, 0.64); +} + +.geral-footer { + display: none; +} + +.table-wrap::-webkit-scrollbar { + height: 10px; + width: 10px; +} + +.table-wrap::-webkit-scrollbar-track { + background: rgba(17, 18, 20, 0.06); +} + +.table-wrap::-webkit-scrollbar-thumb { + background: rgba(103, 114, 143, 0.45); + border-radius: 999px; +} + +@media (max-width: 1200px) { + .controls { + grid-template-columns: 1fr auto; + } + + .table-summary { + grid-column: 1 / -1; + justify-self: start; + } + + .table-modern col.col-date { width: 10%; } + .table-modern col.col-cliente { width: 13%; } + .table-modern col.col-linha { width: 9%; } + .table-modern col.col-usuario { width: 14%; } + .table-modern col.col-tipo { width: 14%; } + .table-modern col.col-franquia { width: 13%; } + .table-modern col.col-descricao { width: 14%; } + + .table-modern thead th, + .table-modern tbody td { + padding: 9px 8px; + font-size: 11px; + } +} + +@media (max-width: 992px) { + .header-row-top { + grid-template-columns: 1fr; + text-align: center; + } + + .title-badge, + .header-title, + .header-actions { + justify-self: center; + } + + .controls { + grid-template-columns: 1fr; + align-items: start; + } + + .message-text { + -webkit-line-clamp: 3; + } +} diff --git a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts new file mode 100644 index 0000000..a52be9f --- /dev/null +++ b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts @@ -0,0 +1,189 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { SolicitacaoLinhaDto, SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; + +@Component({ + selector: 'app-solicitacoes-linhas', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './solicitacoes-linhas.html', + styleUrls: ['./solicitacoes-linhas.scss'], +}) +export class SolicitacoesLinhas implements OnInit, OnDestroy { + items: SolicitacaoLinhaDto[] = []; + loading = false; + errorMsg = ''; + + page = 1; + pageSize = 20; + pageSizeOptions = [10, 20, 50, 100]; + total = 0; + + search = ''; + private searchTimer: ReturnType | null = null; + + constructor(private readonly solicitacoesService: SolicitacoesLinhasService) {} + + ngOnInit(): void { + this.fetch(1); + } + + ngOnDestroy(): void { + if (this.searchTimer) { + clearTimeout(this.searchTimer); + this.searchTimer = null; + } + } + + refresh(): void { + this.fetch(); + } + + onSearchChange(): void { + if (this.searchTimer) { + clearTimeout(this.searchTimer); + } + + this.searchTimer = setTimeout(() => { + this.page = 1; + this.fetch(); + }, 300); + } + + clearSearch(): void { + this.search = ''; + this.page = 1; + this.fetch(); + } + + onPageSizeChange(): void { + this.page = 1; + this.fetch(); + } + + goToPage(pageNumber: number): void { + this.page = Math.max(1, Math.min(this.totalPages, pageNumber)); + 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); + } + + private parseDate(value?: string | null): Date | null { + if (!value) return null; + const d = new Date(value); + return Number.isNaN(d.getTime()) ? null : d; + } + + formatDateTime(value?: string | null): string { + const d = this.parseDate(value); + if (!d) return '-'; + return d.toLocaleString('pt-BR'); + } + + formatDate(value?: string | null): string { + const d = this.parseDate(value); + if (!d) return '-'; + return d.toLocaleDateString('pt-BR'); + } + + formatTime(value?: string | null): string { + const d = this.parseDate(value); + if (!d) return '--:--'; + return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); + } + + formatFranquiaValor(value?: number | null): string { + if (value === null || value === undefined) return '-'; + return new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(value); + } + + formatFranquia(value?: number | null): string { + const formatted = this.formatFranquiaValor(value); + return formatted === '-' ? '-' : `${formatted} GB`; + } + + tipoLabel(value?: string | null): string { + const v = (value ?? '').toString().trim().toUpperCase(); + if (v === 'ALTERACAO_FRANQUIA') return 'Alteração de franquia'; + if (v === 'BLOQUEIO') return 'Bloqueio'; + return v || '-'; + } + + tipoBadgeClass(value?: string | null): string { + const v = (value ?? '').toString().trim().toUpperCase(); + if (v === 'ALTERACAO_FRANQUIA') return 'type-badge type-badge--franquia'; + if (v === 'BLOQUEIO') return 'type-badge type-badge--bloqueio'; + return 'type-badge type-badge--default'; + } + + trackBySolicitacao(_: number, item: SolicitacaoLinhaDto): string { + return item.id; + } + + descricao(item: SolicitacaoLinhaDto): string { + const tipo = (item.tipoSolicitacao ?? '').toString().trim().toUpperCase(); + const linha = (item.linha ?? '').toString().trim(); + + if (tipo === 'ALTERACAO_FRANQUIA') { + return `Mudanca de franquia de ${this.formatFranquiaValor(item.franquiaLineAtual)} para ${this.formatFranquiaValor(item.franquiaLineNova)}`; + } + + if (tipo === 'BLOQUEIO') { + return `Bloqueio da linha ${linha || '-'}`; + } + + return (item.mensagem ?? '').toString().trim() || '-'; + } + + private fetch(goToPage?: number): void { + if (goToPage) this.page = goToPage; + this.loading = true; + this.errorMsg = ''; + + this.solicitacoesService + .list({ + page: this.page, + pageSize: this.pageSize, + search: this.search?.trim() || undefined, + }) + .subscribe({ + next: (res) => { + this.items = res.items || []; + this.total = res.total || 0; + this.page = res.page || this.page; + this.pageSize = res.pageSize || this.pageSize; + this.loading = false; + }, + error: () => { + this.loading = false; + this.errorMsg = 'Não foi possível carregar as solicitações.'; + }, + }); + } +} diff --git a/src/app/services/solicitacoes-linhas.service.ts b/src/app/services/solicitacoes-linhas.service.ts new file mode 100644 index 0000000..d3982ff --- /dev/null +++ b/src/app/services/solicitacoes-linhas.service.ts @@ -0,0 +1,61 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +export interface PagedResult { + page: number; + pageSize: number; + total: number; + items: T[]; +} + +export interface SolicitacaoLinhaDto { + id: string; + tenantId: string; + tenantNome?: string | null; + mobileLineId?: string | null; + linha?: string | null; + usuarioLinha?: string | null; + tipoSolicitacao: string; + franquiaLineAtual?: number | null; + franquiaLineNova?: number | null; + solicitanteNome?: string | null; + mensagem: string; + status: string; + createdAt: string; +} + +export interface SolicitacaoLinhaCreatePayload { + lineId: string; + tipoSolicitacao: 'alteracao-franquia' | 'bloqueio'; + franquiaLineNova?: number | null; +} + +@Injectable({ providedIn: 'root' }) +export class SolicitacoesLinhasService { + private readonly baseUrl: string; + + constructor(private readonly http: HttpClient) { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseUrl = `${apiBase}/solicitacoes-linhas`; + } + + list(params?: { page?: number; pageSize?: number; search?: string }): Observable> { + let httpParams = new HttpParams() + .set('page', String(params?.page ?? 1)) + .set('pageSize', String(params?.pageSize ?? 20)); + + const search = (params?.search ?? '').trim(); + if (search) { + httpParams = httpParams.set('search', search); + } + + return this.http.get>(this.baseUrl, { params: httpParams }); + } + + create(payload: SolicitacaoLinhaCreatePayload): Observable { + return this.http.post(this.baseUrl, payload); + } +}