diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html
index fbcf476..e2c39ec 100644
--- a/src/app/pages/geral/geral.html
+++ b/src/app/pages/geral/geral.html
@@ -398,9 +398,9 @@
+
-
@@ -517,9 +517,9 @@
|
+
-
@@ -1712,9 +1712,51 @@
Usuário
{{ detailData.usuario || '-' }}
-
+
Centro de Custos
-
+ {{ detailData.centroDeCustos || '-' }}
+
+
+ Setor
+ {{ detailData.setorNome || '-' }}
+
+
+ Aparelho
+ {{ detailData.aparelhoNome || '-' }}
+
+
+ Cor do Aparelho
+ {{ detailData.aparelhoCor || '-' }}
+
+
+ IMEI
+ {{ detailData.aparelhoImei || '-' }}
+
+
+ Nota Fiscal (Anexo)
+
+
+
+
+
+ -
+
+
+
+
+ Recibo (Anexo)
+
+
+
+
+
+ -
+
+
Item
@@ -1919,7 +1961,74 @@
-
+
+
+
diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts
index a5f66c2..7e78101 100644
--- a/src/app/pages/geral/geral.ts
+++ b/src/app/pages/geral/geral.ts
@@ -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 {
+ 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),
diff --git a/src/app/services/lines.service.ts b/src/app/services/lines.service.ts
index 5ce8ae2..022570c 100644
--- a/src/app/services/lines.service.ts
+++ b/src/app/services/lines.service.ts
@@ -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;
|