478 lines
14 KiB
TypeScript
478 lines
14 KiB
TypeScript
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<HTMLElement>;
|
|
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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
|
|
}
|
|
}
|
|
}
|