Médicos: badges de ventas 30d al estilo legacy
- Píldora verde 'N ventas' + gris 'N recomendaciones' y píldoras índigo apiladas (usd / m.n. / tarjeta m.n.) en una sola columna - recomendaciones_30d: pacientes captados con 'recomendado por'
This commit is contained in:
@@ -13,11 +13,41 @@ const isMedicalTitle = (jobTitle?: string) => {
|
|||||||
return medicalTerms.some((term) => normalized.includes(term));
|
return medicalTerms.some((term) => normalized.includes(term));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fmtMoney = (n: number) =>
|
const fmtNum = (n: number | null | undefined, decimals = 2) =>
|
||||||
(n || 0).toLocaleString('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 0 });
|
(n ?? 0).toLocaleString('es-MX', { maximumFractionDigits: decimals });
|
||||||
|
|
||||||
const fmtUsd = (n: number | null | undefined) =>
|
/** Badges de ventas 30d al estilo del sistema legacy:
|
||||||
n == null ? '—' : n.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
|
* píldora verde "N ventas" + gris "N recomendaciones" y
|
||||||
|
* píldoras índigo apiladas: usd / m.n. / tarjeta m.n. */
|
||||||
|
const StatBadges: FC<{ d: Doctor }> = ({ d }) => (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 mb-1.5">
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1.5 rounded px-2 py-0.5 text-xs font-medium text-white"
|
||||||
|
style={{ backgroundColor: '#00a65a' }}
|
||||||
|
>
|
||||||
|
<span className="font-bold">{d.ventas_30d ?? 0}</span> ventas
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1.5 rounded px-2 py-0.5 text-xs font-medium"
|
||||||
|
style={{ backgroundColor: '#bdc3c7', color: '#333' }}
|
||||||
|
>
|
||||||
|
<span className="font-bold">{d.recomendaciones_30d ?? 0}</span> recomendaciones
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<span className="block w-fit rounded px-1.5 py-0.5 text-[11px] text-white" style={{ backgroundColor: '#605ca8' }}>
|
||||||
|
{d.total_30d_usd == null ? '—' : fmtNum(d.total_30d_usd)} usd
|
||||||
|
</span>
|
||||||
|
<span className="block w-fit rounded px-1.5 py-0.5 text-[11px] text-white" style={{ backgroundColor: '#605ca8' }}>
|
||||||
|
{fmtNum(d.total_30d ?? 0, 0)} m.n.
|
||||||
|
</span>
|
||||||
|
<span className="block w-fit rounded px-1.5 py-0.5 text-[11px] text-white" style={{ backgroundColor: '#605ca8' }}>
|
||||||
|
{fmtNum(d.card_30d ?? 0, 0)} tarjeta m.n.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const Medicos: FC = () => {
|
const Medicos: FC = () => {
|
||||||
const [employees, setEmployees] = useState<Doctor[]>([]);
|
const [employees, setEmployees] = useState<Doctor[]>([]);
|
||||||
@@ -95,10 +125,7 @@ const Medicos: FC = () => {
|
|||||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">Teléfono</th>
|
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">Teléfono</th>
|
||||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden lg:table-cell">Email</th>
|
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden lg:table-cell">Email</th>
|
||||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">% Comisión</th>
|
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">% Comisión</th>
|
||||||
<th className="text-right p-3 text-xs font-medium text-theme-muted uppercase" title="Ventas con líneas recetadas por el médico, últimos 30 días">Ventas 30d</th>
|
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase" title="Ventas con líneas recetadas por el médico, últimos 30 días">Últimos 30 días</th>
|
||||||
<th className="text-right p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell" title="Subtotal de líneas recetadas, últimos 30 días">Total 30d</th>
|
|
||||||
<th className="text-right p-3 text-xs font-medium text-theme-muted uppercase hidden lg:table-cell" title="Equivalente en USD al TC vigente">Total USD</th>
|
|
||||||
<th className="text-right p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell" title="Pagado con tarjeta (terminal física), últimos 30 días">Pagado tarjeta</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y">
|
<tbody className="divide-y">
|
||||||
@@ -144,10 +171,9 @@ const Medicos: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-sm text-right font-medium text-theme-heading">{d.ventas_30d ?? 0}</td>
|
<td className="p-3 align-top">
|
||||||
<td className="p-3 text-sm text-right text-theme-heading hidden md:table-cell">{fmtMoney(d.total_30d ?? 0)}</td>
|
<StatBadges d={d} />
|
||||||
<td className="p-3 text-sm text-right text-theme-muted hidden lg:table-cell">{fmtUsd(d.total_30d_usd)}</td>
|
</td>
|
||||||
<td className="p-3 text-sm text-right text-theme-heading hidden md:table-cell">{fmtMoney(d.card_30d ?? 0)}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -169,10 +195,7 @@ const Medicos: FC = () => {
|
|||||||
{ label: 'Teléfono', value: d.work_phone || '-' },
|
{ label: 'Teléfono', value: d.work_phone || '-' },
|
||||||
{ label: 'Email', value: d.work_email || '-' },
|
{ label: 'Email', value: d.work_email || '-' },
|
||||||
{ label: '% Comisión', value: `${d.commission_pct ?? 0}%` },
|
{ label: '% Comisión', value: `${d.commission_pct ?? 0}%` },
|
||||||
{ label: 'Ventas 30d', value: String(d.ventas_30d ?? 0) },
|
{ label: 'Últimos 30 días', value: <StatBadges d={d} /> },
|
||||||
{ label: 'Total 30d', value: fmtMoney(d.total_30d ?? 0) },
|
|
||||||
{ label: 'Total USD', value: fmtUsd(d.total_30d_usd) },
|
|
||||||
{ label: 'Pagado tarjeta', value: fmtMoney(d.card_30d ?? 0) },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export interface Doctor {
|
|||||||
work_email: string;
|
work_email: string;
|
||||||
commission_pct: number;
|
commission_pct: number;
|
||||||
patient_count?: number;
|
patient_count?: number;
|
||||||
|
recomendaciones_30d?: number;
|
||||||
ventas_30d?: number;
|
ventas_30d?: number;
|
||||||
total_30d?: number;
|
total_30d?: number;
|
||||||
total_30d_usd?: number | null;
|
total_30d_usd?: number | null;
|
||||||
|
|||||||
@@ -1657,6 +1657,16 @@ class SkeenFrontendController(http.Controller):
|
|||||||
|
|
||||||
# ---- Ventas de los últimos 30 días por médico ----
|
# ---- Ventas de los últimos 30 días por médico ----
|
||||||
since = (date.today() - timedelta(days=30)).strftime('%Y-%m-%d')
|
since = (date.today() - timedelta(days=30)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# Recomendaciones: pacientes captados en 30d asignados al médico
|
||||||
|
# con "recomendado por" capturado (misma métrica visible del legacy)
|
||||||
|
recomendaciones = {}
|
||||||
|
for g in request.env['res.partner'].sudo().read_group(
|
||||||
|
[('is_patient', '=', True), ('primary_doctor_id', 'in', doctors.ids),
|
||||||
|
('referred_by', 'not in', [False, '']), ('create_date', '>=', since)],
|
||||||
|
[], ['primary_doctor_id']):
|
||||||
|
recomendaciones[g['primary_doctor_id'][0]] = g['primary_doctor_id_count']
|
||||||
|
|
||||||
stats = {} # doctor_id -> {'ventas': set, 'total': float}
|
stats = {} # doctor_id -> {'ventas': set, 'total': float}
|
||||||
card_by_venta = {}
|
card_by_venta = {}
|
||||||
ventas = request.env['skeen.venta'].sudo().search([
|
ventas = request.env['skeen.venta'].sudo().search([
|
||||||
@@ -1700,6 +1710,7 @@ class SkeenFrontendController(http.Controller):
|
|||||||
'work_email': d.work_email or '',
|
'work_email': d.work_email or '',
|
||||||
'commission_pct': d.commission_pct or 0.0,
|
'commission_pct': d.commission_pct or 0.0,
|
||||||
'patient_count': counts.get(d.id, 0),
|
'patient_count': counts.get(d.id, 0),
|
||||||
|
'recomendaciones_30d': recomendaciones.get(d.id, 0),
|
||||||
'ventas_30d': len(stats.get(d.id, {}).get('ventas', set())),
|
'ventas_30d': len(stats.get(d.id, {}).get('ventas', set())),
|
||||||
'total_30d': round(stats.get(d.id, {}).get('total', 0.0), 2),
|
'total_30d': round(stats.get(d.id, {}).get('total', 0.0), 2),
|
||||||
'total_30d_usd': round(stats.get(d.id, {}).get('total', 0.0) / rate, 2) if rate else None,
|
'total_30d_usd': round(stats.get(d.id, {}).get('total', 0.0) / rate, 2) if rate else None,
|
||||||
|
|||||||
Reference in New Issue
Block a user