Añade impermeabilización, extras fijos y zonas al motor de presupuesto
Acerca el cálculo a tarifas de mercado sin rehacer el modelo lineal €/m²: - Impermeabilización como partida propia en zonas húmedas (cocina/baño/integral) - Extras fijos que no escalan con m²: boletín (siempre), tuberías (piso anterior a 2000) y cambio de distribución (mover inodoro/ducha/bañera) - Intensidad por tipo en fontanería/electricidad (baseline cocina) para que un integral no escale como un baño - Factor de zona por provincia en tramos (Madrid/BCN 1.40, islas 1.30, capitales 1.20, rural 0.85, resto 1.00) - 2 preguntas nuevas en el formulario del cliente para disparar los extras - Panel de precios: campo de impermeabilización + sección de extras fijos - Seed recalibrado (mano de obra, extras, catálogo suelo/pared) - Migración 0009 (leads.anterior_a_2000, leads.cambio_distribucion, pricing_config.extras) - Tests del motor ampliados (impermeabilización, extras, intensidad por tipo) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,27 @@ import type {
|
||||
CatalogItem,
|
||||
PartidaKey,
|
||||
PricingConfig,
|
||||
TipoReforma,
|
||||
} from './types';
|
||||
|
||||
const LICENCIA_MIN = 30000; // 300 €
|
||||
const LICENCIA_MAX = 150000; // 1.500 €
|
||||
|
||||
// Zonas húmedas: las únicas que llevan impermeabilización.
|
||||
const WET = new Set<TipoReforma>(['cocina', 'bano', 'integral']);
|
||||
|
||||
// Intensidad de instalaciones (fontanería/electricidad) por m² según el tipo de reforma.
|
||||
// Baseline cocina = 1.0. Un baño concentra más instalaciones por m²; un salón o un piso
|
||||
// integral las diluye. Corrige el sesgo del modelo lineal €/m² sin rehacerlo.
|
||||
const TIPO_INTENSIDAD: Record<TipoReforma, number> = {
|
||||
cocina: 1.0,
|
||||
bano: 1.3,
|
||||
integral: 0.45,
|
||||
salon: 0.4,
|
||||
comedor: 0.4,
|
||||
otro: 0.7,
|
||||
};
|
||||
|
||||
// A qué partida contribuye el material de cada categoría.
|
||||
const MATERIAL_PARTIDA: Record<CategoriaMaterial, PartidaKey> = {
|
||||
suelo: 'alicatado',
|
||||
@@ -34,12 +50,14 @@ export function computeBudget(
|
||||
|
||||
const importes: Record<PartidaKey, number> = {
|
||||
demolicion: 0,
|
||||
impermeabilizacion: 0,
|
||||
alicatado: 0,
|
||||
fontaneria: 0,
|
||||
electricidad: 0,
|
||||
carpinteria: 0,
|
||||
mano_de_obra: 0,
|
||||
extras: 0,
|
||||
extras_fijos: 0,
|
||||
licencia: 0,
|
||||
};
|
||||
|
||||
@@ -67,11 +85,25 @@ export function computeBudget(
|
||||
if (item.descriptorRender) materialesRender.push(item.descriptorRender);
|
||||
}
|
||||
|
||||
const intensidad = TIPO_INTENSIDAD[inputs.tipoReforma] ?? 1;
|
||||
importes.demolicion += Math.round(cant.m2Suelo * config.manoObra.demolicion);
|
||||
importes.fontaneria += Math.round(cant.m2Suelo * config.manoObra.fontaneria);
|
||||
importes.electricidad += Math.round(cant.m2Suelo * config.manoObra.electricidad);
|
||||
importes.fontaneria += Math.round(cant.m2Suelo * config.manoObra.fontaneria * intensidad);
|
||||
importes.electricidad += Math.round(cant.m2Suelo * config.manoObra.electricidad * intensidad);
|
||||
importes.mano_de_obra += Math.round(cant.m2Suelo * config.manoObra.mano_de_obra);
|
||||
|
||||
// Impermeabilización: solo en zonas húmedas, proporcional al suelo a tratar.
|
||||
if (WET.has(inputs.tipoReforma)) {
|
||||
importes.impermeabilizacion += Math.round(cant.m2Suelo * config.manoObra.impermeabilizacion);
|
||||
}
|
||||
|
||||
// Extras fijos (no escalan con m²). El boletín eléctrico es siempre obligatorio.
|
||||
const extras = config.extras;
|
||||
if (extras) {
|
||||
importes.extras_fijos += extras.boletin;
|
||||
if (inputs.anteriorA2000) importes.extras_fijos += extras.tuberias;
|
||||
if (inputs.cambioDistribucion) importes.extras_fijos += extras.distribucion;
|
||||
}
|
||||
|
||||
if (inputs.estructural) importes.licencia += LICENCIA_MIN;
|
||||
|
||||
const partidas = PARTIDA_ORDER.filter((k) => importes[k] > 0).map((k) => ({
|
||||
|
||||
@@ -2,22 +2,26 @@ import type { PartidaKey } from './types';
|
||||
|
||||
export const PARTIDA_ORDER: PartidaKey[] = [
|
||||
'demolicion',
|
||||
'impermeabilizacion',
|
||||
'alicatado',
|
||||
'fontaneria',
|
||||
'electricidad',
|
||||
'carpinteria',
|
||||
'mano_de_obra',
|
||||
'extras',
|
||||
'extras_fijos',
|
||||
'licencia',
|
||||
];
|
||||
|
||||
export const PARTIDA_LABEL: Record<PartidaKey, string> = {
|
||||
demolicion: 'Demolición',
|
||||
impermeabilizacion: 'Impermeabilización',
|
||||
alicatado: 'Alicatado y solado',
|
||||
fontaneria: 'Fontanería',
|
||||
electricidad: 'Electricidad',
|
||||
carpinteria: 'Carpintería y mobiliario',
|
||||
mano_de_obra: 'Mano de obra',
|
||||
extras: 'Pintura y extras',
|
||||
extras_fijos: 'Extras (tuberías, boletín, distribución)',
|
||||
licencia: 'Licencia + Proyecto técnico',
|
||||
};
|
||||
|
||||
@@ -5,15 +5,22 @@ export type TipoReforma = 'cocina' | 'bano' | 'salon' | 'comedor' | 'integral' |
|
||||
|
||||
export type PartidaKey =
|
||||
| 'demolicion'
|
||||
| 'impermeabilizacion'
|
||||
| 'alicatado'
|
||||
| 'fontaneria'
|
||||
| 'electricidad'
|
||||
| 'carpinteria'
|
||||
| 'mano_de_obra'
|
||||
| 'extras'
|
||||
| 'extras_fijos'
|
||||
| 'licencia';
|
||||
|
||||
export type ManoObraKey = 'demolicion' | 'fontaneria' | 'electricidad' | 'mano_de_obra';
|
||||
export type ManoObraKey =
|
||||
| 'demolicion'
|
||||
| 'impermeabilizacion'
|
||||
| 'fontaneria'
|
||||
| 'electricidad'
|
||||
| 'mano_de_obra';
|
||||
|
||||
export interface CatalogItem {
|
||||
id: string;
|
||||
@@ -27,10 +34,18 @@ export interface CatalogItem {
|
||||
sku: string;
|
||||
}
|
||||
|
||||
// Extras fijos que no escalan con los m² (céntimos). Se aplican según el estado del piso.
|
||||
export interface ExtrasFijos {
|
||||
tuberias: number; // renovación de tuberías (pisos anteriores a 2000)
|
||||
boletin: number; // boletín eléctrico (siempre obligatorio)
|
||||
distribucion: number; // cambio de distribución (mover inodoro/ducha/bañera)
|
||||
}
|
||||
|
||||
export interface PricingConfig {
|
||||
alturaTechoDefault: number; // metros
|
||||
factorZona: Record<string, number>; // provincia -> multiplicador
|
||||
manoObra: Record<ManoObraKey, number>; // céntimos por m² de suelo
|
||||
extras?: ExtrasFijos; // importes fijos en céntimos
|
||||
}
|
||||
|
||||
export interface BudgetInputs {
|
||||
@@ -41,6 +56,8 @@ export interface BudgetInputs {
|
||||
estructural: boolean;
|
||||
provincia: string | null;
|
||||
materialSelections: Partial<Record<CategoriaMaterial, string>>; // categoria -> catalogItemId
|
||||
anteriorA2000?: boolean; // dispara el extra de renovación de tuberías
|
||||
cambioDistribucion?: boolean; // dispara el extra de cambio de distribución
|
||||
}
|
||||
|
||||
export interface PartidaResult {
|
||||
|
||||
Reference in New Issue
Block a user