line-gestao-frontend/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts

254 lines
7.6 KiB
TypeScript

import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { CustomSelectComponent } from '../../../../components/custom-select/custom-select';
export type MonthOption = { value: number; label: string };
export type ParcelamentoCreateModel = {
anoRef: number | null;
linha: string;
cliente: string;
item: number | null;
qtParcelas: string;
parcelaAtual: number | null;
totalParcelas: number | null;
valorCheio: string;
desconto: string;
valorComDesconto: string;
competenciaAno: number | null;
competenciaMes: number | null;
monthValues: Array<{ competencia: string; valor: string }>;
};
type PreviewRow = {
competencia: string;
label: string;
parcela: number;
valor: string;
};
@Component({
selector: 'app-parcelamento-create-modal',
standalone: true,
imports: [CommonModule, FormsModule, CustomSelectComponent],
templateUrl: './parcelamento-create-modal.html',
styleUrls: ['./parcelamento-create-modal.scss'],
})
export class ParcelamentoCreateModalComponent implements OnChanges {
@Input() open = false;
@Input() monthOptions: MonthOption[] = [];
@Input() model!: ParcelamentoCreateModel;
@Input() title = 'Novo Parcelamento';
@Input() submitLabel = 'Salvar';
@Input() loading = false;
@Input() errorMessage = '';
@Output() close = new EventEmitter<void>();
@Output() save = new EventEmitter<ParcelamentoCreateModel>();
touched = false;
previewRows: PreviewRow[] = [];
ngOnChanges(changes: SimpleChanges): void {
if (changes['model'] && this.model) {
this.syncMonthValues();
return;
}
if (changes['open'] && this.model) {
this.rebuildPreviewRows();
}
}
onValueChange(): void {
const cheio = this.toNumber(this.model.valorCheio);
const desconto = this.toNumber(this.model.desconto);
if (cheio === null) {
this.model.valorComDesconto = '';
this.syncMonthValues();
return;
}
const calc = Math.max(0, cheio - (desconto ?? 0));
this.model.valorComDesconto = this.formatInput(calc);
this.syncMonthValues();
}
onCompetenciaChange(): void {
this.syncMonthValues();
}
onValorComDescontoChange(): void {
this.syncMonthValues();
}
onParcelaChange(): void {
this.syncQtParcelas();
this.syncMonthValues();
}
onQtParcelasChange(): void {
const parsed = this.parseQtParcelas(this.model.qtParcelas);
if (parsed) {
this.model.parcelaAtual = parsed.atual;
this.model.totalParcelas = parsed.total;
}
this.syncMonthValues();
}
get competenciaFinalLabel(): string {
if (this.model.monthValues?.length) {
const last = this.model.monthValues[this.model.monthValues.length - 1];
return this.formatCompetenciaLabel(last.competencia);
}
const total = this.model.totalParcelas ?? 0;
const ano = this.model.competenciaAno ?? 0;
const mes = this.model.competenciaMes ?? 0;
if (!total || !ano || !mes) return '-';
const index = (mes - 1) + (total - 1);
const finalAno = ano + Math.floor(index / 12);
const finalMes = (index % 12) + 1;
return `${String(finalMes).padStart(2, '0')}/${finalAno}`;
}
onPreviewValueChange(competencia: string, value: string): void {
const list = this.model.monthValues ?? [];
const item = list.find((entry) => entry.competencia === competencia);
if (item) item.valor = value ?? '';
const row = this.previewRows.find((entry) => entry.competencia === competencia);
if (row) row.valor = value ?? '';
}
trackByPreview(_: number, row: PreviewRow): string {
return row.competencia;
}
get isValid(): boolean {
return !!(
this.model.anoRef &&
this.model.item &&
this.model.linha?.trim() &&
this.model.cliente?.trim() &&
this.model.totalParcelas &&
this.model.totalParcelas > 0 &&
this.model.valorCheio &&
this.model.competenciaAno &&
this.model.competenciaMes
);
}
onSave(): void {
this.touched = true;
if (!this.isValid) return;
this.save.emit(this.model);
}
private syncQtParcelas(): void {
const atual = this.model.parcelaAtual;
const total = this.model.totalParcelas;
if (atual && total) {
this.model.qtParcelas = `${atual}/${total}`;
}
}
private syncMonthValues(): void {
const total = this.model.totalParcelas ?? 0;
const ano = this.model.competenciaAno ?? 0;
const mes = this.model.competenciaMes ?? 0;
if (!total || !ano || !mes) {
this.model.monthValues = [];
this.previewRows = [];
return;
}
const existing = new Map<string, string>();
(this.model.monthValues ?? []).forEach((m) => {
if (m?.competencia) existing.set(m.competencia, m.valor ?? '');
});
const valorTotal = this.toNumber(this.model.valorComDesconto) ?? this.toNumber(this.model.valorCheio);
const valorParcela = valorTotal !== null ? valorTotal / total : null;
const defaultValor = valorParcela !== null ? this.formatInput(valorParcela) : '';
const list: Array<{ competencia: string; valor: string }> = [];
for (let i = 0; i < total; i++) {
const index = (mes - 1) + i;
const y = ano + Math.floor(index / 12);
const m = (index % 12) + 1;
const competencia = `${y}-${String(m).padStart(2, '0')}-01`;
list.push({
competencia,
valor: existing.get(competencia) ?? defaultValor,
});
}
this.model.monthValues = list;
this.rebuildPreviewRows();
}
private rebuildPreviewRows(): void {
const list = this.model?.monthValues ?? [];
if (!list.length) {
this.previewRows = [];
return;
}
this.previewRows = list.slice(0, 36).map((item, idx) => ({
competencia: item.competencia,
label: this.formatCompetenciaLabel(item.competencia),
parcela: idx + 1,
valor: item.valor ?? '',
}));
}
private formatCompetenciaLabel(value: string): string {
const match = value.match(/^(\d{4})-(\d{2})/);
if (!match) return value || '-';
return `${match[2]}/${match[1]}`;
}
private parseQtParcelas(raw: string | null | undefined): { atual: number; total: number } | null {
if (!raw) return null;
const parts = raw.split('/');
if (parts.length < 2) return null;
const atualStr = this.onlyDigits(parts[0]);
const totalStr = this.onlyDigits(parts[1]);
if (!atualStr || !totalStr) return null;
return { atual: Number(atualStr), total: Number(totalStr) };
}
private toNumber(value: any): number | null {
if (value === null || value === undefined || value === '') return null;
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
const raw = String(value).trim();
if (!raw) return null;
let cleaned = raw.replace(/[^\d,.-]/g, '');
if (cleaned.includes(',') && cleaned.includes('.')) {
if (cleaned.lastIndexOf(',') > cleaned.lastIndexOf('.')) {
cleaned = cleaned.replace(/\./g, '').replace(',', '.');
} else {
cleaned = cleaned.replace(/,/g, '');
}
} else if (cleaned.includes(',')) {
cleaned = cleaned.replace(/\./g, '').replace(',', '.');
} else {
cleaned = cleaned.replace(/,/g, '');
}
const n = Number(cleaned);
return Number.isNaN(n) ? null : n;
}
private onlyDigits(value: string): string {
let out = '';
for (const ch of value ?? '') {
if (ch >= '0' && ch <= '9') out += ch;
}
return out;
}
private formatInput(value: number): string {
return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
}
}