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:
@@ -66,6 +66,7 @@ export async function actualizarConfig(formData: FormData) {
|
||||
alturaTechoDefault: parsePositive(formData.get('alturaTechoDefault'), 'altura de techo'),
|
||||
manoObra: {
|
||||
demolicion: eurosToCents(formData.get('mo_demolicion'), 'demolición'),
|
||||
impermeabilizacion: eurosToCents(formData.get('mo_impermeabilizacion'), 'impermeabilización'),
|
||||
fontaneria: eurosToCents(formData.get('mo_fontaneria'), 'fontanería'),
|
||||
electricidad: eurosToCents(formData.get('mo_electricidad'), 'electricidad'),
|
||||
mano_de_obra: eurosToCents(formData.get('mo_mano_de_obra'), 'mano de obra'),
|
||||
@@ -76,6 +77,22 @@ export async function actualizarConfig(formData: FormData) {
|
||||
revalidatePath('/panel/precios');
|
||||
}
|
||||
|
||||
export async function actualizarExtras(formData: FormData) {
|
||||
const tenantId = await getTenantId();
|
||||
await db
|
||||
.update(pricingConfig)
|
||||
.set({
|
||||
extras: {
|
||||
tuberias: eurosToCents(formData.get('extra_tuberias'), 'renovación de tuberías'),
|
||||
boletin: eurosToCents(formData.get('extra_boletin'), 'boletín eléctrico'),
|
||||
distribucion: eurosToCents(formData.get('extra_distribucion'), 'cambio de distribución'),
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(pricingConfig.tenantId, tenantId));
|
||||
revalidatePath('/panel/precios');
|
||||
}
|
||||
|
||||
export async function actualizarEnvio(formData: FormData) {
|
||||
const tenantId = await getTenantId();
|
||||
const modo = formData.get('modo');
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
actualizarPrecio,
|
||||
borrarMaterial,
|
||||
actualizarConfig,
|
||||
actualizarExtras,
|
||||
actualizarEnvio,
|
||||
importarCatalogoCsv,
|
||||
} from './actions';
|
||||
@@ -96,6 +97,7 @@ export default async function PreciosPage() {
|
||||
{(
|
||||
[
|
||||
['demolicion', 'Demolición'],
|
||||
['impermeabilizacion', 'Impermeabilización'],
|
||||
['fontaneria', 'Fontanería'],
|
||||
['electricidad', 'Electricidad'],
|
||||
['mano_de_obra', 'Mano de obra'],
|
||||
@@ -118,6 +120,39 @@ export default async function PreciosPage() {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* Extras fijos */}
|
||||
<section className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h2 className="font-bold text-black mb-1">Extras fijos</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Importes fijos que no escalan con los metros. El boletín eléctrico se aplica siempre; las
|
||||
tuberías solo en pisos anteriores al año 2000 y la distribución al mover inodoro, ducha o
|
||||
bañera.
|
||||
</p>
|
||||
<form action={actualizarExtras} className="grid grid-cols-2 md:grid-cols-3 gap-3 items-end">
|
||||
{(
|
||||
[
|
||||
['tuberias', 'Renovación de tuberías'],
|
||||
['boletin', 'Boletín eléctrico'],
|
||||
['distribucion', 'Cambio de distribución'],
|
||||
] as const
|
||||
).map(([k, etiqueta]) => (
|
||||
<label key={k} className="text-sm">
|
||||
<span className="block text-xs text-gray-500 mb-1">{etiqueta} (€)</span>
|
||||
<input
|
||||
name={`extra_${k}`}
|
||||
type="number"
|
||||
step="0.01"
|
||||
defaultValue={(config.extras?.[k] ?? 0) / 100}
|
||||
className="w-full border border-gray-300 rounded-lg px-2 py-1.5"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<button className="col-span-2 md:col-span-3 justify-self-start bg-primary-700 text-white rounded-lg px-4 py-2 text-sm font-medium">
|
||||
Guardar extras
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* Catálogo por categoría */}
|
||||
{CATEGORIAS.map((categoria) => {
|
||||
const items = catalog.filter((c) => c.categoria === categoria);
|
||||
|
||||
@@ -146,6 +146,8 @@ export async function guardarDetallesYFotos(leadId: string, formData: FormData):
|
||||
const presupuestoTarget =
|
||||
Number.isFinite(targetEuros) && targetEuros > 0 ? Math.round(targetEuros * 100) : null;
|
||||
const estructural = formData.get('estructural') === 'on';
|
||||
const anteriorA2000 = formData.get('anteriorA2000') === 'on';
|
||||
const cambioDistribucion = formData.get('cambioDistribucion') === 'on';
|
||||
|
||||
let zonas = await parsearZonas(formData);
|
||||
if (zonas.length === 0) {
|
||||
@@ -191,6 +193,8 @@ export async function guardarDetallesYFotos(leadId: string, formData: FormData):
|
||||
urgencia,
|
||||
presupuestoTarget,
|
||||
estructural,
|
||||
anteriorA2000,
|
||||
cambioDistribucion,
|
||||
tasteText,
|
||||
pipelineStage: 'fotos_subidas',
|
||||
updatedAt: new Date(),
|
||||
|
||||
Reference in New Issue
Block a user