fase 2.3: portal publico de reservas en linea (5 endpoints anonimos con rate limit y token HMAC, wizard movil, gestion/cancelacion por enlace, distintivo en agenda, editor y enlace publico en Configuracion); apagado por defecto

This commit is contained in:
2026-10-11 08:28:33 +00:00
parent 358b85afcd
commit add9b28188
14 changed files with 1859 additions and 4 deletions

View File

@@ -72,6 +72,8 @@ export interface Appointment {
doctor?: string;
branch?: string;
medium?: string;
/** Origen de la reserva: 'internal' (default) o 'public_portal' (portal en línea) */
booking_source?: string;
notes?: string;
package_finished?: boolean;
package_finished_date?: string | null;
@@ -1010,6 +1012,106 @@ export interface ClinicSettings {
moneda_base: string;
nota_ticket: string;
impuestos: ImpuestosSettings;
reservas_online: ReservasOnlineSettings;
}
// ---- Reservas en línea (portal público de auto-agendado) ----
export interface ReservasOnlineSettings {
activo: boolean;
titulo: string;
descripcion: string;
requiere_confirmacion: boolean;
permitir_eleccion_doctor: boolean;
permitir_sucursal: boolean;
max_dias_futuro: number;
min_horas_anticipacion: number;
permitir_cancelar: boolean;
cancelar_hasta_horas: number;
mensaje_confirmacion: string;
telefono_contacto: string;
}
export const RESERVAS_ONLINE_DEFAULT: ReservasOnlineSettings = {
activo: false,
titulo: 'Reserva tu cita en línea',
descripcion: '',
requiere_confirmacion: true,
permitir_eleccion_doctor: true,
permitir_sucursal: false,
max_dias_futuro: 60,
min_horas_anticipacion: 2,
permitir_cancelar: true,
cancelar_hasta_horas: 24,
mensaje_confirmacion: '',
telefono_contacto: '',
};
// Contrato del portal público (endpoints /public/booking/*)
export interface BookingConfig {
activo: boolean;
brand: ClinicBrand;
terms: GlossaryTerms;
sucursales: { code: string; name: string; address?: string; phone?: string }[];
categorias: { code: string; name: string }[];
servicios: { id: number; nombre: string; categoria: string; precio: number; duracion_min: number }[];
medicos: { id: number; nombre: string; staff_type: string }[];
config: {
max_dias_futuro: number;
min_horas_anticipacion: number;
permite_sucursal: boolean;
permite_eleccion_doctor: boolean;
requiere_confirmacion: boolean;
permitir_cancelar: boolean;
cancelar_hasta_horas: number;
telefono_contacto: string;
titulo: string;
descripcion: string;
mensaje_confirmacion: string;
zona_horaria?: string;
};
}
export interface BookingCreatePayload {
servicio_id: number;
fecha: string;
hora: string;
medico_id?: number | null;
sucursal?: string | null;
nombre: string;
telefono: string;
email?: string;
notas?: string;
hp?: string;
}
export interface BookingCreateResult {
ok: boolean;
folio: string;
fecha: string;
hora: string;
medico: string | null;
servicio: string;
sucursal: string;
estado: string;
requiere_confirmacion: boolean;
mensaje_confirmacion: string;
token_de_gestion: string;
}
export interface BookingCita {
status: string;
folio: string;
fecha: string;
hora: string;
servicio: string;
medico: string | null;
sucursal: string;
estado: string;
estado_label: string;
puede_cancelar: boolean;
permitir_cancelar: boolean;
cancelar_hasta_horas: number;
telefono_contacto: string;
}
export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
@@ -1026,6 +1128,7 @@ export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
moneda_base: 'MXN',
nota_ticket: '',
impuestos: IMPUESTOS_DEFAULT,
reservas_online: RESERVAS_ONLINE_DEFAULT,
};
export interface FrontendUser {
@@ -1686,6 +1789,32 @@ export const odooApi = {
return data;
},
// ---- Portal público de reservas en línea (sin autenticación) ----
async getBookingConfig(): Promise<BookingConfig> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/booking/config`);
return data;
},
async getBookingSlots(params: { servicio_id: number; fecha: string; medico_id?: number | null; sucursal?: string | null }): Promise<{ status: string; slots: string[]; medico_id: number | null; duracion_min: number; message?: string }> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/booking/slots`, { params });
return data;
},
async createBooking(payload: BookingCreatePayload): Promise<BookingCreateResult> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/public/booking/create`, payload);
return data;
},
async getBookingCita(token: string): Promise<BookingCita> {
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/public/booking/cita`, { params: { token } });
return data;
},
async cancelBooking(token: string, motivo?: string): Promise<{ status: string; ok?: boolean; folio?: string; estado?: string; message?: string }> {
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/public/booking/cancel`, { token, motivo });
return data;
},
async updateRolePermissions(matrix: Record<string, FrontendRole[]>): Promise<{ status: string; matrix: Record<string, FrontendRole[]>; message?: string }> {
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/roles/permissions`, { matrix });
return data;