import { Component, ChangeDetectorRef, ElementRef, Inject, OnInit, PLATFORM_ID, ViewChild } from '@angular/core'; import { CommonModule, isPlatformBrowser } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; import { MveAuditService, type ApplyMveAuditResult, type MveAuditIssue, type MveAuditRun, } from '../../services/mve-audit.service'; import { confirmActionModal } from '../../utils/destructive-confirmation'; import { buildPageNumbers, clampPage, computePageEnd, computePageStart, computeTotalPages, } from '../../utils/pagination.util'; type MveIssueViewMode = 'PENDING' | 'APPLIED' | 'ALL'; type MveIssueCategory = 'ALL' | 'STATUS' | 'LINE' | 'CHIP'; @Component({ selector: 'app-mve-auditoria', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './mve-auditoria.html', styleUrls: ['./mve-auditoria.scss'], }) export class MveAuditoriaPage implements OnInit { @ViewChild('feedbackToast', { static: false }) feedbackToast?: ElementRef; loadingLatest = false; processing = false; syncing = false; selectedFile: File | null = null; auditResult: MveAuditRun | null = null; applyResult: ApplyMveAuditResult | null = null; errorMessage = ''; toastMessage = ''; searchTerm = ''; viewMode: MveIssueViewMode = 'PENDING'; issueCategory: MveIssueCategory = 'ALL'; page = 1; pageSize = 20; readonly pageSizeOptions = [10, 20, 50, 100]; constructor( private readonly mveAuditService: MveAuditService, private readonly cdr: ChangeDetectorRef, @Inject(PLATFORM_ID) private readonly platformId: object ) {} ngOnInit(): void { this.errorMessage = ''; const cachedRun = this.mveAuditService.getCachedRun(); if (cachedRun) { this.auditResult = cachedRun; return; } void this.restoreCachedAudit(); } get hasAuditResult(): boolean { return !!this.auditResult; } get syncableIssues(): MveAuditIssue[] { return this.relevantIssues.filter((issue) => issue.syncable && !issue.applied); } get relevantIssues(): MveAuditIssue[] { const issues = this.auditResult?.issues ?? []; return issues .filter((issue) => this.issueHasRelevantDifference(issue)) .sort((left, right) => Number(left.applied) - Number(right.applied)); } get filteredIssues(): MveAuditIssue[] { const query = this.normalizeSearch(this.searchTerm); return this.relevantIssues.filter((issue) => { if (this.viewMode === 'PENDING' && issue.applied) return false; if (this.viewMode === 'APPLIED' && !issue.applied) return false; if (!this.matchesIssueCategory(issue)) return false; if (!query) return true; const haystack = [ issue.numeroLinha, issue.issueType, issue.actionSuggestion, issue.systemStatus, issue.reportStatus, issue.systemSnapshot?.numeroLinha, issue.reportSnapshot?.numeroLinha, issue.systemSnapshot?.chip, issue.reportSnapshot?.chip, issue.situation, issue.notes, ...(issue.differences ?? []).flatMap((difference) => [ difference.label, difference.systemValue, difference.reportValue, ]), ] .map((value) => this.normalizeSearch(value)) .join(' '); return haystack.includes(query); }); } get pagedIssues(): MveAuditIssue[] { const offset = (this.page - 1) * this.pageSize; return this.filteredIssues.slice(offset, offset + this.pageSize); } get totalPages(): number { return computeTotalPages(this.filteredIssues.length, this.pageSize); } get pageNumbers(): number[] { return buildPageNumbers(this.page, this.totalPages); } get pageStart(): number { return computePageStart(this.filteredIssues.length, this.page, this.pageSize); } get pageEnd(): number { return computePageEnd(this.filteredIssues.length, this.page, this.pageSize); } get totalDifferencesCount(): number { if (!this.auditResult) return 0; return this.auditResult.summary.totalStatusDivergences + this.auditResult.summary.totalDataDivergences; } get manualReviewIssuesCount(): number { return this.relevantIssues.filter((issue) => !issue.syncable && !issue.applied).length; } get statusIssuesCount(): number { return this.relevantIssues.filter((issue) => this.hasDifference(issue, 'status')).length; } get lineIssuesCount(): number { return this.relevantIssues.filter((issue) => this.hasDifference(issue, 'line')).length; } get chipIssuesCount(): number { return this.relevantIssues.filter((issue) => this.hasDifference(issue, 'chip')).length; } get ignoredIssuesCount(): number { if (!this.auditResult) return 0; const summary = this.auditResult.summary; return ( summary.totalOnlyInSystem + summary.totalOnlyInReport + summary.totalDuplicateReportLines + summary.totalDuplicateSystemLines + summary.totalInvalidRows + summary.totalUnknownStatuses ); } async loadLatestAudit(): Promise { this.loadingLatest = true; this.errorMessage = ''; try { this.auditResult = await firstValueFrom(this.mveAuditService.getLatest()); this.applyResult = null; this.issueCategory = 'ALL'; this.page = 1; } catch (error) { const httpError = error as HttpErrorResponse | null; if (httpError?.status !== 404) { this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel carregar a ultima conferencia.'); } this.auditResult = null; } finally { this.loadingLatest = false; } } onFileSelected(event: Event): void { const input = event.target as HTMLInputElement | null; const file = input?.files?.[0] ?? null; this.errorMessage = ''; this.applyResult = null; if (!file) { this.selectedFile = null; return; } if (!file.name.toLowerCase().endsWith('.csv')) { this.selectedFile = null; this.errorMessage = 'Selecione o relatorio exportado pela Vivo.'; return; } if (file.size <= 0) { this.selectedFile = null; this.errorMessage = 'O arquivo selecionado está vazio.'; return; } if (file.size > 20 * 1024 * 1024) { this.selectedFile = null; this.errorMessage = 'O arquivo excede o limite de 20 MB.'; return; } this.selectedFile = file; } clearSelectedFile(): void { this.selectedFile = null; this.errorMessage = ''; } async processAudit(): Promise { if (!this.selectedFile || this.processing || this.syncing) { return; } this.processing = true; this.errorMessage = ''; this.applyResult = null; try { this.auditResult = await firstValueFrom(this.mveAuditService.preview(this.selectedFile)); this.issueCategory = 'ALL'; this.page = 1; this.viewMode = 'PENDING'; this.searchTerm = ''; await this.showToast('Relatorio conferido com sucesso.'); } catch (error) { this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel conferir o relatorio.'); } finally { this.processing = false; } } async syncIssues(): Promise { if (!this.auditResult || this.syncableIssues.length === 0 || this.syncing) { return; } const confirmed = await confirmActionModal({ title: 'Atualizar sistema', message: `${this.syncableIssues.length} ocorrência(s) sincronizável(is) serão aplicadas com base no relatório da Vivo.`, confirmLabel: 'Atualizar agora', cancelLabel: 'Cancelar', tone: 'warning', }); if (!confirmed) { return; } this.syncing = true; this.errorMessage = ''; try { this.applyResult = await firstValueFrom(this.mveAuditService.apply(this.auditResult.id)); this.auditResult = await firstValueFrom(this.mveAuditService.getById(this.auditResult.id)); this.viewMode = 'ALL'; this.page = 1; await this.showToast('Atualizações aplicadas com sucesso.'); } catch (error) { this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel atualizar o sistema.'); } finally { this.syncing = false; } } onSearchChange(): void { this.page = 1; } onPageSizeChange(): void { this.page = 1; } setViewMode(mode: MveIssueViewMode): void { this.viewMode = mode; this.page = 1; } setIssueCategory(category: MveIssueCategory): void { this.issueCategory = category; this.page = 1; } goToPage(page: number): void { this.page = clampPage(page, this.totalPages); } trackByIssue(_: number, issue: MveAuditIssue): string { return issue.id; } formatDateTime(value?: string | null): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '-'; return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short', }).format(date); } statusClass(status: string | null | undefined): string { const normalized = (status ?? '').toLowerCase(); if (normalized.includes('bloq') || normalized.includes('perda') || normalized.includes('roubo')) return 'is-blocked'; if (normalized.includes('ativo')) return 'is-active'; return 'is-neutral'; } statusLabel(status: string | null | undefined): string { const value = (status ?? '').trim(); return value || '-'; } hasDifference(issue: MveAuditIssue, fieldKey: string): boolean { return (issue.differences ?? []).some((difference) => difference.fieldKey === fieldKey); } formatValue(value?: string | null): string { const normalized = (value ?? '').trim(); return normalized || '-'; } issueKindLabel(issue: MveAuditIssue): string { const hasLine = this.hasDifference(issue, 'line'); const hasChip = this.hasDifference(issue, 'chip'); const hasStatus = this.hasDifference(issue, 'status'); if (hasLine && hasStatus) return 'Troca de linha + status'; if (hasChip && hasStatus) return 'Troca de chip + status'; if (hasLine) return 'Troca de linha'; if (hasChip) return 'Troca de chip'; if (hasStatus) return 'Status'; if (issue.issueType === 'DDD_CHANGE_REVIEW') return 'Revisão de DDD'; return 'Revisão'; } issueKindClass(issue: MveAuditIssue): string { if (this.hasDifference(issue, 'line')) return 'is-line'; if (this.hasDifference(issue, 'chip')) return 'is-chip'; if (this.hasDifference(issue, 'status')) return 'is-status'; return issue.syncable ? 'is-neutral' : 'is-review'; } situationClass(issue: MveAuditIssue): string { if (issue.applied) return 'is-applied'; if (!issue.syncable) return 'is-review'; return this.issueKindClass(issue); } severityClass(severity: string | null | undefined): string { const normalized = (severity ?? '').trim().toUpperCase(); if (normalized === 'HIGH') return 'is-high'; if (normalized === 'MEDIUM') return 'is-medium'; if (normalized === 'WARNING') return 'is-warning'; return 'is-neutral'; } severityLabel(severity: string | null | undefined): string { const normalized = (severity ?? '').trim().toUpperCase(); if (normalized === 'HIGH') return 'Alta'; if (normalized === 'MEDIUM') return 'Media'; if (normalized === 'WARNING') return 'Aviso'; return 'Info'; } describeIssue(issue: MveAuditIssue): string { const differences = issue.differences ?? []; if (!differences.length) { return issue.notes?.trim() || 'Sem diferenças detalhadas.'; } return differences .map((difference) => `${difference.label}: ${this.formatValue(difference.systemValue)} -> ${this.formatValue(difference.reportValue)}`) .join(' | '); } private issueHasRelevantDifference(issue: MveAuditIssue): boolean { return (issue.differences ?? []).length > 0; } private async restoreCachedAudit(): Promise { const cachedRunId = this.mveAuditService.getCachedRunId(); if (!cachedRunId) { return; } this.loadingLatest = true; try { const restoredRun = await firstValueFrom(this.mveAuditService.restoreCachedRun()); if (!restoredRun) { return; } this.auditResult = restoredRun; this.applyResult = null; this.issueCategory = 'ALL'; this.page = 1; } finally { this.loadingLatest = false; } } private matchesIssueCategory(issue: MveAuditIssue): boolean { switch (this.issueCategory) { case 'STATUS': return this.hasDifference(issue, 'status'); case 'LINE': return this.hasDifference(issue, 'line'); case 'CHIP': return this.hasDifference(issue, 'chip'); default: return true; } } private normalizeSearch(value: unknown): string { return (value ?? '') .toString() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .trim(); } private extractHttpMessage(error: unknown, fallbackMessage: string): string { const httpError = error as HttpErrorResponse | null; if (httpError?.status === 0) { return 'A API do LineGestao nao respondeu em http://localhost:5298. Inicie o backend e tente novamente.'; } return ( (httpError?.error as { message?: string } | null)?.message || httpError?.message || fallbackMessage ); } private async showToast(message: string): Promise { if (!isPlatformBrowser(this.platformId)) return; this.toastMessage = message; this.cdr.detectChanges(); if (!this.feedbackToast?.nativeElement) return; try { const bootstrap = await import('bootstrap'); const instance = bootstrap.Toast.getOrCreateInstance(this.feedbackToast.nativeElement, { autohide: true, delay: 3200, }); instance.show(); } catch { // ignora falha de feedback visual } } }