feat: Estilizando tela de cliente e imputs
This commit is contained in:
parent
3f5c55162e
commit
59c2cb828e
|
|
@ -11,10 +11,31 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="app-select-panel" *ngIf="isOpen">
|
<div class="app-select-panel" *ngIf="isOpen">
|
||||||
|
<div class="app-select-search" *ngIf="searchable" (click)="$event.stopPropagation()">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="app-select-search-input"
|
||||||
|
[value]="searchTerm"
|
||||||
|
[placeholder]="searchPlaceholder"
|
||||||
|
(input)="onSearchInput($any($event.target).value)"
|
||||||
|
(keydown)="onSearchKeydown($event)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
*ngIf="searchTerm"
|
||||||
|
type="button"
|
||||||
|
class="app-select-search-clear"
|
||||||
|
(click)="clearSearch($event)"
|
||||||
|
aria-label="Limpar pesquisa"
|
||||||
|
>
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="app-select-option"
|
class="app-select-option"
|
||||||
*ngFor="let opt of options; trackBy: trackByValue"
|
*ngFor="let opt of filteredOptions; trackBy: trackByValue"
|
||||||
[class.selected]="isSelected(opt)"
|
[class.selected]="isSelected(opt)"
|
||||||
(click)="selectOption(opt)"
|
(click)="selectOption(opt)"
|
||||||
>
|
>
|
||||||
|
|
@ -22,7 +43,7 @@
|
||||||
<i class="bi bi-check2" *ngIf="isSelected(opt)"></i>
|
<i class="bi bi-check2" *ngIf="isSelected(opt)"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="app-select-empty" *ngIf="!options || options.length === 0">
|
<div class="app-select-empty" *ngIf="!filteredOptions || filteredOptions.length === 0">
|
||||||
Nenhuma opção
|
Nenhuma opção
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,59 @@
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-select-search {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select-search-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0f172a;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select-search-clear {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: #94a3b8;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(148, 163, 184, 0.2);
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.app-select-option {
|
.app-select-option {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,12 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
@Input() valueKey = 'value';
|
@Input() valueKey = 'value';
|
||||||
@Input() size: 'sm' | 'md' = 'md';
|
@Input() size: 'sm' | 'md' = 'md';
|
||||||
@Input() disabled = false;
|
@Input() disabled = false;
|
||||||
|
@Input() searchable = false;
|
||||||
|
@Input() searchPlaceholder = 'Pesquisar...';
|
||||||
|
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
value: any = null;
|
value: any = null;
|
||||||
|
searchTerm = '';
|
||||||
|
|
||||||
private onChange: (value: any) => void = () => {};
|
private onChange: (value: any) => void = () => {};
|
||||||
private onTouched: () => void = () => {};
|
private onTouched: () => void = () => {};
|
||||||
|
|
@ -63,10 +66,12 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.isOpen = !this.isOpen;
|
this.isOpen = !this.isOpen;
|
||||||
|
if (!this.isOpen) this.searchTerm = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
|
this.searchTerm = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOption(option: any): void {
|
selectOption(option: any): void {
|
||||||
|
|
@ -84,6 +89,26 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
|
|
||||||
trackByValue = (_: number, option: any) => this.getOptionValue(option);
|
trackByValue = (_: number, option: any) => this.getOptionValue(option);
|
||||||
|
|
||||||
|
get filteredOptions(): any[] {
|
||||||
|
const opts = this.options || [];
|
||||||
|
const term = this.normalizeText(this.searchTerm);
|
||||||
|
if (!this.searchable || !term) return opts;
|
||||||
|
return opts.filter((opt) => this.normalizeText(this.getOptionLabel(opt)).includes(term));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchInput(value: string): void {
|
||||||
|
this.searchTerm = value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearch(event?: Event): void {
|
||||||
|
event?.stopPropagation();
|
||||||
|
this.searchTerm = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchKeydown(event: KeyboardEvent): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
private getOptionValue(option: any): any {
|
private getOptionValue(option: any): any {
|
||||||
if (option && typeof option === 'object') {
|
if (option && typeof option === 'object') {
|
||||||
return option[this.valueKey];
|
return option[this.valueKey];
|
||||||
|
|
@ -103,6 +128,14 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
return (this.options || []).find((o) => this.getOptionValue(o) === value);
|
return (this.options || []).find((o) => this.getOptionValue(o) === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeText(value: string): string {
|
||||||
|
return (value ?? '')
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('document:click', ['$event'])
|
@HostListener('document:click', ['$event'])
|
||||||
onDocumentClick(event: MouseEvent): void {
|
onDocumentClick(event: MouseEvent): void {
|
||||||
if (!this.isOpen) return;
|
if (!this.isOpen) return;
|
||||||
|
|
|
||||||
|
|
@ -231,34 +231,31 @@
|
||||||
<div class="edit-sections">
|
<div class="edit-sections">
|
||||||
<details open class="detail-box">
|
<details open class="detail-box">
|
||||||
<summary class="box-header">
|
<summary class="box-header">
|
||||||
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com GERAL</span>
|
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com Reserva</span>
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field span-2">
|
<div class="form-field span-2">
|
||||||
<label>Cliente (GERAL)</label>
|
<label>Linha (RESERVA)</label>
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="clientsFromGeral"
|
|
||||||
[(ngModel)]="createModel.selectedClient"
|
|
||||||
(ngModelChange)="onCreateClientChange()"
|
|
||||||
[disabled]="createClientsLoading"
|
|
||||||
></app-select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Linha (GERAL)</label>
|
|
||||||
<app-select
|
<app-select
|
||||||
class="form-select"
|
class="form-select"
|
||||||
size="sm"
|
size="sm"
|
||||||
[options]="lineOptionsCreate"
|
[options]="lineOptionsCreate"
|
||||||
labelKey="label"
|
labelKey="label"
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
|
[searchable]="true"
|
||||||
|
searchPlaceholder="Pesquisar linha da reserva..."
|
||||||
[(ngModel)]="createModel.mobileLineId"
|
[(ngModel)]="createModel.mobileLineId"
|
||||||
(ngModelChange)="onCreateLineChange()"
|
(ngModelChange)="onCreateLineChange()"
|
||||||
[disabled]="createLinesLoading || !createModel.selectedClient"
|
[disabled]="createLinesLoading"
|
||||||
|
placeholder="Selecione uma linha da Reserva..."
|
||||||
></app-select>
|
></app-select>
|
||||||
|
<small class="field-hint" *ngIf="createLinesLoading">Carregando linhas da Reserva...</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Total Franquia Line</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(createFranquiaLineTotal)" readonly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -291,7 +288,10 @@
|
||||||
<label>Razão Social</label>
|
<label>Razão Social</label>
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.razaoSocial" />
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.razaoSocial" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="createModel.linha" /></div>
|
<div class="form-field field-line">
|
||||||
|
<label>Linha</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" [value]="createModel.linha || ''" readonly />
|
||||||
|
</div>
|
||||||
<div class="form-field field-item field-auto">
|
<div class="form-field field-item field-auto">
|
||||||
<label>Item (Automático)</label>
|
<label>Item (Automático)</label>
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
||||||
|
|
@ -357,7 +357,26 @@
|
||||||
<label>Razão Social</label>
|
<label>Razão Social</label>
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.razaoSocial" />
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.razaoSocial" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="editModel.linha" /></div>
|
<div class="form-field field-line">
|
||||||
|
<label>Linha (Reserva)</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="editLineOptions"
|
||||||
|
labelKey="label"
|
||||||
|
valueKey="id"
|
||||||
|
[searchable]="true"
|
||||||
|
searchPlaceholder="Pesquisar linha da reserva..."
|
||||||
|
[(ngModel)]="editSelectedLineId"
|
||||||
|
(ngModelChange)="onEditLineChange()"
|
||||||
|
[disabled]="createLinesLoading"
|
||||||
|
placeholder="Selecione uma linha da Reserva..."
|
||||||
|
></app-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-auto">
|
||||||
|
<label>Total Franquia Line</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(editFranquiaLineTotal)" readonly />
|
||||||
|
</div>
|
||||||
<div class="form-field field-item field-auto">
|
<div class="form-field field-item field-auto">
|
||||||
<label>Item (Automático)</label>
|
<label>Item (Automático)</label>
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ interface LineOptionDto {
|
||||||
item: number;
|
item: number;
|
||||||
linha: string | null;
|
linha: string | null;
|
||||||
usuario: string | null;
|
usuario: string | null;
|
||||||
|
franquiaLine?: number | null;
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,13 +95,15 @@ export class DadosUsuarios implements OnInit {
|
||||||
createSaving = false;
|
createSaving = false;
|
||||||
createModel: any = null;
|
createModel: any = null;
|
||||||
createDateNascimento = '';
|
createDateNascimento = '';
|
||||||
clientsFromGeral: string[] = [];
|
createFranquiaLineTotal = 0;
|
||||||
|
editFranquiaLineTotal = 0;
|
||||||
|
editSelectedLineId = '';
|
||||||
|
editLineOptions: LineOptionDto[] = [];
|
||||||
lineOptionsCreate: LineOptionDto[] = [];
|
lineOptionsCreate: LineOptionDto[] = [];
|
||||||
readonly tipoPessoaOptions: SimpleOption[] = [
|
readonly tipoPessoaOptions: SimpleOption[] = [
|
||||||
{ label: 'Pessoa Física', value: 'PF' },
|
{ label: 'Pessoa Física', value: 'PF' },
|
||||||
{ label: 'Pessoa Jurídica', value: 'PJ' },
|
{ label: 'Pessoa Jurídica', value: 'PJ' },
|
||||||
];
|
];
|
||||||
createClientsLoading = false;
|
|
||||||
createLinesLoading = false;
|
createLinesLoading = false;
|
||||||
|
|
||||||
isSysAdmin = false;
|
isSysAdmin = false;
|
||||||
|
|
@ -295,7 +298,11 @@ export class DadosUsuarios implements OnInit {
|
||||||
razaoSocial: fullData.razaoSocial || (tipo === 'PJ' ? fullData.cliente : '')
|
razaoSocial: fullData.razaoSocial || (tipo === 'PJ' ? fullData.cliente : '')
|
||||||
};
|
};
|
||||||
this.editDateNascimento = this.toDateInput(fullData.dataNascimento);
|
this.editDateNascimento = this.toDateInput(fullData.dataNascimento);
|
||||||
|
this.editFranquiaLineTotal = 0;
|
||||||
|
this.editSelectedLineId = '';
|
||||||
|
this.editLineOptions = [];
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
|
this.loadReserveLinesForSelects();
|
||||||
},
|
},
|
||||||
error: () => this.showToast('Erro ao abrir edição', 'danger')
|
error: () => this.showToast('Erro ao abrir edição', 'danger')
|
||||||
});
|
});
|
||||||
|
|
@ -307,6 +314,9 @@ export class DadosUsuarios implements OnInit {
|
||||||
this.editModel = null;
|
this.editModel = null;
|
||||||
this.editDateNascimento = '';
|
this.editDateNascimento = '';
|
||||||
this.editingId = null;
|
this.editingId = null;
|
||||||
|
this.editSelectedLineId = '';
|
||||||
|
this.editLineOptions = [];
|
||||||
|
this.editFranquiaLineTotal = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditTipoChange() {
|
onEditTipoChange() {
|
||||||
|
|
@ -369,13 +379,14 @@ export class DadosUsuarios implements OnInit {
|
||||||
if (!this.isSysAdmin) return;
|
if (!this.isSysAdmin) return;
|
||||||
this.resetCreateModel();
|
this.resetCreateModel();
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
this.preloadGeralClients();
|
this.loadReserveLinesForSelects();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeCreate() {
|
closeCreate() {
|
||||||
this.createOpen = false;
|
this.createOpen = false;
|
||||||
this.createSaving = false;
|
this.createSaving = false;
|
||||||
this.createModel = null;
|
this.createModel = null;
|
||||||
|
this.createFranquiaLineTotal = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetCreateModel() {
|
private resetCreateModel() {
|
||||||
|
|
@ -397,33 +408,9 @@ export class DadosUsuarios implements OnInit {
|
||||||
telefoneFixo: ''
|
telefoneFixo: ''
|
||||||
};
|
};
|
||||||
this.createDateNascimento = '';
|
this.createDateNascimento = '';
|
||||||
|
this.createFranquiaLineTotal = 0;
|
||||||
this.lineOptionsCreate = [];
|
this.lineOptionsCreate = [];
|
||||||
this.createLinesLoading = false;
|
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.cliente = c;
|
|
||||||
this.lineOptionsCreate = [];
|
|
||||||
|
|
||||||
if (c) this.loadLinesForClient(c);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreateTipoChange() {
|
onCreateTipoChange() {
|
||||||
|
|
@ -438,12 +425,9 @@ export class DadosUsuarios implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadLinesForClient(cliente: string) {
|
private loadReserveLinesForSelects(onDone?: () => void) {
|
||||||
const c = (cliente ?? '').trim();
|
|
||||||
if (!c) return;
|
|
||||||
|
|
||||||
this.createLinesLoading = true;
|
this.createLinesLoading = true;
|
||||||
this.linesService.getLinesByClient(c).subscribe({
|
this.linesService.getLinesByClient('RESERVA').subscribe({
|
||||||
next: (items: any[]) => {
|
next: (items: any[]) => {
|
||||||
const mapped: LineOptionDto[] = (items ?? [])
|
const mapped: LineOptionDto[] = (items ?? [])
|
||||||
.filter(x => !!String(x?.id ?? '').trim())
|
.filter(x => !!String(x?.id ?? '').trim())
|
||||||
|
|
@ -457,12 +441,16 @@ export class DadosUsuarios implements OnInit {
|
||||||
.filter(x => !!String(x.linha ?? '').trim());
|
.filter(x => !!String(x.linha ?? '').trim());
|
||||||
|
|
||||||
this.lineOptionsCreate = mapped;
|
this.lineOptionsCreate = mapped;
|
||||||
|
if (this.editModel) this.syncEditLineOptions();
|
||||||
this.createLinesLoading = false;
|
this.createLinesLoading = false;
|
||||||
|
onDone?.();
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.lineOptionsCreate = [];
|
this.lineOptionsCreate = [];
|
||||||
|
this.editLineOptions = [];
|
||||||
this.createLinesLoading = false;
|
this.createLinesLoading = false;
|
||||||
this.showToast('Erro ao carregar linhas da GERAL.', 'danger');
|
this.showToast('Erro ao carregar linhas da Reserva.', 'danger');
|
||||||
|
onDone?.();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -477,13 +465,56 @@ export class DadosUsuarios implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEditLineChange() {
|
||||||
|
const id = String(this.editSelectedLineId ?? '').trim();
|
||||||
|
if (!id || id === '__CURRENT__') return;
|
||||||
|
this.linesService.getById(id).subscribe({
|
||||||
|
next: (d: MobileLineDetail) => this.applyLineDetailToEdit(d),
|
||||||
|
error: () => this.showToast('Erro ao carregar dados da linha.', 'danger')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncEditLineOptions() {
|
||||||
|
if (!this.editModel) {
|
||||||
|
this.editLineOptions = [];
|
||||||
|
this.editSelectedLineId = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLine = String(this.editModel.linha ?? '').trim();
|
||||||
|
const fromReserva = this.lineOptionsCreate.find((x) => String(x.linha ?? '').trim() === currentLine);
|
||||||
|
const options = [...this.lineOptionsCreate];
|
||||||
|
|
||||||
|
if (currentLine && !fromReserva) {
|
||||||
|
options.unshift({
|
||||||
|
id: '__CURRENT__',
|
||||||
|
item: Number(this.editModel.item ?? 0),
|
||||||
|
linha: currentLine,
|
||||||
|
usuario: this.editModel.cliente ?? null,
|
||||||
|
label: `Atual • ${currentLine}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editLineOptions = options;
|
||||||
|
this.editSelectedLineId = fromReserva?.id ?? (currentLine ? '__CURRENT__' : '');
|
||||||
|
if (fromReserva?.id) {
|
||||||
|
this.onEditLineChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applyLineDetailToCreate(d: MobileLineDetail) {
|
private applyLineDetailToCreate(d: MobileLineDetail) {
|
||||||
this.createModel.linha = d.linha ?? '';
|
this.createModel.linha = d.linha ?? '';
|
||||||
this.createModel.cliente = d.cliente ?? this.createModel.cliente ?? '';
|
this.createFranquiaLineTotal = this.toNullableNumber(d.franquiaLine) ?? 0;
|
||||||
if (!String(this.createModel.item ?? '').trim() && d.item) {
|
if (!String(this.createModel.item ?? '').trim() && d.item) {
|
||||||
this.createModel.item = String(d.item);
|
this.createModel.item = String(d.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lineClient = String(d.cliente ?? '').trim();
|
||||||
|
const isReserva = lineClient.localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0;
|
||||||
|
if (!isReserva && lineClient) {
|
||||||
|
this.createModel.cliente = lineClient;
|
||||||
|
}
|
||||||
|
|
||||||
if ((this.createModel.tipoPessoa ?? '').toUpperCase() === 'PJ') {
|
if ((this.createModel.tipoPessoa ?? '').toUpperCase() === 'PJ') {
|
||||||
if (!this.createModel.razaoSocial) this.createModel.razaoSocial = this.createModel.cliente;
|
if (!this.createModel.razaoSocial) this.createModel.razaoSocial = this.createModel.cliente;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -491,6 +522,15 @@ export class DadosUsuarios implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyLineDetailToEdit(d: MobileLineDetail) {
|
||||||
|
if (!this.editModel) return;
|
||||||
|
this.editModel.linha = d.linha ?? this.editModel.linha;
|
||||||
|
this.editFranquiaLineTotal = this.toNullableNumber(d.franquiaLine) ?? 0;
|
||||||
|
if (!String(this.editModel.item ?? '').trim() && d.item) {
|
||||||
|
this.editModel.item = d.item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveCreate() {
|
saveCreate() {
|
||||||
if (!this.createModel) return;
|
if (!this.createModel) return;
|
||||||
this.createSaving = true;
|
this.createSaving = true;
|
||||||
|
|
@ -584,6 +624,11 @@ export class DadosUsuarios implements OnInit {
|
||||||
return Number.isNaN(n) ? null : n;
|
return Number.isNaN(n) ? null : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatFranquiaLine(value: any): string {
|
||||||
|
const n = this.toNullableNumber(value) ?? 0;
|
||||||
|
return `${n.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })} GB`;
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeTipo(row: UserDataRow | null | undefined): 'PF' | 'PJ' {
|
private normalizeTipo(row: UserDataRow | null | undefined): 'PF' | 'PJ' {
|
||||||
const t = (row?.tipoPessoa ?? '').toString().trim().toUpperCase();
|
const t = (row?.tipoPessoa ?? '').toString().trim().toUpperCase();
|
||||||
if (t === 'PJ') return 'PJ';
|
if (t === 'PJ') return 'PJ';
|
||||||
|
|
|
||||||
|
|
@ -354,20 +354,10 @@
|
||||||
<span class="lbl">Ativas</span>
|
<span class="lbl">Ativas</span>
|
||||||
<span class="val">{{ statusResumo.ativos | number:'1.0-0' }}</span>
|
<span class="val">{{ statusResumo.ativos | number:'1.0-0' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-blocked"></span>
|
|
||||||
<span class="lbl">Bloqueadas</span>
|
|
||||||
<span class="val">{{ statusResumo.bloqueadas | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-reserve"></span>
|
|
||||||
<span class="lbl">Reserva</span>
|
|
||||||
<span class="val">{{ statusResumo.reservas | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="dot d-blocked-soft"></span>
|
<span class="dot d-blocked-soft"></span>
|
||||||
<span class="lbl">Outros Status</span>
|
<span class="lbl">Demais Linhas</span>
|
||||||
<span class="val">{{ clientOverview.outrosStatus | number:'1.0-0' }}</span>
|
<span class="val">{{ clientDemaisLinhas | number:'1.0-0' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-item total-row">
|
<div class="status-item total-row">
|
||||||
<span class="lbl">Total</span>
|
<span class="lbl">Total</span>
|
||||||
|
|
@ -393,8 +383,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'260ms'">
|
<div class="dashboard-section fade-in-up" [style.animation-delay]="'260ms'">
|
||||||
<div class="grid-halves">
|
<div class="card-modern full-width">
|
||||||
<div class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
<div class="card-header-clean">
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>Top Planos (Qtd. Linhas)</h3>
|
<h3>Top Planos (Qtd. Linhas)</h3>
|
||||||
|
|
@ -406,31 +395,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-halves mt-3 client-secondary-grid">
|
||||||
<div class="card-modern">
|
<div class="card-modern">
|
||||||
<div class="card-header-clean">
|
<div class="card-header-clean">
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>Top Usuários (Qtd. Linhas)</h3>
|
<h3>Top Usuários (Qtd. Linhas)</h3>
|
||||||
<p>Usuários com maior concentração de linhas</p>
|
<p>Apenas usuários de fato (sem bloqueados/aguardando)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact">
|
<div class="chart-wrapper-bar compact">
|
||||||
<canvas #chartResumoTopClientes></canvas>
|
<canvas #chartResumoTopClientes></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid-halves mt-3">
|
|
||||||
<div class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Reserva por DDD</h3>
|
|
||||||
<p>Linhas disponíveis em reserva por região</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-bar compact">
|
|
||||||
<canvas #chartResumoReservaDdd></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-modern">
|
<div class="card-modern">
|
||||||
<div class="card-header-clean">
|
<div class="card-header-clean">
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ type InsightsChartSeries = {
|
||||||
type InsightsKpisVivo = {
|
type InsightsKpisVivo = {
|
||||||
qtdLinhas?: number | null;
|
qtdLinhas?: number | null;
|
||||||
totalFranquiaGb?: number | null;
|
totalFranquiaGb?: number | null;
|
||||||
|
totalFranquiaLine?: number | null;
|
||||||
totalBaseMensal?: number | null;
|
totalBaseMensal?: number | null;
|
||||||
totalAdicionaisMensal?: number | null;
|
totalAdicionaisMensal?: number | null;
|
||||||
totalGeralMensal?: number | null;
|
totalGeralMensal?: number | null;
|
||||||
|
|
@ -429,7 +430,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.fetchAllDashboardLines(true),
|
this.fetchAllDashboardLines(true),
|
||||||
]);
|
]);
|
||||||
const allLines = [...operacionais, ...reservas];
|
const allLines = [...operacionais, ...reservas];
|
||||||
this.applyClientLineAggregates(allLines, reservas);
|
this.applyClientLineAggregates(allLines);
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.resumoLoading = false;
|
this.resumoLoading = false;
|
||||||
|
|
@ -480,12 +481,10 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyClientLineAggregates(
|
private applyClientLineAggregates(
|
||||||
allLines: DashboardLineListItemDto[],
|
allLines: DashboardLineListItemDto[]
|
||||||
reservaLines: DashboardLineListItemDto[]
|
|
||||||
): void {
|
): void {
|
||||||
const planMap = new Map<string, number>();
|
const planMap = new Map<string, number>();
|
||||||
const userMap = new Map<string, number>();
|
const userMap = new Map<string, number>();
|
||||||
const reservaDddMap = new Map<string, number>();
|
|
||||||
const franquiaBandMap = new Map<string, number>();
|
const franquiaBandMap = new Map<string, number>();
|
||||||
|
|
||||||
let totalLinhas = 0;
|
let totalLinhas = 0;
|
||||||
|
|
@ -504,6 +503,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const status = this.normalizeSeriesKey(this.readLineString(line, 'status', 'Status'));
|
const status = this.normalizeSeriesKey(this.readLineString(line, 'status', 'Status'));
|
||||||
const planoContrato = this.readLineString(line, 'planoContrato', 'PlanoContrato').trim();
|
const planoContrato = this.readLineString(line, 'planoContrato', 'PlanoContrato').trim();
|
||||||
const usuario = this.readLineString(line, 'usuario', 'Usuario').trim();
|
const usuario = this.readLineString(line, 'usuario', 'Usuario').trim();
|
||||||
|
const usuarioKey = this.normalizeSeriesKey(usuario);
|
||||||
const franquiaLine = this.readLineNumber(line, 'franquiaLine', 'FranquiaLine');
|
const franquiaLine = this.readLineNumber(line, 'franquiaLine', 'FranquiaLine');
|
||||||
franquiaLineTotalGb += franquiaLine > 0 ? franquiaLine : 0;
|
franquiaLineTotalGb += franquiaLine > 0 ? franquiaLine : 0;
|
||||||
|
|
||||||
|
|
@ -527,8 +527,9 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const planoKey = planoContrato || 'Sem plano';
|
const planoKey = planoContrato || 'Sem plano';
|
||||||
planMap.set(planoKey, (planMap.get(planoKey) ?? 0) + 1);
|
planMap.set(planoKey, (planMap.get(planoKey) ?? 0) + 1);
|
||||||
|
|
||||||
const userKey = usuario || 'Sem usuário';
|
if (this.shouldIncludeTopUsuario(usuarioKey, status)) {
|
||||||
userMap.set(userKey, (userMap.get(userKey) ?? 0) + 1);
|
userMap.set(usuario, (userMap.get(usuario) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
const faixa = this.resolveFranquiaLineBand(franquiaLine);
|
const faixa = this.resolveFranquiaLineBand(franquiaLine);
|
||||||
franquiaBandMap.set(faixa, (franquiaBandMap.get(faixa) ?? 0) + 1);
|
franquiaBandMap.set(faixa, (franquiaBandMap.get(faixa) ?? 0) + 1);
|
||||||
|
|
@ -544,11 +545,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const line of reservaLines) {
|
|
||||||
const ddd = this.extractDddFromLine(this.readLineString(line, 'linha', 'Linha')) ?? 'Sem DDD';
|
|
||||||
reservaDddMap.set(ddd, (reservaDddMap.get(ddd) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const topPlanos = Array.from(planMap.entries())
|
const topPlanos = Array.from(planMap.entries())
|
||||||
.map(([plano, linhas]) => ({ plano, linhas }))
|
.map(([plano, linhas]) => ({ plano, linhas }))
|
||||||
.sort((a, b) => b.linhas - a.linhas || a.plano.localeCompare(b.plano, 'pt-BR'))
|
.sort((a, b) => b.linhas - a.linhas || a.plano.localeCompare(b.plano, 'pt-BR'))
|
||||||
|
|
@ -559,11 +555,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
.sort((a, b) => b.linhas - a.linhas || a.cliente.localeCompare(b.cliente, 'pt-BR'))
|
.sort((a, b) => b.linhas - a.linhas || a.cliente.localeCompare(b.cliente, 'pt-BR'))
|
||||||
.slice(0, this.resumoTopN);
|
.slice(0, this.resumoTopN);
|
||||||
|
|
||||||
const topReserva = Array.from(reservaDddMap.entries())
|
|
||||||
.map(([ddd, total]) => ({ ddd, total, linhas: total }))
|
|
||||||
.sort((a, b) => b.total - a.total || a.ddd.localeCompare(b.ddd, 'pt-BR'))
|
|
||||||
.slice(0, this.resumoTopN);
|
|
||||||
|
|
||||||
const franquiaOrder = ['Sem franquia', 'Até 10 GB', '10 a 20 GB', '20 a 50 GB', 'Acima de 50 GB'];
|
const franquiaOrder = ['Sem franquia', 'Até 10 GB', '10 a 20 GB', '20 a 50 GB', 'Acima de 50 GB'];
|
||||||
const franquiaLabels = franquiaOrder.filter((label) => (franquiaBandMap.get(label) ?? 0) > 0);
|
const franquiaLabels = franquiaOrder.filter((label) => (franquiaBandMap.get(label) ?? 0) > 0);
|
||||||
this.franquiaLabels = franquiaLabels.length ? franquiaLabels : franquiaOrder;
|
this.franquiaLabels = franquiaLabels.length ? franquiaLabels : franquiaOrder;
|
||||||
|
|
@ -609,9 +600,9 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.resumoClientesLabels = topUsuarios.map((x) => x.cliente);
|
this.resumoClientesLabels = topUsuarios.map((x) => x.cliente);
|
||||||
this.resumoClientesValues = topUsuarios.map((x) => x.linhas);
|
this.resumoClientesValues = topUsuarios.map((x) => x.linhas);
|
||||||
|
|
||||||
this.resumoTopReserva = topReserva;
|
this.resumoTopReserva = [];
|
||||||
this.resumoReservaLabels = topReserva.map((x) => x.ddd);
|
this.resumoReservaLabels = [];
|
||||||
this.resumoReservaValues = topReserva.map((x) => x.total);
|
this.resumoReservaValues = [];
|
||||||
|
|
||||||
this.resumoPfPjLabels = [];
|
this.resumoPfPjLabels = [];
|
||||||
this.resumoPfPjValues = [];
|
this.resumoPfPjValues = [];
|
||||||
|
|
@ -815,6 +806,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
vivo: {
|
vivo: {
|
||||||
qtdLinhas: this.toNumberOrNull(this.readNode(vivoRaw, 'qtdLinhas', 'QtdLinhas')),
|
qtdLinhas: this.toNumberOrNull(this.readNode(vivoRaw, 'qtdLinhas', 'QtdLinhas')),
|
||||||
totalFranquiaGb: this.toNumberOrNull(this.readNode(vivoRaw, 'totalFranquiaGb', 'TotalFranquiaGb')),
|
totalFranquiaGb: this.toNumberOrNull(this.readNode(vivoRaw, 'totalFranquiaGb', 'TotalFranquiaGb')),
|
||||||
|
totalFranquiaLine: this.toNumberOrNull(this.readNode(vivoRaw, 'totalFranquiaLine', 'TotalFranquiaLine')),
|
||||||
totalBaseMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalBaseMensal', 'TotalBaseMensal')),
|
totalBaseMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalBaseMensal', 'TotalBaseMensal')),
|
||||||
totalAdicionaisMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalAdicionaisMensal', 'TotalAdicionaisMensal')),
|
totalAdicionaisMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalAdicionaisMensal', 'TotalAdicionaisMensal')),
|
||||||
totalGeralMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalGeralMensal', 'TotalGeralMensal')),
|
totalGeralMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalGeralMensal', 'TotalGeralMensal')),
|
||||||
|
|
@ -1188,6 +1180,28 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return cliente === 'RESERVA' || usuario === 'RESERVA' || skil === 'RESERVA';
|
return cliente === 'RESERVA' || usuario === 'RESERVA' || skil === 'RESERVA';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldIncludeTopUsuario(usuarioKey: string, statusKey: string): boolean {
|
||||||
|
if (!usuarioKey) return false;
|
||||||
|
|
||||||
|
const invalidUserTokens = [
|
||||||
|
'SEMUSUARIO',
|
||||||
|
'AGUARDANDOUSUARIO',
|
||||||
|
'AGUARDANDO',
|
||||||
|
'BLOQUEAR',
|
||||||
|
'BLOQUEAD',
|
||||||
|
'BLOQUEADO',
|
||||||
|
'RESERVA',
|
||||||
|
'NAOATRIBUIDO',
|
||||||
|
'PENDENTE',
|
||||||
|
];
|
||||||
|
if (invalidUserTokens.some((token) => usuarioKey.includes(token))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockedStatusTokens = ['BLOQUE', 'PERDA', 'ROUBO', 'SUSPEN', 'CANCEL', 'AGUARD'];
|
||||||
|
return !blockedStatusTokens.some((token) => statusKey.includes(token));
|
||||||
|
}
|
||||||
|
|
||||||
private resolveFranquiaLineBand(value: number): string {
|
private resolveFranquiaLineBand(value: number): string {
|
||||||
if (!Number.isFinite(value) || value <= 0) return 'Sem franquia';
|
if (!Number.isFinite(value) || value <= 0) return 'Sem franquia';
|
||||||
if (value < 10) return 'Até 10 GB';
|
if (value < 10) return 'Até 10 GB';
|
||||||
|
|
@ -1250,20 +1264,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
icon: 'bi bi-check2-circle',
|
icon: 'bi bi-check2-circle',
|
||||||
hint: 'Status ativo',
|
hint: 'Status ativo',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'linhas_bloqueadas',
|
|
||||||
title: 'Linhas Bloqueadas',
|
|
||||||
value: this.formatInt(overview.bloqueadas),
|
|
||||||
icon: 'bi bi-slash-circle',
|
|
||||||
hint: 'Bloqueio/suspensão',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'linhas_reserva',
|
|
||||||
title: 'Linhas em Reserva',
|
|
||||||
value: this.formatInt(overview.reservas),
|
|
||||||
icon: 'bi bi-inboxes-fill',
|
|
||||||
hint: 'Disponíveis para uso',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'franquia_line_total',
|
key: 'franquia_line_total',
|
||||||
title: 'Franquia Line Total',
|
title: 'Franquia Line Total',
|
||||||
|
|
@ -1306,15 +1306,33 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
add('linhas_ativas', 'Linhas Ativas', this.formatInt(dashboard.ativos), 'bi bi-check2-circle', 'Status ativo');
|
add('linhas_ativas', 'Linhas Ativas', this.formatInt(dashboard.ativos), 'bi bi-check2-circle', 'Status ativo');
|
||||||
add('linhas_bloqueadas', 'Linhas Bloqueadas', this.formatInt(dashboard.bloqueados), 'bi bi-slash-circle', 'Todos os bloqueios');
|
add('linhas_bloqueadas', 'Linhas Bloqueadas', this.formatInt(dashboard.bloqueados), 'bi bi-slash-circle', 'Todos os bloqueios');
|
||||||
add('linhas_reserva', 'Linhas em Reserva', this.formatInt(dashboard.reservas), 'bi bi-inboxes-fill', 'Base de reserva');
|
add('linhas_reserva', 'Linhas em Reserva', this.formatInt(dashboard.reservas), 'bi bi-inboxes-fill', 'Base de reserva');
|
||||||
if (insights) {
|
const franquiaVivoTotal = this.toNumberOrNull(insights?.vivo?.totalFranquiaGb)
|
||||||
|
?? this.toNumberOrNull(this.resumo?.vivoLineTotals?.franquiaTotal)
|
||||||
|
?? (this.resumo?.vivoLineResumos ?? []).reduce(
|
||||||
|
(acc, row) => acc + (this.toNumberOrNull(row?.franquiaTotal) ?? 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
add(
|
add(
|
||||||
'franquia_vivo_total',
|
'franquia_vivo_total',
|
||||||
'Total Franquia Vivo',
|
'Total Franquia Vivo',
|
||||||
this.formatDataAllowance(this.toNumberOrNull(insights.vivo?.totalFranquiaGb) ?? 0),
|
this.formatDataAllowance(franquiaVivoTotal),
|
||||||
'bi bi-diagram-3-fill',
|
'bi bi-diagram-3-fill',
|
||||||
'Soma das franquias (Geral)'
|
'Soma das franquias (Geral)'
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
const franquiaLineTotal = this.toNumberOrNull(insights?.vivo?.totalFranquiaLine)
|
||||||
|
?? this.toNumberOrNull(this.resumo?.vivoLineTotals?.franquiaLine)
|
||||||
|
?? (this.resumo?.vivoLineResumos ?? []).reduce(
|
||||||
|
(acc, row) => acc + (this.toNumberOrNull(row?.franquiaLine) ?? 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
add(
|
||||||
|
'franquia_line_total',
|
||||||
|
'Total Franquia Line',
|
||||||
|
this.formatDataAllowance(franquiaLineTotal),
|
||||||
|
'bi bi-hdd-network-fill',
|
||||||
|
'Soma da franquia line'
|
||||||
|
);
|
||||||
add('vig_vencidos', 'Vigencia Vencida', this.formatInt(dashboard.vigenciaVencidos), 'bi bi-exclamation-triangle-fill', 'Prioridade alta');
|
add('vig_vencidos', 'Vigencia Vencida', this.formatInt(dashboard.vigenciaVencidos), 'bi bi-exclamation-triangle-fill', 'Prioridade alta');
|
||||||
add('vig_30', 'Vence em 30 dias', this.formatInt(dashboard.vigenciaAVencer30), 'bi bi-calendar2-week-fill', 'Prioridade');
|
add('vig_30', 'Vence em 30 dias', this.formatInt(dashboard.vigenciaAVencer30), 'bi bi-calendar2-week-fill', 'Prioridade');
|
||||||
add('mureg_30', 'MUREG 30 dias', this.formatInt(dashboard.muregsUltimos30Dias), 'bi bi-arrow-repeat', 'Movimentacao');
|
add('mureg_30', 'MUREG 30 dias', this.formatInt(dashboard.muregsUltimos30Dias), 'bi bi-arrow-repeat', 'Movimentacao');
|
||||||
|
|
@ -1414,10 +1432,10 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
// 1. Status Pie
|
// 1. Status Pie
|
||||||
if (this.chartStatusPie?.nativeElement) {
|
if (this.chartStatusPie?.nativeElement) {
|
||||||
const chartLabels = this.isCliente
|
const chartLabels = this.isCliente
|
||||||
? ['Ativas', 'Bloqueadas', 'Reservas']
|
? ['Ativas', 'Demais linhas']
|
||||||
: ['Ativos', 'Perda/Roubo', 'Bloq 120d', 'Reservas', 'Outros'];
|
: ['Ativos', 'Perda/Roubo', 'Bloq 120d', 'Reservas', 'Outros'];
|
||||||
const chartData = this.isCliente
|
const chartData = this.isCliente
|
||||||
? [this.statusResumo.ativos, this.statusResumo.bloqueadas, this.statusResumo.reservas]
|
? [this.statusResumo.ativos, this.clientDemaisLinhas]
|
||||||
: [
|
: [
|
||||||
this.statusResumo.ativos,
|
this.statusResumo.ativos,
|
||||||
this.statusResumo.perdaRoubo,
|
this.statusResumo.perdaRoubo,
|
||||||
|
|
@ -1426,7 +1444,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.statusResumo.outras,
|
this.statusResumo.outras,
|
||||||
];
|
];
|
||||||
const chartColors = this.isCliente
|
const chartColors = this.isCliente
|
||||||
? [palette.status.ativos, palette.status.blocked, palette.status.reserve]
|
? [palette.status.ativos, '#cbd5e1']
|
||||||
: [
|
: [
|
||||||
palette.status.ativos,
|
palette.status.ativos,
|
||||||
palette.status.blocked,
|
palette.status.blocked,
|
||||||
|
|
@ -1852,6 +1870,10 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return Number.isNaN(n) ? null : n;
|
return Number.isNaN(n) ? null : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get clientDemaisLinhas(): number {
|
||||||
|
return Math.max(0, (this.statusResumo.total ?? 0) - (this.statusResumo.ativos ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
trackByKpiKey = (_: number, item: KpiCard) => item.key;
|
trackByKpiKey = (_: number, item: KpiCard) => item.key;
|
||||||
|
|
||||||
private getPalette() {
|
private getPalette() {
|
||||||
|
|
|
||||||
|
|
@ -697,16 +697,20 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>
|
<label>Linha <span class="text-danger">*</span></label>
|
||||||
{{ isCreateBatchMode ? 'Linha (Preencher no Lote)' : 'Linha' }}
|
<app-select
|
||||||
<span class="text-danger" *ngIf="!isCreateBatchMode">*</span>
|
class="form-select"
|
||||||
</label>
|
size="sm"
|
||||||
<input
|
[options]="createReservaLineOptions"
|
||||||
class="form-control form-control-sm"
|
labelKey="label"
|
||||||
[(ngModel)]="createModel.linha"
|
valueKey="value"
|
||||||
[disabled]="isCreateBatchMode"
|
[searchable]="true"
|
||||||
[placeholder]="isCreateBatchMode ? 'Use a tabela de lote abaixo' : '119...'"
|
searchPlaceholder="Pesquisar linha da reserva..."
|
||||||
/>
|
[placeholder]="loadingCreateReservaLines ? 'Carregando linhas da Reserva...' : 'Selecione uma linha da Reserva'"
|
||||||
|
[disabled]="loadingCreateReservaLines"
|
||||||
|
[(ngModel)]="createModel.reservaLineId"
|
||||||
|
(ngModelChange)="onCreateReservaLineChange()"
|
||||||
|
></app-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,14 @@ interface AccountCompanyOption {
|
||||||
contas: string[];
|
contas: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReservaLineOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
linha: string;
|
||||||
|
chip?: string;
|
||||||
|
usuario?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateBatchLineDraft extends Partial<CreateMobileLineRequest> {
|
interface CreateBatchLineDraft extends Partial<CreateMobileLineRequest> {
|
||||||
uid: number;
|
uid: number;
|
||||||
linha: string;
|
linha: string;
|
||||||
|
|
@ -398,6 +406,9 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies];
|
accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies];
|
||||||
loadingAccountCompanies = false;
|
loadingAccountCompanies = false;
|
||||||
|
createReservaLineOptions: ReservaLineOption[] = [];
|
||||||
|
loadingCreateReservaLines = false;
|
||||||
|
private createReservaLineLookup = new Map<string, ReservaLineOption>();
|
||||||
|
|
||||||
get contaEmpresaOptions(): string[] {
|
get contaEmpresaOptions(): string[] {
|
||||||
return this.accountCompanies.map((x) => x.empresa);
|
return this.accountCompanies.map((x) => x.empresa);
|
||||||
|
|
@ -437,6 +448,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
docType: 'PF',
|
docType: 'PF',
|
||||||
docNumber: '',
|
docNumber: '',
|
||||||
contaEmpresa: '',
|
contaEmpresa: '',
|
||||||
|
reservaLineId: '',
|
||||||
linha: '',
|
linha: '',
|
||||||
chip: '',
|
chip: '',
|
||||||
tipoDeChip: '',
|
tipoDeChip: '',
|
||||||
|
|
@ -2002,6 +2014,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.createMode = 'NEW_CLIENT';
|
this.createMode = 'NEW_CLIENT';
|
||||||
this.resetCreateModel();
|
this.resetCreateModel();
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
|
void this.loadCreateReservaLines();
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2022,6 +2035,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.syncContaEmpresaSelection(this.createModel);
|
this.syncContaEmpresaSelection(this.createModel);
|
||||||
|
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
|
void this.loadCreateReservaLines();
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2031,6 +2045,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
docType: 'PF',
|
docType: 'PF',
|
||||||
docNumber: '',
|
docNumber: '',
|
||||||
contaEmpresa: '',
|
contaEmpresa: '',
|
||||||
|
reservaLineId: '',
|
||||||
linha: '',
|
linha: '',
|
||||||
chip: '',
|
chip: '',
|
||||||
tipoDeChip: '',
|
tipoDeChip: '',
|
||||||
|
|
@ -2701,7 +2716,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private buildCreatePayload(model: any): CreateMobileLineRequest {
|
private buildCreatePayload(model: any): CreateMobileLineRequest {
|
||||||
this.calculateFinancials(model);
|
this.calculateFinancials(model);
|
||||||
|
|
||||||
const { contaEmpresa: _contaEmpresa, uid: _uid, ...createModelPayload } = model;
|
const { contaEmpresa: _contaEmpresa, uid: _uid, reservaLineId: _reservaLineId, ...createModelPayload } = model;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...createModelPayload,
|
...createModelPayload,
|
||||||
|
|
@ -2816,6 +2831,99 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.createModel.docNumber = value;
|
this.createModel.docNumber = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async loadCreateReservaLines(): Promise<void> {
|
||||||
|
if (this.loadingCreateReservaLines) return;
|
||||||
|
this.loadingCreateReservaLines = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pageSize = 500;
|
||||||
|
let page = 1;
|
||||||
|
const collected: ReservaLineOption[] = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const params = new HttpParams()
|
||||||
|
.set('page', String(page))
|
||||||
|
.set('pageSize', String(pageSize))
|
||||||
|
.set('skil', 'RESERVA');
|
||||||
|
|
||||||
|
const response = await firstValueFrom(this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params }));
|
||||||
|
const items = Array.isArray(response?.items) ? response.items : [];
|
||||||
|
|
||||||
|
for (const row of items) {
|
||||||
|
const id = (row?.id ?? '').toString().trim();
|
||||||
|
const linha = (row?.linha ?? '').toString().trim();
|
||||||
|
if (!id || !linha) continue;
|
||||||
|
collected.push({
|
||||||
|
value: id,
|
||||||
|
label: `${row?.item ?? ''} • ${linha} • ${(row?.usuario ?? 'SEM USUÁRIO').toString()}`,
|
||||||
|
linha,
|
||||||
|
chip: (row?.chip ?? '').toString(),
|
||||||
|
usuario: (row?.usuario ?? '').toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = Number(response?.total ?? 0);
|
||||||
|
if (!items.length || (total > 0 && collected.length >= total) || items.length < pageSize) break;
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const unique = collected.filter((opt) => {
|
||||||
|
if (seen.has(opt.value)) return false;
|
||||||
|
seen.add(opt.value);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
unique.sort((a, b) => a.linha.localeCompare(b.linha, 'pt-BR', { numeric: true, sensitivity: 'base' }));
|
||||||
|
|
||||||
|
this.createReservaLineOptions = unique;
|
||||||
|
this.createReservaLineLookup = new Map(unique.map((opt) => [opt.value, opt]));
|
||||||
|
} catch {
|
||||||
|
this.createReservaLineOptions = [];
|
||||||
|
this.createReservaLineLookup.clear();
|
||||||
|
await this.showToast('Erro ao carregar linhas da Reserva.');
|
||||||
|
} finally {
|
||||||
|
this.loadingCreateReservaLines = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreateReservaLineChange() {
|
||||||
|
const lineId = (this.createModel?.reservaLineId ?? '').toString().trim();
|
||||||
|
if (!lineId) {
|
||||||
|
this.createModel.linha = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = this.createReservaLineLookup.get(lineId);
|
||||||
|
if (selected) {
|
||||||
|
this.createModel.linha = selected.linha ?? '';
|
||||||
|
if (!String(this.createModel.chip ?? '').trim() && selected.chip) {
|
||||||
|
this.createModel.chip = selected.chip;
|
||||||
|
}
|
||||||
|
if (!String(this.createModel.usuario ?? '').trim() && selected.usuario) {
|
||||||
|
this.createModel.usuario = selected.usuario;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.http.get<ApiLineDetail>(`${this.apiBase}/${lineId}`).subscribe({
|
||||||
|
next: (detail) => {
|
||||||
|
this.createModel.linha = (detail?.linha ?? this.createModel.linha ?? '').toString();
|
||||||
|
if (!String(this.createModel.chip ?? '').trim() && detail?.chip) {
|
||||||
|
this.createModel.chip = detail.chip;
|
||||||
|
}
|
||||||
|
if (!String(this.createModel.tipoDeChip ?? '').trim() && detail?.tipoDeChip) {
|
||||||
|
this.createModel.tipoDeChip = detail.tipoDeChip;
|
||||||
|
}
|
||||||
|
if (!String(this.createModel.usuario ?? '').trim() && detail?.usuario) {
|
||||||
|
this.createModel.usuario = detail.usuario;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
// Mantém dados já carregados da lista.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async saveCreate() {
|
async saveCreate() {
|
||||||
if (this.isCreateBatchMode) {
|
if (this.isCreateBatchMode) {
|
||||||
await this.saveCreateBatch();
|
await this.saveCreateBatch();
|
||||||
|
|
|
||||||
|
|
@ -34,19 +34,18 @@
|
||||||
<div class="form-field span-2">
|
<div class="form-field span-2">
|
||||||
<label for="tenantId">Cliente</label>
|
<label for="tenantId">Cliente</label>
|
||||||
<div class="select-row">
|
<div class="select-row">
|
||||||
<select
|
<app-select
|
||||||
id="tenantId"
|
id="tenantId"
|
||||||
formControlName="tenantId"
|
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
[options]="tenantOptions"
|
||||||
<option value="">Selecione um cliente...</option>
|
labelKey="label"
|
||||||
<option
|
valueKey="value"
|
||||||
*ngFor="let tenant of tenants; trackBy: trackByTenantId"
|
formControlName="tenantId"
|
||||||
[value]="tenant.tenantId"
|
[disabled]="tenantsLoading || tenantOptions.length === 0"
|
||||||
>
|
[searchable]="true"
|
||||||
{{ tenant.nomeOficial }}
|
searchPlaceholder="Pesquisar cliente..."
|
||||||
</option>
|
[placeholder]="tenantsLoading ? 'Carregando clientes...' : 'Selecione um cliente...'"
|
||||||
</select>
|
></app-select>
|
||||||
<button type="button" class="btn btn-ghost" (click)="loadTenants()" [disabled]="tenantsLoading">
|
<button type="button" class="btn btn-ghost" (click)="loadTenants()" [disabled]="tenantsLoading">
|
||||||
{{ tenantsLoading ? 'Atualizando...' : 'Atualizar lista' }}
|
{{ tenantsLoading ? 'Atualizando...' : 'Atualizar lista' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
ValidationErrors,
|
ValidationErrors,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SysadminService,
|
SysadminService,
|
||||||
|
|
@ -19,7 +20,7 @@ import {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-system-provision-user',
|
selector: 'app-system-provision-user',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReactiveFormsModule],
|
imports: [CommonModule, ReactiveFormsModule, CustomSelectComponent],
|
||||||
templateUrl: './system-provision-user.html',
|
templateUrl: './system-provision-user.html',
|
||||||
styleUrls: ['./system-provision-user.scss'],
|
styleUrls: ['./system-provision-user.scss'],
|
||||||
})
|
})
|
||||||
|
|
@ -28,6 +29,7 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
|
|
||||||
provisionForm: FormGroup;
|
provisionForm: FormGroup;
|
||||||
tenants: SystemTenantDto[] = [];
|
tenants: SystemTenantDto[] = [];
|
||||||
|
tenantOptions: Array<{ label: string; value: string }> = [];
|
||||||
tenantsLoading = false;
|
tenantsLoading = false;
|
||||||
tenantsError = '';
|
tenantsError = '';
|
||||||
|
|
||||||
|
|
@ -71,11 +73,17 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
this.tenants = (tenants || []).slice().sort((a, b) =>
|
this.tenants = (tenants || []).slice().sort((a, b) =>
|
||||||
(a.nomeOficial || '').localeCompare(b.nomeOficial || '', 'pt-BR', { sensitivity: 'base' })
|
(a.nomeOficial || '').localeCompare(b.nomeOficial || '', 'pt-BR', { sensitivity: 'base' })
|
||||||
);
|
);
|
||||||
|
this.tenantOptions = this.tenants.map((tenant) => ({
|
||||||
|
label: tenant.nomeOficial || tenant.tenantId,
|
||||||
|
value: tenant.tenantId,
|
||||||
|
}));
|
||||||
this.tenantsLoading = false;
|
this.tenantsLoading = false;
|
||||||
this.syncTenantControlAvailability();
|
this.syncTenantControlAvailability();
|
||||||
},
|
},
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
this.tenantsLoading = false;
|
this.tenantsLoading = false;
|
||||||
|
this.tenants = [];
|
||||||
|
this.tenantOptions = [];
|
||||||
this.tenantsError = this.extractErrorMessage(
|
this.tenantsError = this.extractErrorMessage(
|
||||||
err,
|
err,
|
||||||
'Não foi possível carregar os clientes. Verifique se a conta possui role sysadmin.'
|
'Não foi possível carregar os clientes. Verifique se a conta possui role sysadmin.'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue