254 lines
7.6 KiB
TypeScript
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);
|
|
}
|
|
}
|