fase 3.1: recursos fisicos en agenda (modelo skeen.recurso con semilla cabina/consultoria, asignacion multiple a citas con validacion de solape, bloqueos por recurso, lista de espera desde recursos, vista/filtro por recurso, reporte de ocupacion); apagado por defecto
This commit is contained in:
@@ -70,6 +70,9 @@ export interface Appointment {
|
||||
is_first_visit?: boolean;
|
||||
doctor_id?: number | null;
|
||||
doctor?: string;
|
||||
/** Recursos asignados (cabina, sala, equipo). Vacío si el módulo está apagado. */
|
||||
recurso_ids?: number[];
|
||||
recursos?: RecursoBrief[];
|
||||
branch?: string;
|
||||
medium?: string;
|
||||
/** Origen de la reserva: 'internal' (default) o 'public_portal' (portal en línea) */
|
||||
@@ -94,10 +97,60 @@ export interface Appointment {
|
||||
/** Motivos predefinidos de un bloqueo de agenda */
|
||||
export type BloqueoMotivo = 'vacaciones' | 'junta' | 'comida' | 'personal' | 'otro';
|
||||
|
||||
// ---- Recursos de la agenda (cabinas, salas, consultorios, equipos) ----
|
||||
export interface RecursoBrief {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface Recurso {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
tipo: RecursoTipo;
|
||||
tipo_label: string;
|
||||
activo: boolean;
|
||||
sequence: number;
|
||||
sucursales: { id: number; code: string; name: string }[];
|
||||
capacidad: number;
|
||||
costo_hora: number;
|
||||
color: string;
|
||||
notas: string;
|
||||
}
|
||||
|
||||
export type RecursoTipo = 'cabina' | 'sala' | 'consultorio' | 'equipo' | 'otro';
|
||||
|
||||
export interface RecursoInput {
|
||||
name: string;
|
||||
code?: string;
|
||||
tipo?: RecursoTipo;
|
||||
activo?: boolean;
|
||||
sequence?: number;
|
||||
sucursal_ids?: number[];
|
||||
capacidad?: number;
|
||||
costo_hora?: number;
|
||||
color?: string;
|
||||
notas?: string;
|
||||
motivo?: string;
|
||||
}
|
||||
|
||||
export const RECURSO_TIPOS: { value: RecursoTipo; label: string }[] = [
|
||||
{ value: 'cabina', label: 'Cabina' },
|
||||
{ value: 'sala', label: 'Sala' },
|
||||
{ value: 'consultorio', label: 'Consultorio' },
|
||||
{ value: 'equipo', label: 'Equipo' },
|
||||
{ value: 'otro', label: 'Otro' },
|
||||
];
|
||||
|
||||
export interface Bloqueo {
|
||||
id: number;
|
||||
doctor_id: number;
|
||||
doctor: string;
|
||||
doctor_id: number | null;
|
||||
doctor: string | null;
|
||||
/** Recursos bloqueados (cabina, sala, equipo). Vacío si solo bloquea médico. */
|
||||
recurso_ids?: number[];
|
||||
recursos?: RecursoBrief[];
|
||||
date: string;
|
||||
all_day: boolean;
|
||||
time_from: number;
|
||||
@@ -124,8 +177,8 @@ export interface ListaEspera {
|
||||
priority: 'alta' | 'normal' | 'baja';
|
||||
priority_label: string;
|
||||
franja: '' | 'manana' | 'tarde';
|
||||
// Dónde espera: consultoría (médico) o cabina (tratamiento)
|
||||
sala?: '' | 'consultoria' | 'cabina';
|
||||
// Dónde espera: el código de un recurso de la agenda (cabina, sala...)
|
||||
sala?: string;
|
||||
sala_label?: string;
|
||||
date_from: string | null;
|
||||
date_to: string | null;
|
||||
@@ -1015,6 +1068,7 @@ export interface ClinicSettings {
|
||||
reservas_online: ReservasOnlineSettings;
|
||||
notificaciones: NotificacionesSettings;
|
||||
membresias: MembresiasSettings;
|
||||
recursos: RecursosSettings;
|
||||
}
|
||||
|
||||
// ---- Reservas en línea (portal público de auto-agendado) ----
|
||||
@@ -1049,6 +1103,46 @@ export const RESERVAS_ONLINE_DEFAULT: ReservasOnlineSettings = {
|
||||
};
|
||||
|
||||
// ---- Notificaciones y recordatorios de citas ----
|
||||
// ---- Recursos físicos de la agenda ----
|
||||
export interface RecursosSettings {
|
||||
/** Enciende el módulo de recursos. Apagado = la agenda se ve igual que hoy. */
|
||||
activo: boolean;
|
||||
/** Impide dos citas/bloqueos en el mismo recurso a la misma hora. */
|
||||
validar_solape: boolean;
|
||||
/** Exige elegir al menos un recurso al crear la cita. */
|
||||
obligatorio_al_agendar: boolean;
|
||||
/** Permite asignar más de un recurso a una cita. */
|
||||
permitir_multiples: boolean;
|
||||
/** Muestra el recurso (chip/color) en la agenda. */
|
||||
mostrar_en_agenda: boolean;
|
||||
/** Ofrece el filtro por recurso junto al de doctores. */
|
||||
filtrar_como_doctores: boolean;
|
||||
}
|
||||
|
||||
export const RECURSOS_DEFAULT: RecursosSettings = {
|
||||
activo: false,
|
||||
validar_solape: true,
|
||||
obligatorio_al_agendar: false,
|
||||
permitir_multiples: true,
|
||||
mostrar_en_agenda: true,
|
||||
filtrar_como_doctores: true,
|
||||
};
|
||||
|
||||
export interface RecursosReportRow {
|
||||
recurso_id: number;
|
||||
recurso: string;
|
||||
code: string;
|
||||
tipo: RecursoTipo;
|
||||
tipo_label: string;
|
||||
color: string;
|
||||
citas: number;
|
||||
horas_ocupadas: number;
|
||||
horas_disponibles: number;
|
||||
ocupacion_pct: number;
|
||||
costo_hora: number;
|
||||
ingresos_estimados: number;
|
||||
}
|
||||
|
||||
export type NotificacionCanal = 'whatsapp' | 'email';
|
||||
export type NotificacionCanalConfig = NotificacionCanal | 'ambos';
|
||||
export type NotificacionTipo = 'recordatorio' | 'confirmacion' | 'cumpleanos' | 'post_visita' | 'recuperacion' | 'manual';
|
||||
@@ -1541,6 +1635,7 @@ export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
|
||||
reservas_online: RESERVAS_ONLINE_DEFAULT,
|
||||
notificaciones: NOTIFICACIONES_DEFAULT,
|
||||
membresias: MEMBRESIAS_DEFAULT,
|
||||
recursos: RECURSOS_DEFAULT,
|
||||
};
|
||||
|
||||
export interface FrontendUser {
|
||||
@@ -2190,6 +2285,40 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
// ---- Recursos de la agenda (cabinas, salas, consultorios, equipos) ----
|
||||
async getRecursos(opts?: { includeInactive?: boolean }): Promise<{ status: string; recursos: Recurso[]; total: number; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/recursos`, {
|
||||
params: opts?.includeInactive ? { include_inactive: 1 } : undefined,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
async createRecurso(payload: RecursoInput): Promise<{ status: string; recurso: Recurso; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/recursos`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateRecurso(id: number, payload: Partial<RecursoInput>): Promise<{ status: string; recurso: Recurso; message?: string }> {
|
||||
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/recursos/${id}`, payload);
|
||||
return data;
|
||||
},
|
||||
|
||||
async deleteRecurso(id: number): Promise<{ status: string; recurso: Recurso; en_uso: number; message: string }> {
|
||||
const { data } = await axios.delete(`${ODOO_BASE}/skeen/frontend/v1/recursos/${id}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
async getRecursosReport(start: string, end: string): Promise<{
|
||||
status: string; start: string; end: string; dias: number; horas_disponibles: number;
|
||||
recursos: RecursosReportRow[]; totales: { citas: number; horas_ocupadas: number; ingresos_estimados: number };
|
||||
message?: string;
|
||||
}> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/recursos`, {
|
||||
params: { start, end },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
// ---- Ajustes de la clínica ----
|
||||
async getClinicSettings(): Promise<{ status: string; settings: ClinicSettings; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/clinic-settings`);
|
||||
@@ -2492,7 +2621,7 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
async createQuickAppointment(payload: { name: string; phone: string; date: string; time: number; doctor_id?: number; notes?: string; duration?: number; branch?: 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; branch?: string; recurso_ids?: 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;
|
||||
},
|
||||
@@ -2596,6 +2725,7 @@ export const odooApi = {
|
||||
|
||||
async createBloqueo(payload: {
|
||||
doctor_ids: number[];
|
||||
recurso_ids?: number[];
|
||||
date_from: string;
|
||||
date_to?: string;
|
||||
days?: number[];
|
||||
@@ -2611,7 +2741,7 @@ export const odooApi = {
|
||||
},
|
||||
|
||||
async updateBloqueo(id: number, payload: Partial<{
|
||||
doctor_id: number; date: string; all_day: boolean; time_from: number; time_to: number;
|
||||
doctor_id: number; recurso_ids: 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);
|
||||
|
||||
Reference in New Issue
Block a user