Agenda: cierre rápido de agendas por médico, lista de espera, duraciones 30/45/60, checklist de estados con bitácora y auto-refresh 30s
- BloqueosModal: alta multi-médico, rango repetible por día de la semana, motivos predefinidos, edición/borrado y cierre rápido desde la cabecera de cada columna (todo el día / resto del día / rango). - skeen.lista.espera + panel y modal en la agenda: prioridad, franja, rango de fechas, agendar precargado, avisar por WhatsApp y aviso cuando se libera un hueco compatible. - duration en skeen.cita (0 = la del servicio): la cita ocupa sus bloques de 15 min reales en las 4 vistas, con celdas de continuación. - Checklist de estados (Agendada → Confirmada → En cabina → En consulta → Completada) con avance/retroceso, cancelación y no-show con confirmación, y bitácora de quién/cuándo en skeen.cita.state.log. - Etiquetas del sistema legacy alineadas (Canceló / Lista de espera / En cabina) en badges de agenda y dashboard. - Auto refresco silencioso cada 30 s (pausable, se pospone con modales, escritura o drag) en citas + bloqueos + lista de espera en un solo request.
This commit is contained in:
@@ -62,7 +62,11 @@ export interface Appointment {
|
||||
payment_state: string;
|
||||
price: number;
|
||||
amount_paid?: number;
|
||||
/** Duración efectiva en minutos (propia de la cita o la del servicio) */
|
||||
duration?: number;
|
||||
/** Duración propia de la cita (0 = la del servicio) */
|
||||
duration_override?: number;
|
||||
service_duration?: number;
|
||||
is_first_visit?: boolean;
|
||||
doctor_id?: number | null;
|
||||
doctor?: string;
|
||||
@@ -79,8 +83,15 @@ export interface Appointment {
|
||||
arrived_at?: string | null;
|
||||
// El paciente cumple años el día de la cita (badge pastel en agenda)
|
||||
patient_birthday?: boolean;
|
||||
/** Hora en float (p. ej. 9.5) tal como la guarda Odoo */
|
||||
time_float?: number;
|
||||
/** Etiqueta del estado con el vocabulario de la clínica */
|
||||
state_label?: string;
|
||||
}
|
||||
|
||||
/** Motivos predefinidos de un bloqueo de agenda */
|
||||
export type BloqueoMotivo = 'vacaciones' | 'junta' | 'comida' | 'personal' | 'otro';
|
||||
|
||||
export interface Bloqueo {
|
||||
id: number;
|
||||
doctor_id: number;
|
||||
@@ -90,6 +101,50 @@ export interface Bloqueo {
|
||||
time_from: number;
|
||||
time_to: number;
|
||||
motivo: string;
|
||||
motivo_tipo?: BloqueoMotivo;
|
||||
motivo_tipo_label?: string;
|
||||
motivo_label?: string;
|
||||
grupo?: string;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
/** Entrada en la lista de espera de pacientes */
|
||||
export interface ListaEspera {
|
||||
id: number;
|
||||
reference: string;
|
||||
patient_id: number;
|
||||
patient: string;
|
||||
phone: string;
|
||||
doctor_id: number | null;
|
||||
doctor: string | null;
|
||||
service_id: number | null;
|
||||
service: string | null;
|
||||
priority: 'alta' | 'normal' | 'baja';
|
||||
priority_label: string;
|
||||
franja: '' | 'manana' | 'tarde';
|
||||
date_from: string | null;
|
||||
date_to: string | null;
|
||||
notes: string;
|
||||
state: 'esperando' | 'agendado' | 'descartado';
|
||||
cita_id: number | null;
|
||||
created_by: string;
|
||||
create_date: string | null;
|
||||
resolved_at: string | null;
|
||||
espera_dias: number;
|
||||
match_servicio?: boolean;
|
||||
match_doctor?: boolean;
|
||||
}
|
||||
|
||||
/** Cambio de estado de una cita (bitácora del checklist) */
|
||||
export interface AppointmentStateLog {
|
||||
id: number;
|
||||
from_state: string;
|
||||
from_label: string;
|
||||
to_state: string;
|
||||
to_label: string;
|
||||
user: string;
|
||||
note: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export type VisitaState = 'en_curso' | 'completada' | 'cancelada';
|
||||
@@ -1182,7 +1237,7 @@ export const odooApi = {
|
||||
},
|
||||
|
||||
// Citas
|
||||
async getAppointments(params?: Record<string, string | number>): Promise<{ status: string; appointments: Appointment[]; bloqueos?: Bloqueo[]; total?: number; page?: number; page_size?: number; total_pages?: number }> {
|
||||
async getAppointments(params?: Record<string, string | number>): Promise<{ status: string; appointments: Appointment[]; bloqueos?: Bloqueo[]; lista_espera?: ListaEspera[]; total?: number; page?: number; page_size?: number; total_pages?: number; medico_sin_employee?: boolean }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments`, { params });
|
||||
return data;
|
||||
},
|
||||
@@ -1192,7 +1247,7 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
async createQuickAppointment(payload: { name: string; phone: string; date: string; time: number; doctor_id?: number; notes?: string }): Promise<{ status: string; appointment: Appointment; patient: Patient; patient_created: boolean; message?: string }> {
|
||||
async createQuickAppointment(payload: { name: string; phone: string; date: string; time: number; doctor_id?: number; notes?: string; duration?: number }): Promise<{ status: string; appointment: Appointment; patient: Patient; patient_created: boolean; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/quick`, payload);
|
||||
return data;
|
||||
},
|
||||
@@ -1222,8 +1277,14 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateAppointmentStatus(id: number, action: string, reason?: string): Promise<{ status: string; appointment: Appointment }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/status`, { action, reason });
|
||||
async updateAppointmentStatus(id: number, action: string, reason?: string, state?: string): Promise<{ status: string; appointment: Appointment }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/status`, { action, reason, state });
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Bitácora de cambios de estado de la cita (checklist con retroceso) */
|
||||
async getAppointmentStateHistory(id: number): Promise<{ status: string; history: AppointmentStateLog[] }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/${id}/state_history`);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -1274,8 +1335,11 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
async getAvailableSlots(date: string, servicio_id: number): Promise<{ status: string; slots: { time: number; time_str: string }[] }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/slots`, { params: { date, servicio_id } });
|
||||
async getAvailableSlots(date: string, servicio_id: number, doctor_id?: number, duration?: number): Promise<{ status: string; slots: { time: number; time_str: string }[] }> {
|
||||
const params: Record<string, string | number> = { date, servicio_id };
|
||||
if (doctor_id) params.doctor_id = doctor_id;
|
||||
if (duration) params.duration = duration;
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/appointments/slots`, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -1285,16 +1349,75 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
async createBloqueo(payload: { doctor_id: number; date: string; all_day?: boolean; time_from?: number; time_to?: number; motivo?: string }): Promise<{ status: string; bloqueo: Bloqueo }> {
|
||||
async createBloqueo(payload: {
|
||||
doctor_ids: number[];
|
||||
date_from: string;
|
||||
date_to?: string;
|
||||
days?: number[];
|
||||
all_day?: boolean;
|
||||
time_from?: number;
|
||||
time_to?: number;
|
||||
motivo_tipo?: BloqueoMotivo;
|
||||
motivo?: string;
|
||||
force?: boolean;
|
||||
}): Promise<{ status: string; grupo: string; creados: number; bloqueos: Bloqueo[] }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/bloqueos`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateBloqueo(id: number, payload: Partial<{
|
||||
doctor_id: number; date: string; all_day: boolean; time_from: number; time_to: number;
|
||||
motivo_tipo: BloqueoMotivo; motivo: string; force: boolean;
|
||||
}>): Promise<{ status: string; bloqueo: Bloqueo }> {
|
||||
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/bloqueos/${id}`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async deleteBloqueo(id: number): Promise<{ status: string; message: string }> {
|
||||
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/bloqueos/${id}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Elimina la serie completa de bloqueos creados juntos (mismo grupo) */
|
||||
async deleteBloqueoGrupo(grupo: string): Promise<{ status: string; eliminados: number }> {
|
||||
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/bloqueos/grupo/${grupo}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
// Lista de espera de pacientes
|
||||
async getListaEspera(params?: Record<string, string | number>): Promise<{ status: string; lista: ListaEspera[] }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/lista_espera`, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
async createListaEspera(payload: {
|
||||
partner_id: number; doctor_id?: number | null; service_id?: number | null;
|
||||
priority?: string; franja?: string; date_from?: string; date_to?: string; notes?: string;
|
||||
}): Promise<{ status: string; registro: ListaEspera }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/lista_espera`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateListaEspera(id: number, payload: Partial<{
|
||||
state: 'esperando' | 'agendado' | 'descartado'; cita_id: number; priority: string;
|
||||
franja: string; date_from: string; date_to: string; notes: string; doctor_id: number;
|
||||
service_id: number;
|
||||
}>): Promise<{ status: string; registro: ListaEspera }> {
|
||||
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/lista_espera/${id}`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async deleteListaEspera(id: number): Promise<{ status: string; message: string }> {
|
||||
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/lista_espera/${id}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Pacientes en espera compatibles con un hueco que se liberó */
|
||||
async getListaEsperaCompatibles(params: { doctor_id?: number; servicio_id?: number; date?: string }): Promise<{ status: string; lista: ListaEspera[]; total: number }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/lista_espera/compatibles`, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
// Visitas clínicas
|
||||
async getVisitas(params?: Record<string, string | number>): Promise<{ status: string; visitas: Visita[]; total?: number; page?: number; page_size?: number; total_pages?: number }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/visitas`, { params });
|
||||
|
||||
Reference in New Issue
Block a user