Adelanta de F1.5 a F2 la validación pre-envío: el panel permite elegir modo de envío (automático/revisión), editar los conceptos del presupuesto y enviar al cliente por WhatsApp (simulado). Añade datos de empresa y logo configurables en /panel/empresa y genera el presupuesto como PDF real descargable con esa marca vía @react-pdf/renderer, sustituyendo la vista HTML imprimible. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { applyConceptoEdits, AVISO_EDITADO } from '@/budget/edit';
|
|
import type { BudgetResult } from '@/budget/types';
|
|
|
|
const base: BudgetResult = {
|
|
partidas: [
|
|
{ key: 'demolicion', label: 'Demolición', importe: 100000 },
|
|
{ key: 'alicatado', label: 'Alicatado y solado', importe: 200000 },
|
|
],
|
|
subtotal: 300000,
|
|
factorZona: 1.2,
|
|
total: 360000,
|
|
rango: { min: 324000, max: 396000 },
|
|
confianza: 'media',
|
|
materialesRender: ['azulejo blanco'],
|
|
avisos: ['Sin precio para pintura'],
|
|
};
|
|
|
|
describe('applyConceptoEdits', () => {
|
|
it('recalcula subtotal y total aplicando el factor de zona', () => {
|
|
const result = applyConceptoEdits(base, [
|
|
{ key: 'demolicion', label: 'Demolición', importe: 150000 },
|
|
{ key: 'alicatado', label: 'Alicatado y solado', importe: 200000 },
|
|
]);
|
|
expect(result.subtotal).toBe(350000);
|
|
expect(result.total).toBe(420000); // 350000 * 1.2
|
|
});
|
|
|
|
it('permite añadir partidas libres y quitar existentes', () => {
|
|
const result = applyConceptoEdits(base, [
|
|
{ key: 'demolicion', label: 'Demolición', importe: 100000 },
|
|
{ key: 'custom-1', label: 'Imprevistos de obra', importe: 50000 },
|
|
]);
|
|
expect(result.partidas).toHaveLength(2);
|
|
expect(result.partidas[1]).toEqual({ key: 'custom-1', label: 'Imprevistos de obra', importe: 50000 });
|
|
expect(result.subtotal).toBe(150000);
|
|
});
|
|
|
|
it('descarta partidas sin etiqueta y normaliza importes (>=0, enteros)', () => {
|
|
const result = applyConceptoEdits(base, [
|
|
{ key: 'a', label: ' ', importe: 999 },
|
|
{ key: 'b', label: 'Válida', importe: -500 },
|
|
{ key: 'c', label: 'Decimal', importe: 123.7 },
|
|
]);
|
|
expect(result.partidas).toEqual([
|
|
{ key: 'b', label: 'Válida', importe: 0 },
|
|
{ key: 'c', label: 'Decimal', importe: 124 },
|
|
]);
|
|
});
|
|
|
|
it('marca el presupuesto como ajustado a mano (aviso idempotente, confianza alta, rango = total)', () => {
|
|
const once = applyConceptoEdits(base, base.partidas);
|
|
expect(once.confianza).toBe('alta');
|
|
expect(once.avisos).toContain(AVISO_EDITADO);
|
|
expect(once.rango).toEqual({ min: once.total, max: once.total });
|
|
|
|
const twice = applyConceptoEdits(once, once.partidas);
|
|
expect(twice.avisos.filter((a) => a === AVISO_EDITADO)).toHaveLength(1);
|
|
});
|
|
});
|