Añade baremo de rentabilidad (valor de panel) e indicador en la ficha del lead
Parte C del plan: el baremo mínimo de rentabilidad es ahora un valor configurable del reformista, solo informativo. Los agentes NO lo usan para decidir nada. - schema: pricing_config.baremo_minimo (céntimos, nullable) + migración 0012. - pricing-queries / budget types: exponen baremoMinimo. - panel/precios: sección "Baremo de rentabilidad" + action actualizarBaremo (vacío = sin baremo). - panel/[id]: el presupuesto estimado se muestra en rojo con aviso "Por debajo de tu baremo (X €)" cuando no alcanza el baremo del tenant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,7 @@ CREATE TABLE "pricing_config" (
|
||||
"factor_zona" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"mano_obra" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"extras" jsonb DEFAULT '{"tuberias":0,"boletin":0,"distribucion":0}'::jsonb NOT NULL,
|
||||
"baremo_minimo" integer,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "pricing_config_tenant_id_unique" UNIQUE("tenant_id")
|
||||
);
|
||||
|
||||
1
mvp/b2c/drizzle/0012_lame_sentinel.sql
Normal file
1
mvp/b2c/drizzle/0012_lame_sentinel.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "pricing_config" ADD COLUMN "baremo_minimo" integer;
|
||||
2458
mvp/b2c/drizzle/meta/0012_snapshot.json
Normal file
2458
mvp/b2c/drizzle/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,13 @@
|
||||
"when": 1780593183911,
|
||||
"tag": "0011_warm_post",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "7",
|
||||
"when": 1781189893331,
|
||||
"tag": "0012_lame_sentinel",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link';
|
||||
import { headers } from 'next/headers';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getLead } from '@/db/queries';
|
||||
import { getPricingConfigFor } from '@/db/pricing-queries';
|
||||
import EstadoControl from '@/components/panel/EstadoControl';
|
||||
import ConceptosEditor from '@/components/panel/ConceptosEditor';
|
||||
import OpinionLinkBox from '@/components/panel/OpinionLinkBox';
|
||||
@@ -41,6 +42,14 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id:
|
||||
const prefs = lead.preferencesSnapshot as import('@/lib/voice/preferences').AbstractedPreferences | null;
|
||||
const yaEnviado = lead.pipelineStage === 'whatsapp_entregado';
|
||||
|
||||
// Baremo de rentabilidad del reformista (informativo): si el presupuesto estimado no lo alcanza,
|
||||
// se marca en rojo. null = sin baremo o sin presupuesto aún (no se marca nada).
|
||||
const baremoMinimo = (await getPricingConfigFor(lead.tenantId)).baremoMinimo ?? null;
|
||||
const pasaBaremo =
|
||||
baremoMinimo != null && lead.presupuestoEstimado != null
|
||||
? lead.presupuestoEstimado >= baremoMinimo
|
||||
: null;
|
||||
|
||||
const h = await headers();
|
||||
const host = h.get('x-forwarded-host') ?? h.get('host') ?? 'reformix.dv3.com.es';
|
||||
const proto = h.get('x-forwarded-proto') ?? 'https';
|
||||
@@ -64,7 +73,14 @@ export default async function LeadDetailPage({ params }: { params: Promise<{ id:
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-gray-400">Presupuesto estimado</div>
|
||||
<div className="text-2xl font-black text-black">{formatEuros(lead.presupuestoEstimado)}</div>
|
||||
<div className={`text-2xl font-black ${pasaBaremo === false ? 'text-red-600' : 'text-black'}`}>
|
||||
{formatEuros(lead.presupuestoEstimado)}
|
||||
</div>
|
||||
{pasaBaremo === false && baremoMinimo != null && (
|
||||
<div className="mt-0.5 text-xs font-semibold text-red-600">
|
||||
Por debajo de tu baremo ({formatEuros(baremoMinimo)})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EstadoControl
|
||||
|
||||
@@ -93,6 +93,19 @@ export async function actualizarExtras(formData: FormData) {
|
||||
revalidatePath('/panel/precios');
|
||||
}
|
||||
|
||||
export async function actualizarBaremo(formData: FormData) {
|
||||
const tenantId = await getTenantId();
|
||||
const raw = formData.get('baremoMinimo');
|
||||
const txt = typeof raw === 'string' ? raw.trim() : '';
|
||||
// Vacío = sin baremo (null). Con valor = euros → céntimos.
|
||||
const baremoMinimo = txt === '' ? null : eurosToCents(raw, 'baremo de rentabilidad');
|
||||
await db
|
||||
.update(pricingConfig)
|
||||
.set({ baremoMinimo, 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');
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
borrarMaterial,
|
||||
actualizarConfig,
|
||||
actualizarExtras,
|
||||
actualizarBaremo,
|
||||
actualizarEnvio,
|
||||
importarCatalogoCsv,
|
||||
} from './actions';
|
||||
@@ -120,6 +121,34 @@ export default async function PreciosPage() {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* Baremo de rentabilidad */}
|
||||
<section className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h2 className="font-bold text-black mb-1">Baremo de rentabilidad</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Importe mínimo de un trabajo para que te resulte rentable. Es solo orientativo para ti: en la
|
||||
ficha de cada lead verás marcados en otro color los presupuestos que no lleguen a este valor.
|
||||
No afecta a lo que ve el cliente ni a la conversación de los agentes. Déjalo vacío para no usar
|
||||
baremo.
|
||||
</p>
|
||||
<form action={actualizarBaremo} className="flex flex-wrap items-end gap-3">
|
||||
<label className="text-sm">
|
||||
<span className="block text-xs text-gray-500 mb-1">Baremo mínimo (€)</span>
|
||||
<input
|
||||
name="baremoMinimo"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
defaultValue={config.baremoMinimo != null ? config.baremoMinimo / 100 : ''}
|
||||
placeholder="Sin baremo"
|
||||
className="w-40 border border-gray-300 rounded-lg px-2 py-1.5"
|
||||
/>
|
||||
</label>
|
||||
<button className="bg-primary-700 text-white rounded-lg px-4 py-2 text-sm font-medium">
|
||||
Guardar baremo
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface PricingConfig {
|
||||
factorZona: Record<string, number>; // provincia -> multiplicador
|
||||
manoObra: Record<ManoObraKey, number>; // céntimos por m² de suelo
|
||||
extras?: ExtrasFijos; // importes fijos en céntimos
|
||||
baremoMinimo?: number | null; // céntimos; trabajo mínimo rentable (informativo, no lo usan los agentes)
|
||||
}
|
||||
|
||||
export interface BudgetInputs {
|
||||
|
||||
@@ -42,6 +42,7 @@ export async function getPricingConfigFor(tenantId: string): Promise<PricingConf
|
||||
factorZona: {},
|
||||
manoObra: { ...MANO_OBRA_DEFAULT },
|
||||
extras: { ...EXTRAS_DEFAULT },
|
||||
baremoMinimo: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -49,6 +50,7 @@ export async function getPricingConfigFor(tenantId: string): Promise<PricingConf
|
||||
factorZona: row.factorZona,
|
||||
manoObra: { ...MANO_OBRA_DEFAULT, ...(row.manoObra as Record<ManoObraKey, number>) },
|
||||
extras: { ...EXTRAS_DEFAULT, ...(row.extras ?? {}) },
|
||||
baremoMinimo: row.baremoMinimo ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -396,6 +396,10 @@ export const pricingConfig = pgTable('pricing_config', {
|
||||
.$type<{ tuberias: number; boletin: number; distribucion: number }>()
|
||||
.notNull()
|
||||
.default({ tuberias: 0, boletin: 0, distribucion: 0 }),
|
||||
// Baremo de rentabilidad (céntimos): importe mínimo que el reformista considera rentable. Solo
|
||||
// informativo en el panel (marca en otro color los leads por debajo); los agentes NO lo usan para
|
||||
// decidir nada. Null = sin baremo configurado.
|
||||
baremoMinimo: integer('baremo_minimo'),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user