From 339ba235b130c78b1d55ac9ee7d20419b4633c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Consultor=C3=ADa=20Alcaraz=20Salazar?= Date: Mon, 31 Aug 2026 04:45:45 +0000 Subject: [PATCH] Configuracion: seccion Comisiones con % global default y % editable por doctor --- frontend/src/pages/Configuracion.tsx | 152 +++++++++++++++++- frontend/src/services/odoo.ts | 15 ++ .../skeen_whatsapp/controllers/frontend.py | 51 ++++++ 3 files changed, 216 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Configuracion.tsx b/frontend/src/pages/Configuracion.tsx index 58e2709..49d6663 100644 --- a/frontend/src/pages/Configuracion.tsx +++ b/frontend/src/pages/Configuracion.tsx @@ -1,14 +1,14 @@ import type { FC } from 'react'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Phone, MapPin, Mail, Globe, Save, Building2, ArrowRightLeft } from 'lucide-react'; +import { Phone, MapPin, Mail, Globe, Save, Building2, ArrowRightLeft, Percent, Check } from 'lucide-react'; import Layout from '../components/Layout'; import UsuariosPanel from '../components/UsuariosPanel'; import RolesPanel from '../components/RolesPanel'; import RecetasPanel from '../components/RecetasPanel'; import CatalogoPanel from '../components/CatalogoPanel'; import { Card, Button, Input, PageHeader, toast } from '../components/ui'; -import { odooApi } from '../services/odoo'; +import { odooApi, type Doctor } from '../services/odoo'; const TABS = [ { key: 'clinica', label: 'Clínica' }, @@ -110,6 +110,70 @@ const Configuracion: FC = () => { } }; + // ---- Comisiones de médicos ---- + const [commissionDefault, setCommissionDefault] = useState(''); + const [commissionDefaultSaving, setCommissionDefaultSaving] = useState(false); + const [doctors, setDoctors] = useState([]); + const [commissionInputs, setCommissionInputs] = useState>({}); + const [commissionSavingId, setCommissionSavingId] = useState(null); + const [commissionSavedId, setCommissionSavedId] = useState(null); + + useEffect(() => { + odooApi.getCommissionDefault() + .then((res) => setCommissionDefault(String(res.commission_pct ?? 0))) + .catch(() => {/* odoo no disponible, ignorar */}); + odooApi.getDoctors() + .then((res) => { + if (res.status === 'success') { + setDoctors(res.doctors); + setCommissionInputs(Object.fromEntries(res.doctors.map((d) => [d.id, String(d.commission_pct ?? 0)]))); + } + }) + .catch(() => {/* odoo no disponible, ignorar */}); + }, []); + + const validPct = (raw: string): number | null => { + const pct = parseFloat(raw); + if (Number.isNaN(pct) || pct < 0 || pct > 100) return null; + return pct; + }; + + const saveCommissionDefault = async () => { + const pct = validPct(commissionDefault); + if (pct === null) { + toast.error('El porcentaje debe estar entre 0 y 100'); + return; + } + setCommissionDefaultSaving(true); + try { + await odooApi.setCommissionDefault(pct); + toast.success(`Comisión global actualizada: ${pct}%`); + } catch { + toast.error('No se pudo guardar la comisión global'); + } finally { + setCommissionDefaultSaving(false); + } + }; + + const saveDoctorCommission = async (id: number) => { + const pct = validPct(commissionInputs[id] ?? ''); + if (pct === null) { + toast.error('El porcentaje debe estar entre 0 y 100'); + return; + } + setCommissionSavingId(id); + try { + await odooApi.setDoctorCommission(id, pct); + setDoctors((prev) => prev.map((d) => (d.id === id ? { ...d, commission_pct: pct } : d))); + setCommissionSavedId(id); + setTimeout(() => setCommissionSavedId((cur) => (cur === id ? null : cur)), 2000); + } catch { + toast.error('No se pudo guardar la comisión del médico'); + } finally { + setCommissionSavingId(null); + } + }; + return ( @@ -313,6 +377,90 @@ const Configuracion: FC = () => { + + + +

+ + Comisiones +

+

+ Porcentaje de comisión sobre las ventas atribuidas a cada médico (visible en su página "Mis ventas"). +

+
+
+ setCommissionDefault(e.target.value)} + placeholder="0" + /> +

Se aplica a médicos sin porcentaje propio (con 0%).

+
+ +
+
+ + + + + + + + + + + {doctors.map((d) => ( + + + + + + + ))} + {doctors.length === 0 && ( + + + + )} + +
Médico% actualNuevo %Acción
{d.name} + {d.commission_pct ? `${d.commission_pct}%` : '0% (usa el global)'} + + setCommissionInputs((prev) => ({ ...prev, [d.id]: e.target.value }))} + /> + + {commissionSavedId === d.id ? ( + + Guardado + + ) : ( + + )} +
No hay médicos dados de alta.
+
+
+
)}
diff --git a/frontend/src/services/odoo.ts b/frontend/src/services/odoo.ts index eec632b..8fc2708 100644 --- a/frontend/src/services/odoo.ts +++ b/frontend/src/services/odoo.ts @@ -1216,6 +1216,21 @@ export const odooApi = { return data; }, + async setDoctorCommission(id: number, commission_pct: number): Promise<{ status: string; doctor: { id: number; name: string; commission_pct: number } }> { + const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/commission`, { commission_pct }); + return data; + }, + + async getCommissionDefault(): Promise<{ status: string; commission_pct: number }> { + const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/config/commission_default`); + return data; + }, + + async setCommissionDefault(commission_pct: number): Promise<{ status: string; commission_pct: number }> { + const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/config/commission_default`, { commission_pct }); + return data; + }, + async getDoctorHistory(id: number): Promise { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/doctors/${id}/history`); return data; diff --git a/odoo-addons/skeen_whatsapp/controllers/frontend.py b/odoo-addons/skeen_whatsapp/controllers/frontend.py index ad8d2c0..a30809f 100644 --- a/odoo-addons/skeen_whatsapp/controllers/frontend.py +++ b/odoo-addons/skeen_whatsapp/controllers/frontend.py @@ -1944,6 +1944,30 @@ class SkeenFrontendController(http.Controller): except Exception as e: return json_response({'status': 'error', 'message': str(e)}, 500) + @http.route('/skeen/frontend/v1/doctors//commission', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) + @require_role('admin') + def set_doctor_commission(self, doctor_id, **kw): + """Designa el % de comisión de un doctor (0-100). 0 = usa el % global.""" + try: + doctor = request.env['hr.employee'].sudo().browse(doctor_id) + if not doctor.exists(): + return json_response({'status': 'error', 'message': 'Médico no encontrado'}, 404) + data = _parse_json_body() + try: + pct = float(data.get('commission_pct')) + except (TypeError, ValueError): + return json_response({'status': 'error', 'message': 'commission_pct debe ser numérico'}, 400) + if not 0 <= pct <= 100: + return json_response({'status': 'error', 'message': 'El porcentaje debe estar entre 0 y 100'}, 400) + doctor.write({'commission_pct': pct}) + return json_response({ + 'status': 'success', + 'doctor': {'id': doctor.id, 'name': doctor.name, 'commission_pct': doctor.commission_pct}, + }) + except Exception as e: + request.env.cr.rollback() + return json_response({'status': 'error', 'message': str(e)}, 500) + @http.route('/skeen/frontend/v1/doctors//history', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) @require_role('lectura') def doctor_history(self, doctor_id, **kw): @@ -3693,6 +3717,33 @@ class SkeenFrontendController(http.Controller): except Exception as e: return json_response({'status': 'error', 'message': str(e)}, 500) + @http.route('/skeen/frontend/v1/config/commission_default', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False) + @require_role('admin') + def get_commission_default(self, **kw): + """% de comisión global por defecto (se aplica a médicos sin % propio).""" + try: + ICP = request.env['ir.config_parameter'].sudo() + pct = float(ICP.get_param('skeen.commission_pct', 0) or 0) + return json_response({'status': 'success', 'commission_pct': pct}) + except Exception as e: + return json_response({'status': 'error', 'message': str(e)}, 500) + + @http.route('/skeen/frontend/v1/config/commission_default', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False) + @require_role('admin') + def set_commission_default(self, **kw): + try: + data = _parse_json_body() + try: + pct = float(data.get('commission_pct')) + except (TypeError, ValueError): + return json_response({'status': 'error', 'message': 'commission_pct debe ser numérico'}, 400) + if not 0 <= pct <= 100: + return json_response({'status': 'error', 'message': 'El porcentaje debe estar entre 0 y 100'}, 400) + request.env['ir.config_parameter'].sudo().set_param('skeen.commission_pct', pct) + return json_response({'status': 'success', 'commission_pct': pct}) + except Exception as e: + return json_response({'status': 'error', 'message': str(e)}, 500) + # ============================================================ # Reporte de cumpleañeros # ============================================================