Agenda: horario 8-19h, bloques de 15 min, cita rapida, vista restringida para rol medico y chips de pacientes por doctor
This commit is contained in:
@@ -4,6 +4,7 @@ from odoo import http
|
||||
from odoo.http import request, Response
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from datetime import date, datetime, timedelta
|
||||
from .auth import sign_token, current_user, require_role, verify_token
|
||||
from ..roles import ROLE_SELECTION, ROLE_KEYS, MENU_ROLES_KEY, get_menu_roles
|
||||
@@ -35,6 +36,43 @@ def _parse_json_body():
|
||||
class SkeenFrontendController(http.Controller):
|
||||
"""Controladores API públicos para el Frontend React de SKEEN"""
|
||||
|
||||
# Horario de agenda de la clínica (hora de inicio permitida de una cita)
|
||||
AGENDA_START = 8.0 # 8:00 AM
|
||||
AGENDA_END = 19.0 # 7:00 PM
|
||||
|
||||
def _valid_agenda_time(self, value):
|
||||
"""Valida hora de inicio dentro del horario de agenda (8:00-19:00)."""
|
||||
try:
|
||||
t = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return self.AGENDA_START <= t < self.AGENDA_END
|
||||
|
||||
def _linked_employee(self, user):
|
||||
"""hr.employee vinculado a un usuario frontend (rol medico).
|
||||
|
||||
Vía principal: res.users con el mismo login -> employee_ids.
|
||||
Fallback: empleado cuyo work_email coincide con el login o cuyo
|
||||
nombre coincide con el nombre del usuario frontend.
|
||||
"""
|
||||
Emp = request.env['hr.employee'].sudo()
|
||||
ru = request.env['res.users'].sudo().search([('login', '=', user.login)], limit=1)
|
||||
emp = ru.employee_ids[:1] if ru else Emp.browse()
|
||||
if not emp and user.login:
|
||||
emp = Emp.search([('work_email', '=ilike', user.login)], limit=1)
|
||||
if not emp and user.name:
|
||||
emp = Emp.search([('name', '=ilike', user.name)], limit=1)
|
||||
return emp
|
||||
|
||||
def _frontend_user_dict(self, user):
|
||||
"""to_public_dict + datos del empleado vinculado (rol medico)."""
|
||||
d = user.to_public_dict()
|
||||
if user.role == 'medico':
|
||||
emp = self._linked_employee(user)
|
||||
d['employee_id'] = emp.id if emp else None
|
||||
d['employee_name'] = emp.name if emp else None
|
||||
return d
|
||||
|
||||
# ============================================================
|
||||
# Health
|
||||
# ============================================================
|
||||
@@ -61,7 +99,7 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': 'Credenciales inválidas'}, 401)
|
||||
user.write({'last_login': fields.Datetime.now()})
|
||||
token = sign_token(request.env, user)
|
||||
return json_response({'status': 'success', 'token': token, 'user': user.to_public_dict()})
|
||||
return json_response({'status': 'success', 'token': token, 'user': self._frontend_user_dict(user)})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@@ -69,7 +107,7 @@ class SkeenFrontendController(http.Controller):
|
||||
@require_role('lectura')
|
||||
def auth_me(self, **kw):
|
||||
try:
|
||||
return json_response({'status': 'success', 'user': request.frontend_user.to_public_dict()})
|
||||
return json_response({'status': 'success', 'user': self._frontend_user_dict(request.frontend_user)})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@@ -274,9 +312,30 @@ class SkeenFrontendController(http.Controller):
|
||||
@http.route('/skeen/frontend/v1/appointments', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def list_appointments(self, **kw):
|
||||
"""Listar citas (opcionalmente filtradas por fecha, estado, paciente)"""
|
||||
"""Listar citas (opcionalmente filtradas por fecha, estado, paciente)
|
||||
|
||||
Rol 'medico': solo ve las citas asignadas a su hr.employee vinculado
|
||||
(filtro del lado del servidor, ignora cualquier doctor_id recibido).
|
||||
"""
|
||||
try:
|
||||
domain = []
|
||||
user = request.frontend_user
|
||||
if user and user.role == 'medico':
|
||||
emp = self._linked_employee(user)
|
||||
if not emp:
|
||||
# Sin empleado vinculado: agenda vacía (el frontend muestra aviso)
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'appointments': [],
|
||||
'bloqueos': [],
|
||||
'total': 0,
|
||||
'page': 1,
|
||||
'page_size': int(kw.get('page_size', 50) or 50),
|
||||
'total_pages': 0,
|
||||
'medico_sin_employee': True,
|
||||
})
|
||||
domain = [('doctor_id', '=', emp.id)]
|
||||
else:
|
||||
domain = []
|
||||
if kw.get('date'):
|
||||
domain.append(('date', '=', kw.get('date')))
|
||||
if kw.get('date_from'):
|
||||
@@ -287,7 +346,7 @@ class SkeenFrontendController(http.Controller):
|
||||
domain.append(('state', '=', kw.get('state')))
|
||||
if kw.get('patient_id'):
|
||||
domain.append(('partner_id', '=', int(kw.get('patient_id'))))
|
||||
if kw.get('doctor_id'):
|
||||
if kw.get('doctor_id') and not (user and user.role == 'medico'):
|
||||
domain.append(('doctor_id', '=', int(kw.get('doctor_id'))))
|
||||
if kw.get('search'):
|
||||
s = kw.get('search')
|
||||
@@ -344,6 +403,9 @@ class SkeenFrontendController(http.Controller):
|
||||
}
|
||||
first_date = data.get('date')
|
||||
first_time = data.get('time')
|
||||
if first_time is not None and not self._valid_agenda_time(first_time):
|
||||
return json_response(
|
||||
{'status': 'error', 'message': 'La hora debe estar dentro del horario de agenda (8:00 a 19:00)'}, 400)
|
||||
created = []
|
||||
# Para paquetes, generamos citas separadas con fechas consecutivas
|
||||
current_date = datetime.strptime(first_date, '%Y-%m-%d').date() if first_date else date.today()
|
||||
@@ -365,6 +427,94 @@ class SkeenFrontendController(http.Controller):
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/appointments/quick', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
||||
@require_role('recepcion')
|
||||
def create_quick_appointment(self, **kw):
|
||||
"""Cita rápida: solo nombre + teléfono (+ fecha/hora/médico del contexto).
|
||||
|
||||
Busca el paciente por teléfono y lo reutiliza si existe; si no, lo crea
|
||||
con datos mínimos. El servicio usa uno por defecto (primera 'consulta'
|
||||
activa, o el primer servicio activo) salvo que se indique servicio_id.
|
||||
"""
|
||||
try:
|
||||
data = _parse_json_body()
|
||||
name = (data.get('name') or '').strip()
|
||||
phone = (data.get('phone') or '').strip()
|
||||
if not name or not phone:
|
||||
return json_response({'status': 'error', 'message': 'Nombre y teléfono son obligatorios'}, 400)
|
||||
apt_date = data.get('date') or date.today().strftime('%Y-%m-%d')
|
||||
apt_time = data.get('time')
|
||||
if apt_time is None or not self._valid_agenda_time(apt_time):
|
||||
return json_response(
|
||||
{'status': 'error', 'message': 'La hora debe estar dentro del horario de agenda (8:00 a 19:00)'}, 400)
|
||||
|
||||
Partner = request.env['res.partner'].sudo()
|
||||
digits = re.sub(r'\D', '', phone)
|
||||
patient = None
|
||||
if len(digits) >= 7:
|
||||
# Comparación sobre dígitos normalizados (ignora espacios, +52, etc.):
|
||||
# coinciden si los últimos N dígitos (N = longitud del más corto) son iguales
|
||||
match_id = None
|
||||
candidates = Partner.search_read(
|
||||
[('is_patient', '=', True), '|', ('phone', '!=', False), ('mobile', '!=', False)],
|
||||
['phone', 'mobile'], limit=10000)
|
||||
for c in candidates:
|
||||
for f in ('phone', 'mobile'):
|
||||
c_digits = re.sub(r'\D', '', c.get(f) or '')
|
||||
n = min(len(c_digits), len(digits))
|
||||
if n >= 7 and c_digits[-n:] == digits[-n:]:
|
||||
match_id = c['id']
|
||||
break
|
||||
if match_id:
|
||||
break
|
||||
if match_id:
|
||||
patient = Partner.browse(match_id)
|
||||
if not patient:
|
||||
patient = Partner.search([
|
||||
('is_patient', '=', True), ('phone', '=', phone),
|
||||
], limit=1)
|
||||
patient_created = False
|
||||
if not patient:
|
||||
patient = Partner.create({
|
||||
'name': name,
|
||||
'phone': phone,
|
||||
'is_patient': True,
|
||||
'source': data.get('source', 'walkin'),
|
||||
})
|
||||
patient_created = True
|
||||
|
||||
Servicio = request.env['skeen.servicio'].sudo()
|
||||
servicio = Servicio.browse(int(data.get('servicio_id') or 0))
|
||||
if not servicio.exists():
|
||||
servicio = Servicio.search([('category', '=', 'consulta'), ('active', '=', True)],
|
||||
order='sequence, id', limit=1)
|
||||
if not servicio:
|
||||
servicio = Servicio.search([('active', '=', True)], order='sequence, id', limit=1)
|
||||
if not servicio:
|
||||
return json_response({'status': 'error', 'message': 'No hay servicios activos para agendar'}, 400)
|
||||
|
||||
cita = request.env['skeen.cita'].sudo().create({
|
||||
'partner_id': patient.id,
|
||||
'servicio_id': servicio.id,
|
||||
'doctor_id': int(data['doctor_id']) if data.get('doctor_id') else False,
|
||||
'date': apt_date,
|
||||
'time': float(apt_time),
|
||||
'branch': data.get('branch', 'rosarito'),
|
||||
'medium': data.get('medium', 'onsite'),
|
||||
'notes': data.get('notes', ''),
|
||||
'state': data.get('state', 'pending'),
|
||||
})
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'appointment': self._appointment_to_dict(cita),
|
||||
'patient': self._patient_to_dict(patient),
|
||||
'patient_created': patient_created,
|
||||
}, 201)
|
||||
except Exception as e:
|
||||
# Rollback explícito: evita persistir paciente/cita con validación rechazada
|
||||
request.env.cr.rollback()
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/appointments/<int:appointment_id>', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def get_appointment(self, appointment_id, **kw):
|
||||
@@ -386,6 +536,9 @@ class SkeenFrontendController(http.Controller):
|
||||
data = _parse_json_body()
|
||||
allowed = ['partner_id', 'servicio_id', 'date', 'time', 'doctor_id', 'branch', 'medium', 'notes', 'state']
|
||||
vals = {k: data[k] for k in allowed if k in data}
|
||||
if 'time' in vals and not self._valid_agenda_time(vals['time']):
|
||||
return json_response(
|
||||
{'status': 'error', 'message': 'La hora debe estar dentro del horario de agenda (8:00 a 19:00)'}, 400)
|
||||
cita.write(vals)
|
||||
return json_response({'status': 'success', 'appointment': self._appointment_to_dict(cita)})
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user