feat: Cliente poder alterar e solicitar alteracoes
This commit is contained in:
parent
5d0dc3b367
commit
79d372d67b
|
|
@ -21,6 +21,7 @@ import { Parcelamentos } from './pages/parcelamentos/parcelamentos';
|
||||||
import { Historico } from './pages/historico/historico';
|
import { Historico } from './pages/historico/historico';
|
||||||
import { Perfil } from './pages/perfil/perfil';
|
import { Perfil } from './pages/perfil/perfil';
|
||||||
import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user';
|
import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user';
|
||||||
|
import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: Home },
|
{ path: '', component: Home },
|
||||||
|
|
@ -38,6 +39,7 @@ export const routes: Routes = [
|
||||||
{ path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' },
|
{ path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' },
|
||||||
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Parcelamentos' },
|
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Parcelamentos' },
|
||||||
{ path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' },
|
{ 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: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' },
|
||||||
{
|
{
|
||||||
path: 'system/fornecer-usuario',
|
path: 'system/fornecer-usuario',
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export class AppComponent {
|
||||||
// ✅ rotas internas (LOGADO) que devem esconder footer
|
// ✅ rotas internas (LOGADO) que devem esconder footer
|
||||||
private readonly loggedPrefixes = [
|
private readonly loggedPrefixes = [
|
||||||
'/geral',
|
'/geral',
|
||||||
|
'/solicitacoes-linhas',
|
||||||
'/mureg',
|
'/mureg',
|
||||||
'/faturamento',
|
'/faturamento',
|
||||||
'/dadosusuarios',
|
'/dadosusuarios',
|
||||||
|
|
|
||||||
|
|
@ -548,6 +548,9 @@
|
||||||
<a *ngIf="canViewAll" routerLink="/historico" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="canViewAll" routerLink="/historico" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-clock-history"></i> <span>Histórico</span>
|
<i class="bi bi-clock-history"></i> <span>Histórico</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a *ngIf="canViewAll" routerLink="/solicitacoes" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
|
<i class="bi bi-envelope-paper"></i> <span>Solicitações</span>
|
||||||
|
</a>
|
||||||
<a *ngIf="canViewAll" routerLink="/dadosusuarios" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="canViewAll" routerLink="/dadosusuarios" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-people-fill"></i> <span>Dados PF/PJ</span>
|
<i class="bi bi-people-fill"></i> <span>Dados PF/PJ</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
'/resumo',
|
'/resumo',
|
||||||
'/parcelamentos',
|
'/parcelamentos',
|
||||||
'/historico',
|
'/historico',
|
||||||
|
'/solicitacoes',
|
||||||
'/perfil',
|
'/perfil',
|
||||||
'/system',
|
'/system',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1975,12 +1975,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeAllModals()" [disabled]="editSaving">
|
<button class="btn btn-glass btn-sm" (click)="closeAllModals()" [disabled]="editSaving || requestSaving">
|
||||||
<i class="bi bi-x-lg me-1"></i> Cancelar
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving">
|
<ng-container *ngIf="isClientRestricted; else adminEditActions">
|
||||||
Salvar
|
<button type="button" class="btn btn-outline-danger btn-sm" (click)="requestLineBlock()" [disabled]="editSaving || requestSaving">
|
||||||
</button>
|
<span *ngIf="!requestSaving"><i class="bi bi-lock me-1"></i> Bloquear Linha</span>
|
||||||
|
<span *ngIf="requestSaving"><span class="spinner-border spinner-border-sm me-2"></span> Enviando...</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving || requestSaving">
|
||||||
|
<span *ngIf="!editSaving">Salvar</span>
|
||||||
|
<span *ngIf="editSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #adminEditActions>
|
||||||
|
<button class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving">
|
||||||
|
Salvar
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1994,22 +2006,35 @@
|
||||||
</summary>
|
</summary>
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field">
|
|
||||||
<label>Item</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" disabled />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>Linha</label>
|
<label>Linha</label>
|
||||||
<input class="form-control form-control-sm bg-light" [(ngModel)]="editModel.linha" disabled />
|
<input class="form-control form-control-sm bg-light" [(ngModel)]="editModel.linha" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>Usuário</label>
|
<label>Cliente</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" [(ngModel)]="editModel.cliente" readonly />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Usuário da Linha</label>
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.usuario" />
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.usuario" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>Centro de Custos</label>
|
<label>Centro de Custos</label>
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.centroDeCustos" />
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.centroDeCustos" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Setor</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.setorNome" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Franquia Line (GB)</label>
|
||||||
|
<input
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
[(ngModel)]="editModel.franquiaLineSolicitada"
|
||||||
|
[disabled]="editSaving || requestSaving" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -2062,6 +2087,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-sections" *ngIf="!isClientRestricted">
|
<div class="edit-sections" *ngIf="!isClientRestricted">
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { CustomSelectComponent } from '../../components/custom-select/custom-sel
|
||||||
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { TenantSyncService } from '../../services/tenant-sync.service';
|
import { TenantSyncService } from '../../services/tenant-sync.service';
|
||||||
|
import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service';
|
||||||
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
|
|
@ -289,7 +290,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private planAutoFill: PlanAutoFillService,
|
private planAutoFill: PlanAutoFillService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private tenantSyncService: TenantSyncService
|
private tenantSyncService: TenantSyncService,
|
||||||
|
private solicitacoesLinhasService: SolicitacoesLinhasService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly apiBase = (() => {
|
private readonly apiBase = (() => {
|
||||||
|
|
@ -347,6 +349,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
financeOpen = false;
|
financeOpen = false;
|
||||||
editOpen = false;
|
editOpen = false;
|
||||||
editSaving = false;
|
editSaving = false;
|
||||||
|
requestSaving = false;
|
||||||
|
|
||||||
createOpen = false;
|
createOpen = false;
|
||||||
createSaving = false;
|
createSaving = false;
|
||||||
|
|
@ -858,6 +861,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.aparelhoReciboFile = null;
|
this.aparelhoReciboFile = null;
|
||||||
|
|
||||||
this.editSaving = false;
|
this.editSaving = false;
|
||||||
|
this.requestSaving = false;
|
||||||
this.createSaving = false;
|
this.createSaving = false;
|
||||||
|
|
||||||
this.editModel = null;
|
this.editModel = null;
|
||||||
|
|
@ -1865,6 +1869,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
async onEditar(r: LineRow) {
|
async onEditar(r: LineRow) {
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
this.editSaving = false;
|
this.editSaving = false;
|
||||||
|
this.requestSaving = false;
|
||||||
this.editModel = null;
|
this.editModel = null;
|
||||||
this.aparelhoNotaFiscalFile = null;
|
this.aparelhoNotaFiscalFile = null;
|
||||||
this.aparelhoReciboFile = null;
|
this.aparelhoReciboFile = null;
|
||||||
|
|
@ -1942,11 +1947,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveEdit() {
|
async saveEdit() {
|
||||||
if (!this.editingId || !this.editModel) return;
|
if (!this.editingId || !this.editModel || this.requestSaving) return;
|
||||||
|
|
||||||
this.editSaving = true;
|
this.editSaving = true;
|
||||||
const editingId = this.editingId;
|
const editingId = this.editingId;
|
||||||
const shouldUploadAttachments = !!(this.aparelhoNotaFiscalFile || this.aparelhoReciboFile);
|
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;
|
let payload: UpdateMobileLineRequest;
|
||||||
|
|
||||||
if (this.isClientRestricted) {
|
if (this.isClientRestricted) {
|
||||||
|
|
@ -1954,15 +1963,21 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
item: this.toInt(this.editModel.item),
|
item: this.toInt(this.editModel.item),
|
||||||
usuario: (this.editModel.usuario ?? '').toString(),
|
usuario: (this.editModel.usuario ?? '').toString(),
|
||||||
centroDeCustos: (this.editModel.centroDeCustos ?? '').toString(),
|
centroDeCustos: (this.editModel.centroDeCustos ?? '').toString(),
|
||||||
|
setorNome: (this.editModel.setorNome ?? '').toString(),
|
||||||
aparelhoId: (this.editModel.aparelhoId ?? null) as string | null,
|
aparelhoId: (this.editModel.aparelhoId ?? null) as string | null,
|
||||||
aparelhoNome: (this.editModel.aparelhoNome ?? '').toString(),
|
aparelhoNome: (this.editModel.aparelhoNome ?? '').toString(),
|
||||||
aparelhoCor: (this.editModel.aparelhoCor ?? '').toString(),
|
aparelhoCor: (this.editModel.aparelhoCor ?? '').toString(),
|
||||||
aparelhoImei: (this.editModel.aparelhoImei ?? '').toString()
|
aparelhoImei: (this.editModel.aparelhoImei ?? '').toString(),
|
||||||
|
franquiaLine: franquiaLineAtual
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.calculateFinancials(this.editModel);
|
this.calculateFinancials(this.editModel);
|
||||||
|
|
||||||
const { contaEmpresa: _contaEmpresa, ...editModelPayload } = this.editModel;
|
const {
|
||||||
|
contaEmpresa: _contaEmpresa,
|
||||||
|
franquiaLineSolicitada: _franquiaLineSolicitada,
|
||||||
|
...editModelPayload
|
||||||
|
} = this.editModel;
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
...editModelPayload,
|
...editModelPayload,
|
||||||
|
|
@ -2004,12 +2019,48 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return;
|
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;
|
this.editSaving = false;
|
||||||
|
|
||||||
// fecha e limpa overlay SEMPRE
|
// fecha e limpa overlay SEMPRE
|
||||||
this.closeAllModals();
|
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) {
|
if (this.isGroupMode && this.expandedGroup) {
|
||||||
const term = (this.searchTerm ?? '').trim();
|
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) {
|
onAparelhoNotaFiscalSelected(event: Event) {
|
||||||
const input = event.target as HTMLInputElement | null;
|
const input = event.target as HTMLInputElement | null;
|
||||||
this.aparelhoNotaFiscalFile = input?.files && input.files.length > 0 ? input.files[0] : 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,
|
valorContratoVivo: d.valorContratoVivo ?? null,
|
||||||
|
|
||||||
franquiaLine: d.franquiaLine ?? null,
|
franquiaLine: d.franquiaLine ?? null,
|
||||||
|
franquiaLineSolicitada: d.franquiaLine ?? null,
|
||||||
franquiaGestao: d.franquiaGestao ?? null,
|
franquiaGestao: d.franquiaGestao ?? null,
|
||||||
locacaoAp: d.locacaoAp ?? null,
|
locacaoAp: d.locacaoAp ?? null,
|
||||||
valorContratoLine: d.valorContratoLine ?? null,
|
valorContratoLine: d.valorContratoLine ?? null,
|
||||||
|
|
@ -3660,6 +3732,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return Number.isNaN(n) ? null : n;
|
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[] {
|
private mergeOption(current: any, list: string[]): string[] {
|
||||||
const v = (current ?? '').toString().trim();
|
const v = (current ?? '').toString().trim();
|
||||||
if (!v) return list;
|
if (!v) return list;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
<section class="solicitacoes-page">
|
||||||
|
<div class="container-geral-responsive">
|
||||||
|
<div class="geral-card">
|
||||||
|
<div class="geral-header">
|
||||||
|
<div class="header-row-top">
|
||||||
|
<div class="title-badge">
|
||||||
|
<i class="bi bi-envelope-paper"></i> Administração
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-title">
|
||||||
|
<h5 class="title mb-0">Solicitações</h5>
|
||||||
|
<small class="subtitle">Pedidos de alteração de franquia e bloqueio enviados pelos usuários.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions d-flex gap-2 justify-content-end">
|
||||||
|
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls mt-3">
|
||||||
|
<div class="input-group input-group-sm search-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi" [class.bi-search]="!loading" [class.bi-hourglass-split]="loading"></i>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Pesquisar por linha, usuário da linha ou descrição..."
|
||||||
|
[(ngModel)]="search"
|
||||||
|
(ngModelChange)="onSearchChange()" />
|
||||||
|
<button class="btn btn-outline-secondary btn-clear" type="button" (click)="clearSearch()" *ngIf="search">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-size d-flex align-items-center gap-2">
|
||||||
|
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">
|
||||||
|
Itens por pág:
|
||||||
|
</span>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<app-select
|
||||||
|
class="select-glass"
|
||||||
|
size="sm"
|
||||||
|
[options]="pageSizeOptions"
|
||||||
|
[(ngModel)]="pageSize"
|
||||||
|
(ngModelChange)="onPageSizeChange()"
|
||||||
|
[disabled]="loading"></app-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-summary" *ngIf="!loading && !errorMsg">
|
||||||
|
Mostrando <strong>{{ pageStart }}</strong>-<strong>{{ pageEnd }}</strong> de <strong>{{ total }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="geral-body">
|
||||||
|
<div class="table-wrap">
|
||||||
|
<div class="text-center p-5" *ngIf="loading">
|
||||||
|
<span class="spinner-border text-brand"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-danger m-4" *ngIf="!loading && errorMsg">
|
||||||
|
{{ errorMsg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="empty-group" *ngIf="!loading && !errorMsg && items.length === 0">
|
||||||
|
Nenhuma solicitação encontrada.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-modern align-middle mb-0" *ngIf="!loading && !errorMsg && items.length > 0">
|
||||||
|
<colgroup>
|
||||||
|
<col class="col-date" />
|
||||||
|
<col class="col-cliente" />
|
||||||
|
<col class="col-linha" />
|
||||||
|
<col class="col-usuario" />
|
||||||
|
<col class="col-tipo" />
|
||||||
|
<col class="col-franquia" />
|
||||||
|
<col class="col-franquia" />
|
||||||
|
<col class="col-descricao" />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>DATA</th>
|
||||||
|
<th>CLIENTE</th>
|
||||||
|
<th>LINHA</th>
|
||||||
|
<th>USUARIO LINHA</th>
|
||||||
|
<th>TIPO</th>
|
||||||
|
<th>FRANQUIA ANTES</th>
|
||||||
|
<th>FRANQUIA DEPOIS</th>
|
||||||
|
<th>DESCRICAO</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let item of items; trackBy: trackBySolicitacao">
|
||||||
|
<td>
|
||||||
|
<div class="date-cell">
|
||||||
|
<span class="date-main">{{ formatDate(item.createdAt) }}</span>
|
||||||
|
<span class="date-sub">{{ formatTime(item.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="cell-ellipsis" [title]="item.tenantNome || '-'">{{ item.tenantNome || '-' }}</td>
|
||||||
|
<td class="mono-cell">{{ item.linha || '-' }}</td>
|
||||||
|
<td class="cell-ellipsis" [title]="item.usuarioLinha || '-'">{{ item.usuarioLinha || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<span [class]="tipoBadgeClass(item.tipoSolicitacao)">{{ tipoLabel(item.tipoSolicitacao) }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="franquia-cell">{{ formatFranquia(item.franquiaLineAtual) }}</td>
|
||||||
|
<td class="franquia-cell">{{ formatFranquia(item.franquiaLineNova) }}</td>
|
||||||
|
<td class="message-cell" [title]="descricao(item)">
|
||||||
|
<span class="message-text">{{ descricao(item) }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="geral-footer">
|
||||||
|
<div class="small text-muted fw-bold">Mostrando {{ pageStart }}–{{ pageEnd }} de {{ total }} registros</div>
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
||||||
|
<li class="page-item" [class.disabled]="page === 1 || loading">
|
||||||
|
<button class="page-link" (click)="goToPage(page - 1)">Anterior</button>
|
||||||
|
</li>
|
||||||
|
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
||||||
|
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
||||||
|
</li>
|
||||||
|
<li class="page-item" [class.disabled]="page === totalPages || loading">
|
||||||
|
<button class="page-link" (click)="goToPage(page + 1)">Próxima</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<typeof setTimeout> | 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.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<T> {
|
||||||
|
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<PagedResult<SolicitacaoLinhaDto>> {
|
||||||
|
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<PagedResult<SolicitacaoLinhaDto>>(this.baseUrl, { params: httpParams });
|
||||||
|
}
|
||||||
|
|
||||||
|
create(payload: SolicitacaoLinhaCreatePayload): Observable<SolicitacaoLinhaDto> {
|
||||||
|
return this.http.post<SolicitacaoLinhaDto>(this.baseUrl, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue