Compare commits

..

2 Commits

Author SHA1 Message Date
Leon 5d0dc3b367 feat: teste 2026-03-05 15:31:04 -03:00
Leon c38a786f89 feat: Implementação do anexo e cliente poder editar 2026-03-03 17:43:20 -03:00
4 changed files with 411 additions and 40 deletions

View File

@ -398,9 +398,9 @@
<td>
<div class="action-group justify-content-center">
<button class="btn-icon" (click)="onDetalhes(r)" title="Detalhes"><i class="bi bi-eye"></i></button>
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
<ng-container *ngIf="!isClientRestricted">
<button class="btn-icon success" (click)="onFinanceiro(r)" title="Financeiro"><i class="bi bi-cash-coin"></i></button>
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
<button *ngIf="isSysAdmin" class="btn-icon danger" (click)="onRemover(r, true)" title="Remover"><i class="bi bi-trash"></i></button>
</ng-container>
</div>
@ -517,9 +517,9 @@
<td class="text-center">
<div class="action-group justify-content-center">
<button class="btn-icon" (click)="onDetalhes(r)" title="Detalhes"><i class="bi bi-eye"></i></button>
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
<ng-container *ngIf="!isClientRestricted">
<button class="btn-icon success" (click)="onFinanceiro(r)" title="Financeiro"><i class="bi bi-cash-coin"></i></button>
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
<button *ngIf="isSysAdmin" class="btn-icon danger" (click)="onRemover(r)" title="Remover"><i class="bi bi-trash"></i></button>
</ng-container>
</div>
@ -1693,8 +1693,8 @@
<div class="modal-body modern-body bg-light-gray" *ngIf="detailData; else detailLoading">
<div class="details-dashboard">
<div class="dashboard-column">
<div class="detail-box h-100">
<ng-template #detailIdentificacaoCard>
<div class="detail-box">
<div class="box-header justify-content-center">
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
</div>
@ -1712,9 +1712,13 @@
<span class="lbl">Usuário</span>
<span class="val">{{ detailData.usuario || '-' }}</span>
</div>
<div class="info-item span-2" *ngIf="isClientRestricted">
<div class="info-item span-2">
<span class="lbl">Centro de Custos</span>
<span class="val text-dark">&nbsp;</span>
<span class="val text-dark">{{ detailData.centroDeCustos || '-' }}</span>
</div>
<div class="info-item span-2">
<span class="lbl">Setor</span>
<span class="val">{{ detailData.setorNome || '-' }}</span>
</div>
<div class="info-item">
<span class="lbl">Item</span>
@ -1735,6 +1739,69 @@
</div>
</div>
</div>
</ng-template>
<ng-template #detailAparelhoCard>
<div class="detail-box">
<div class="box-header justify-content-center">
<span><i class="bi bi-phone me-2"></i> Aparelho</span>
</div>
<div class="box-body compact-padding">
<div class="info-grid compact-gap">
<div class="info-item span-2">
<span class="lbl">Aparelho</span>
<span class="val">{{ detailData.aparelhoNome || '-' }}</span>
</div>
<div class="info-item">
<span class="lbl">Cor do Aparelho</span>
<span class="val">{{ detailData.aparelhoCor || '-' }}</span>
</div>
<div class="info-item">
<span class="lbl">IMEI</span>
<span class="val">{{ detailData.aparelhoImei || '-' }}</span>
</div>
<div class="info-item span-2">
<span class="lbl">Nota Fiscal (Anexo)</span>
<span class="val">
<ng-container *ngIf="detailData.aparelhoNotaFiscalTemArquivo; else noNotaFiscalAnexo">
<button class="btn btn-sm btn-outline-primary" (click)="downloadAparelhoAnexo('nota-fiscal', detailData.id)">
Baixar arquivo
</button>
</ng-container>
<ng-template #noNotaFiscalAnexo>
-
</ng-template>
</span>
</div>
<div class="info-item span-2">
<span class="lbl">Recibo (Anexo)</span>
<span class="val">
<ng-container *ngIf="detailData.aparelhoReciboTemArquivo; else noReciboAnexo">
<button class="btn btn-sm btn-outline-primary" (click)="downloadAparelhoAnexo('recibo', detailData.id)">
Baixar arquivo
</button>
</ng-container>
<ng-template #noReciboAnexo>
-
</ng-template>
</span>
</div>
</div>
</div>
</div>
</ng-template>
<div class="dashboard-column d-flex flex-column gap-2" *ngIf="!isClientRestricted">
<ng-container *ngTemplateOutlet="detailIdentificacaoCard"></ng-container>
<ng-container *ngTemplateOutlet="detailAparelhoCard"></ng-container>
</div>
<div class="dashboard-column" *ngIf="isClientRestricted">
<ng-container *ngTemplateOutlet="detailIdentificacaoCard"></ng-container>
</div>
<div class="dashboard-column" *ngIf="isClientRestricted">
<ng-container *ngTemplateOutlet="detailAparelhoCard"></ng-container>
</div>
<div class="dashboard-column d-flex flex-column gap-2" *ngIf="!isClientRestricted">
@ -1919,7 +1986,85 @@
<div class="modal-body modern-body bg-light-gray">
<ng-container *ngIf="editModel; else editLoadingTpl">
<div class="edit-sections">
<div class="edit-sections" *ngIf="isClientRestricted">
<details open class="detail-box">
<summary class="box-header">
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
</summary>
<div class="box-body">
<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">
<label>Linha</label>
<input class="form-control form-control-sm bg-light" [(ngModel)]="editModel.linha" disabled />
</div>
<div class="form-field">
<label>Usuário</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.usuario" />
</div>
<div class="form-field">
<label>Centro de Custos</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.centroDeCustos" />
</div>
</div>
</div>
</details>
<details open class="detail-box">
<summary class="box-header">
<span><i class="bi bi-phone me-2"></i> Aparelho</span>
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
</summary>
<div class="box-body">
<div class="form-grid">
<div class="form-field span-2">
<label>Aparelho (Nome)</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.aparelhoNome" />
</div>
<div class="form-field">
<label>Cor do Aparelho</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.aparelhoCor" />
</div>
<div class="form-field">
<label>IMEI</label>
<input class="form-control form-control-sm" [(ngModel)]="editModel.aparelhoImei" />
</div>
<div class="form-field span-2">
<label>Nota Fiscal (Arquivo)</label>
<input class="form-control form-control-sm" type="file" accept=".pdf,.png,.jpg,.jpeg,.webp" (change)="onAparelhoNotaFiscalSelected($event)" />
<small class="text-muted d-block mt-1" *ngIf="aparelhoNotaFiscalFile">Selecionado: {{ aparelhoNotaFiscalFile.name }}</small>
<button
class="btn btn-sm btn-outline-primary mt-2"
type="button"
*ngIf="!aparelhoNotaFiscalFile && editModel.aparelhoNotaFiscalTemArquivo"
(click)="downloadAparelhoAnexo('nota-fiscal', editModel?.id)"
>
Baixar arquivo atual
</button>
</div>
<div class="form-field span-2">
<label>Recibo (Arquivo)</label>
<input class="form-control form-control-sm" type="file" accept=".pdf,.png,.jpg,.jpeg,.webp" (change)="onAparelhoReciboSelected($event)" />
<small class="text-muted d-block mt-1" *ngIf="aparelhoReciboFile">Selecionado: {{ aparelhoReciboFile.name }}</small>
<button
class="btn btn-sm btn-outline-primary mt-2"
type="button"
*ngIf="!aparelhoReciboFile && editModel.aparelhoReciboTemArquivo"
(click)="downloadAparelhoAnexo('recibo', editModel?.id)"
>
Baixar arquivo atual
</button>
</div>
</div>
</div>
</details>
</div>
<div class="edit-sections" *ngIf="!isClientRestricted">
<details open class="detail-box">
<summary class="box-header"><span><i class="bi bi-person-badge me-2"></i> Identificação</span><i class="bi bi-chevron-down ms-auto transition-icon"></i></summary>
<div class="box-body">
@ -1932,6 +2077,45 @@
<div class="form-field"><label>Tipo de Chip</label><input class="form-control form-control-sm" [(ngModel)]="editModel.tipoDeChip" /></div>
<div class="form-field"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" /></div>
<div class="form-field"><label>Usuário</label><input class="form-control form-control-sm" [(ngModel)]="editModel.usuario" /></div>
<div class="form-field"><label>Centro de Custos</label><input class="form-control form-control-sm" [(ngModel)]="editModel.centroDeCustos" /></div>
<div class="form-field"><label>Setor</label><input class="form-control form-control-sm" [(ngModel)]="editModel.setorNome" /></div>
</div>
</div>
</details>
<details open class="detail-box">
<summary class="box-header"><span><i class="bi bi-phone me-2"></i> Aparelho</span><i class="bi bi-chevron-down ms-auto transition-icon"></i></summary>
<div class="box-body">
<div class="form-grid">
<div class="form-field span-2"><label>Aparelho (Nome)</label><input class="form-control form-control-sm" [(ngModel)]="editModel.aparelhoNome" /></div>
<div class="form-field"><label>Aparelho (Cor)</label><input class="form-control form-control-sm" [(ngModel)]="editModel.aparelhoCor" /></div>
<div class="form-field"><label>Aparelho (IMEI)</label><input class="form-control form-control-sm" [(ngModel)]="editModel.aparelhoImei" /></div>
<div class="form-field span-2">
<label>Nota Fiscal (Arquivo)</label>
<input class="form-control form-control-sm" type="file" accept=".pdf,.png,.jpg,.jpeg,.webp" (change)="onAparelhoNotaFiscalSelected($event)" />
<small class="text-muted d-block mt-1" *ngIf="aparelhoNotaFiscalFile">Selecionado: {{ aparelhoNotaFiscalFile.name }}</small>
<button
class="btn btn-sm btn-outline-primary mt-2"
type="button"
*ngIf="!aparelhoNotaFiscalFile && editModel.aparelhoNotaFiscalTemArquivo"
(click)="downloadAparelhoAnexo('nota-fiscal', editModel?.id)"
>
Baixar arquivo atual
</button>
</div>
<div class="form-field span-2">
<label>Recibo (Arquivo)</label>
<input class="form-control form-control-sm" type="file" accept=".pdf,.png,.jpg,.jpeg,.webp" (change)="onAparelhoReciboSelected($event)" />
<small class="text-muted d-block mt-1" *ngIf="aparelhoReciboFile">Selecionado: {{ aparelhoReciboFile.name }}</small>
<button
class="btn btn-sm btn-outline-primary mt-2"
type="button"
*ngIf="!aparelhoReciboFile && editModel.aparelhoReciboTemArquivo"
(click)="downloadAparelhoAnexo('recibo', editModel?.id)"
>
Baixar arquivo atual
</button>
</div>
</div>
</div>
</details>

View File

@ -518,12 +518,18 @@
.modal-body .box-body { overflow: visible; }
.modal-xl-custom { width: min(1100px, 95vw); max-height: 85vh; }
.modal-card.modal-client-detail {
width: min(560px, 95vw);
width: min(980px, 96vw);
}
.modal-card.modal-client-detail .details-dashboard {
grid-template-columns: 1fr;
max-width: 520px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
max-width: 940px;
margin: 0 auto;
@media (max-width: 900px) {
grid-template-columns: 1fr;
max-width: 520px;
}
}
.modal-card.modal-create { width: min(1280px, 96vw); max-height: 92vh; }
.modal-card.modal-create.batch-mode { width: min(1560px, 99vw); }

View File

@ -49,6 +49,10 @@ interface LineRow {
chip?: string;
cliente: string;
usuario: string;
centroDeCustos?: string;
setorNome?: string;
aparelhoNome?: string;
aparelhoCor?: string;
status: string;
skil: string;
contrato: string;
@ -68,6 +72,10 @@ interface ApiLineList {
chip?: string | null;
cliente: string | null;
usuario: string | null;
centroDeCustos?: string | null;
setorNome?: string | null;
aparelhoNome?: string | null;
aparelhoCor?: string | null;
vencConta: string | null;
status?: string | null;
skil?: string | null;
@ -89,6 +97,15 @@ interface ApiLineDetail {
tipoDeChip?: string | null;
cliente?: string | null;
usuario?: string | null;
centroDeCustos?: string | null;
setorId?: string | null;
setorNome?: string | null;
aparelhoId?: string | null;
aparelhoNome?: string | null;
aparelhoCor?: string | null;
aparelhoImei?: string | null;
aparelhoNotaFiscalTemArquivo?: boolean;
aparelhoReciboTemArquivo?: boolean;
planoContrato?: string | null;
status?: string | null;
skil?: string | null;
@ -365,6 +382,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
detailData: any = null;
financeData: any = null;
editModel: any = null;
aparelhoNotaFiscalFile: File | null = null;
aparelhoReciboFile: File | null = null;
private editingId: string | null = null;
private searchTimer: any = null;
@ -835,6 +854,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
this.detailData = null;
this.financeData = null;
this.aparelhoNotaFiscalFile = null;
this.aparelhoReciboFile = null;
this.editSaving = false;
this.createSaving = false;
@ -1609,6 +1630,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
chip: x.chip ?? '',
cliente: x.cliente ?? '',
usuario: x.usuario ?? '',
centroDeCustos: x.centroDeCustos ?? '',
setorNome: x.setorNome ?? '',
aparelhoNome: x.aparelhoNome ?? '',
aparelhoCor: x.aparelhoCor ?? '',
status: x.status ?? '',
skil: x.skil ?? '',
contrato: x.vencConta ?? ''
@ -1841,6 +1866,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
this.editOpen = true;
this.editSaving = false;
this.editModel = null;
this.aparelhoNotaFiscalFile = null;
this.aparelhoReciboFile = null;
this.editingId = r.id;
this.cdr.detectChanges();
@ -1918,45 +1945,71 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
if (!this.editingId || !this.editModel) return;
this.editSaving = true;
this.calculateFinancials(this.editModel);
const editingId = this.editingId;
const shouldUploadAttachments = !!(this.aparelhoNotaFiscalFile || this.aparelhoReciboFile);
let payload: UpdateMobileLineRequest;
const { contaEmpresa: _contaEmpresa, ...editModelPayload } = this.editModel;
if (this.isClientRestricted) {
payload = {
item: this.toInt(this.editModel.item),
usuario: (this.editModel.usuario ?? '').toString(),
centroDeCustos: (this.editModel.centroDeCustos ?? '').toString(),
aparelhoId: (this.editModel.aparelhoId ?? null) as string | null,
aparelhoNome: (this.editModel.aparelhoNome ?? '').toString(),
aparelhoCor: (this.editModel.aparelhoCor ?? '').toString(),
aparelhoImei: (this.editModel.aparelhoImei ?? '').toString()
};
} else {
this.calculateFinancials(this.editModel);
const payload: UpdateMobileLineRequest = {
...editModelPayload,
item: this.toInt(this.editModel.item),
dataBloqueio: this.dateInputToIso(this.editModel.dataBloqueio),
dataEntregaOpera: this.dateInputToIso(this.editModel.dataEntregaOpera),
dataEntregaCliente: this.dateInputToIso(this.editModel.dataEntregaCliente),
dtEfetivacaoServico: this.dateInputToIso(this.editModel.dtEfetivacaoServico),
dtTerminoFidelizacao: this.dateInputToIso(this.editModel.dtTerminoFidelizacao),
vencConta: (this.editModel.vencConta ?? '').toString(),
franquiaVivo: this.toNullableNumber(this.editModel.franquiaVivo),
valorPlanoVivo: this.toNullableNumber(this.editModel.valorPlanoVivo),
gestaoVozDados: this.toNullableNumber(this.editModel.gestaoVozDados),
skeelo: this.toNullableNumber(this.editModel.skeelo),
vivoNewsPlus: this.toNullableNumber(this.editModel.vivoNewsPlus),
vivoTravelMundo: this.toNullableNumber(this.editModel.vivoTravelMundo),
vivoGestaoDispositivo: this.toNullableNumber(this.editModel.vivoGestaoDispositivo),
vivoSync: this.toNullableNumber(this.editModel.vivoSync),
valorContratoVivo: this.toNullableNumber(this.editModel.valorContratoVivo),
franquiaLine: this.toNullableNumber(this.editModel.franquiaLine),
franquiaGestao: this.toNullableNumber(this.editModel.franquiaGestao),
locacaoAp: this.toNullableNumber(this.editModel.locacaoAp),
valorContratoLine: this.toNullableNumber(this.editModel.valorContratoLine),
desconto: this.toNullableNumber(this.editModel.desconto),
lucro: this.toNullableNumber(this.editModel.lucro),
tipoDeChip: (this.editModel.tipoDeChip ?? '').toString()
};
const { contaEmpresa: _contaEmpresa, ...editModelPayload } = this.editModel;
this.http.put(`${this.apiBase}/${this.editingId}`, payload).subscribe({
payload = {
...editModelPayload,
item: this.toInt(this.editModel.item),
dataBloqueio: this.dateInputToIso(this.editModel.dataBloqueio),
dataEntregaOpera: this.dateInputToIso(this.editModel.dataEntregaOpera),
dataEntregaCliente: this.dateInputToIso(this.editModel.dataEntregaCliente),
dtEfetivacaoServico: this.dateInputToIso(this.editModel.dtEfetivacaoServico),
dtTerminoFidelizacao: this.dateInputToIso(this.editModel.dtTerminoFidelizacao),
vencConta: (this.editModel.vencConta ?? '').toString(),
franquiaVivo: this.toNullableNumber(this.editModel.franquiaVivo),
valorPlanoVivo: this.toNullableNumber(this.editModel.valorPlanoVivo),
gestaoVozDados: this.toNullableNumber(this.editModel.gestaoVozDados),
skeelo: this.toNullableNumber(this.editModel.skeelo),
vivoNewsPlus: this.toNullableNumber(this.editModel.vivoNewsPlus),
vivoTravelMundo: this.toNullableNumber(this.editModel.vivoTravelMundo),
vivoGestaoDispositivo: this.toNullableNumber(this.editModel.vivoGestaoDispositivo),
vivoSync: this.toNullableNumber(this.editModel.vivoSync),
valorContratoVivo: this.toNullableNumber(this.editModel.valorContratoVivo),
franquiaLine: this.toNullableNumber(this.editModel.franquiaLine),
franquiaGestao: this.toNullableNumber(this.editModel.franquiaGestao),
locacaoAp: this.toNullableNumber(this.editModel.locacaoAp),
valorContratoLine: this.toNullableNumber(this.editModel.valorContratoLine),
desconto: this.toNullableNumber(this.editModel.desconto),
lucro: this.toNullableNumber(this.editModel.lucro),
tipoDeChip: (this.editModel.tipoDeChip ?? '').toString()
};
}
this.http.put(`${this.apiBase}/${editingId}`, payload).subscribe({
next: async () => {
try {
if (shouldUploadAttachments) {
await this.uploadAparelhoAnexos(editingId);
}
} catch {
this.editSaving = false;
await this.showToast('Registro salvo, mas falhou o upload dos anexos.');
return;
}
this.editSaving = false;
// fecha e limpa overlay SEMPRE
this.closeAllModals();
await this.showToast('Registro atualizado!');
await this.showToast(shouldUploadAttachments ? 'Registro e anexos atualizados!' : 'Registro atualizado!');
if (this.isGroupMode && this.expandedGroup) {
const term = (this.searchTerm ?? '').trim();
@ -1976,6 +2029,115 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
});
}
onAparelhoNotaFiscalSelected(event: Event) {
const input = event.target as HTMLInputElement | null;
this.aparelhoNotaFiscalFile = input?.files && input.files.length > 0 ? input.files[0] : null;
}
onAparelhoReciboSelected(event: Event) {
const input = event.target as HTMLInputElement | null;
this.aparelhoReciboFile = input?.files && input.files.length > 0 ? input.files[0] : null;
}
async downloadAparelhoAnexo(tipo: 'nota-fiscal' | 'recibo', lineId?: string | null) {
const targetLineId = (lineId ?? this.detailData?.id ?? this.editingId ?? '').toString().trim();
if (!targetLineId) {
await this.showToast('Linha inválida para download do anexo.');
return;
}
try {
const response = await firstValueFrom(
this.http.get(`${this.apiBase}/${targetLineId}/aparelho/anexos/${tipo}`, {
observe: 'response',
responseType: 'blob'
})
);
const blob = response.body;
if (!blob) {
await this.showToast('Arquivo de anexo não encontrado.');
return;
}
const disposition = response.headers.get('content-disposition');
const contentType = (response.headers.get('content-type') || blob.type || '').toLowerCase();
const fallbackName = tipo === 'nota-fiscal' ? 'nota-fiscal' : 'recibo';
const fileName = this.extractAttachmentFileName(disposition, fallbackName, contentType);
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
} catch {
await this.showToast('Não foi possível baixar o anexo.');
}
}
private async uploadAparelhoAnexos(lineId: string): Promise<void> {
const hasAnyFile = !!this.aparelhoNotaFiscalFile || !!this.aparelhoReciboFile;
if (!hasAnyFile) {
return;
}
const formData = new FormData();
if (this.aparelhoNotaFiscalFile) {
formData.append('notaFiscal', this.aparelhoNotaFiscalFile, this.aparelhoNotaFiscalFile.name);
}
if (this.aparelhoReciboFile) {
formData.append('recibo', this.aparelhoReciboFile, this.aparelhoReciboFile.name);
}
await firstValueFrom(this.http.post(`${this.apiBase}/${lineId}/aparelho/anexos`, formData));
this.aparelhoNotaFiscalFile = null;
this.aparelhoReciboFile = null;
}
private extractAttachmentFileName(
contentDisposition: string | null,
fallbackBaseName: string,
contentType: string
): string {
const raw = (contentDisposition ?? '').trim();
const inferredExtension = this.extensionFromContentType(contentType) ?? '.pdf';
if (!raw) {
return `${fallbackBaseName}${inferredExtension}`;
}
const utf8Match = raw.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match?.[1]) {
return this.ensureFileNameExtension(decodeURIComponent(utf8Match[1]), inferredExtension);
}
const simpleMatch = raw.match(/filename="?([^";]+)"?/i);
if (simpleMatch?.[1]) {
return this.ensureFileNameExtension(simpleMatch[1], inferredExtension);
}
return `${fallbackBaseName}${inferredExtension}`;
}
private extensionFromContentType(contentType: string): string | null {
if (!contentType) return null;
if (contentType.includes('application/pdf')) return '.pdf';
if (contentType.includes('image/png')) return '.png';
if (contentType.includes('image/jpeg') || contentType.includes('image/jpg')) return '.jpg';
if (contentType.includes('image/webp')) return '.webp';
return null;
}
private ensureFileNameExtension(fileName: string, fallbackExtension: string): string {
const normalized = (fileName ?? '').trim();
if (!normalized) return `anexo${fallbackExtension}`;
const hasExtension = /\.[A-Za-z0-9]{2,6}$/.test(normalized);
return hasExtension ? normalized : `${normalized}${fallbackExtension}`;
}
async onRemover(r: LineRow, fromGroup = false) {
if (!this.isSysAdmin) {
await this.showToast('Apenas sysadmin pode remover linhas.');
@ -3430,6 +3592,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
return {
...d,
item: d.item ?? 0,
centroDeCustos: d.centroDeCustos ?? '',
setorId: d.setorId ?? null,
setorNome: d.setorNome ?? '',
aparelhoId: d.aparelhoId ?? null,
aparelhoNome: d.aparelhoNome ?? '',
aparelhoCor: d.aparelhoCor ?? '',
aparelhoImei: d.aparelhoImei ?? '',
aparelhoNotaFiscalTemArquivo: !!d.aparelhoNotaFiscalTemArquivo,
aparelhoReciboTemArquivo: !!d.aparelhoReciboTemArquivo,
dataBloqueio: this.isoToDateInput(d.dataBloqueio),
dataEntregaOpera: this.isoToDateInput(d.dataEntregaOpera),
dataEntregaCliente: this.isoToDateInput(d.dataEntregaCliente),

View File

@ -18,6 +18,10 @@ export interface MobileLineList {
chip: string | null;
cliente: string | null;
usuario: string | null;
centroDeCustos?: string | null;
setorNome?: string | null;
aparelhoNome?: string | null;
aparelhoCor?: string | null;
planoContrato: string | null;
status: string | null;
skil: string | null;
@ -26,6 +30,12 @@ export interface MobileLineList {
}
export interface MobileLineDetail extends MobileLineList {
setorId?: string | null;
aparelhoId?: string | null;
aparelhoImei?: string | null;
aparelhoNotaFiscalTemArquivo?: boolean;
aparelhoReciboTemArquivo?: boolean;
franquiaVivo?: number | null;
valorPlanoVivo?: number | null;
gestaoVozDados?: number | null;