bloque 3: lealtad configurable (motor de puntos con multiplicadores por servicio/categoria/sucursal/dia/nivel, niveles VIP con descuento y factor, expiracion, ajuste manual con motivo y reportes) + propinas en punto de venta con reparto entre el personal, desglose en ticket y corte, y opcion en la liquidacion de comisiones; modo clasico y propinas apagadas preservan el comportamiento actual
This commit is contained in:
@@ -622,6 +622,12 @@ export interface Sale {
|
||||
/** Cobro: quién cobró y con qué método (módulo Tickets) */
|
||||
payment_method?: string;
|
||||
user_login?: string;
|
||||
/** Propina cobrada (0 si el módulo está apagado). */
|
||||
propina?: number;
|
||||
propina_moneda?: string;
|
||||
/** Descuento automático por nivel VIP. */
|
||||
nivel_discount?: number;
|
||||
nivel_codigo?: string;
|
||||
/** Pagos ligados al ticket (los nuevos; los antiguos no tienen vínculo) */
|
||||
pagos?: Payment[];
|
||||
}
|
||||
@@ -1087,6 +1093,8 @@ export interface ClinicSettings {
|
||||
recursos: RecursosSettings;
|
||||
compras: ComprasSettings;
|
||||
comisiones: ComisionesSettings;
|
||||
lealtad: LealtadSettings;
|
||||
propinas: PropinasSettings;
|
||||
licencia: LicenciaSettings;
|
||||
portal_cliente: PortalClienteSettings;
|
||||
}
|
||||
@@ -1567,6 +1575,156 @@ export const COMISIONES_DEFAULT: ComisionesSettings = {
|
||||
pago_minimo: 0,
|
||||
};
|
||||
|
||||
// ---- Lealtad (puntos del monedero y niveles VIP) ----
|
||||
export type LealtadModo = 'clasico' | 'configurable';
|
||||
export type LealtadAcumular = 'pagado' | 'vendido';
|
||||
export type LealtadRedondear = 'abajo' | 'normal';
|
||||
export type LealtadAplicar = 'servicios' | 'productos' | 'membresias' | 'paquetes';
|
||||
export type LealtadAlcance = 'servicio' | 'articulo' | 'categoria' | 'nivel' | 'sucursal' | 'rango_fechas' | 'dia_semana';
|
||||
|
||||
export interface NivelLealtad {
|
||||
codigo: string;
|
||||
nombre: string;
|
||||
gasto_minimo: number;
|
||||
ventana_meses: number;
|
||||
descuento_pct: number;
|
||||
factor_puntos: number;
|
||||
vigencia_meses: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface MultiplicadorLealtad {
|
||||
nombre: string;
|
||||
alcance: LealtadAlcance;
|
||||
valor: string;
|
||||
factor: number;
|
||||
fecha_desde: string;
|
||||
fecha_hasta: string;
|
||||
activo: boolean;
|
||||
}
|
||||
|
||||
export interface LealtadSettings {
|
||||
activo: boolean;
|
||||
modo: LealtadModo;
|
||||
puntos_por_unidad: number;
|
||||
valor_punto: number;
|
||||
moneda_base: string;
|
||||
canje_max_pct: number;
|
||||
expira_meses: number;
|
||||
acumular_sobre: LealtadAcumular;
|
||||
aplicar_a: LealtadAplicar[];
|
||||
redondear: LealtadRedondear;
|
||||
niveles: NivelLealtad[];
|
||||
multiplicadores: MultiplicadorLealtad[];
|
||||
}
|
||||
|
||||
export const LEALTAD_DEFAULT: LealtadSettings = {
|
||||
activo: true,
|
||||
modo: 'clasico',
|
||||
puntos_por_unidad: 0.1,
|
||||
valor_punto: 1,
|
||||
moneda_base: 'MXN',
|
||||
canje_max_pct: 50,
|
||||
expira_meses: 0,
|
||||
acumular_sobre: 'pagado',
|
||||
aplicar_a: ['servicios', 'productos', 'membresias', 'paquetes'],
|
||||
redondear: 'abajo',
|
||||
niveles: [],
|
||||
multiplicadores: [],
|
||||
};
|
||||
|
||||
export interface NivelPaciente {
|
||||
codigo: string;
|
||||
nombre: string;
|
||||
descuento_pct: number;
|
||||
factor_puntos: number;
|
||||
color?: string;
|
||||
desde?: string | null;
|
||||
manual?: boolean;
|
||||
revisar?: string | null;
|
||||
}
|
||||
|
||||
export interface LealtadPaciente {
|
||||
status: string;
|
||||
patient_id: number;
|
||||
patient: string;
|
||||
activo: boolean;
|
||||
modo: LealtadModo;
|
||||
valor_punto: number;
|
||||
puntos: number;
|
||||
equivalente_mxn: number;
|
||||
gasto_ventana: number;
|
||||
nivel: NivelPaciente;
|
||||
niveles: NivelLealtad[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ReporteLealtad {
|
||||
status: string;
|
||||
modo: LealtadModo;
|
||||
valor_punto: number;
|
||||
otorgados: number;
|
||||
canjeados: number;
|
||||
expirados: number;
|
||||
ajustes: number;
|
||||
depositos: number;
|
||||
by_type: Record<string, number>;
|
||||
puntos_activos: number;
|
||||
pasivo_mxn: number;
|
||||
cuentas: number;
|
||||
niveles: NivelLealtad[];
|
||||
conteo_niveles: Record<string, number>;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// ---- Propinas en el punto de venta ----
|
||||
export type PropinasReparto = 'ninguno' | 'equitativo' | 'por_participacion';
|
||||
|
||||
export interface PropinasSettings {
|
||||
activo: boolean;
|
||||
sugerencias_pct: number[];
|
||||
permitir_personalizado: boolean;
|
||||
permitir_sin_propina: boolean;
|
||||
reparto: PropinasReparto;
|
||||
incluir_en_liquidacion_comisiones: boolean;
|
||||
cuenta_propinas: string;
|
||||
}
|
||||
|
||||
export const PROPINAS_DEFAULT: PropinasSettings = {
|
||||
activo: false,
|
||||
sugerencias_pct: [10, 15, 20],
|
||||
permitir_personalizado: true,
|
||||
permitir_sin_propina: true,
|
||||
reparto: 'ninguno',
|
||||
incluir_en_liquidacion_comisiones: false,
|
||||
cuenta_propinas: '',
|
||||
};
|
||||
|
||||
export interface PropinaRepartoFila {
|
||||
employee_id: number | null;
|
||||
empleado: string;
|
||||
monto: number;
|
||||
propinas: number;
|
||||
}
|
||||
|
||||
export interface ReportePropinas {
|
||||
status: string;
|
||||
start: string;
|
||||
end: string;
|
||||
activo: boolean;
|
||||
reparto: PropinasReparto;
|
||||
por_empleado: PropinaRepartoFila[];
|
||||
detalle: {
|
||||
venta_id: number; folio: string; date: string | null; empleado: string;
|
||||
employee_id: number | null; rol: string; monto: number; moneda: string; cuenta: string;
|
||||
}[];
|
||||
total: number;
|
||||
total_repartido: number;
|
||||
total_ventas: number;
|
||||
cuenta_propinas: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type AlcanceComision = 'global' | 'categoria' | 'servicio' | 'articulo' | 'membresia' | 'paquete';
|
||||
export type TipoComision = 'porcentaje' | 'monto_fijo' | 'escalonado';
|
||||
export type TipoBonoComision = 'fijo' | 'porcentaje';
|
||||
@@ -2424,6 +2582,8 @@ export const CLINIC_SETTINGS_DEFAULT: ClinicSettings = {
|
||||
recursos: RECURSOS_DEFAULT,
|
||||
compras: COMPRAS_DEFAULT,
|
||||
comisiones: COMISIONES_DEFAULT,
|
||||
lealtad: LEALTAD_DEFAULT,
|
||||
propinas: PROPINAS_DEFAULT,
|
||||
licencia: LICENCIA_DEFAULT,
|
||||
portal_cliente: PORTAL_CLIENTE_DEFAULT,
|
||||
};
|
||||
@@ -2595,10 +2755,13 @@ export interface PatientWallet {
|
||||
patient: string;
|
||||
wallet_id: number | null;
|
||||
points: number;
|
||||
valor_punto?: number;
|
||||
equivalent_mxn: number;
|
||||
equivalent_usd: number | null;
|
||||
rate: number | null;
|
||||
depositado: number;
|
||||
/** Nivel VIP del paciente (vacío si no hay niveles configurados). */
|
||||
nivel?: NivelPaciente | null;
|
||||
movimientos: WalletTransaction[];
|
||||
message?: string;
|
||||
}
|
||||
@@ -2850,6 +3013,13 @@ export interface PosCheckoutPayload {
|
||||
amount_received?: number;
|
||||
pay_with_points?: boolean;
|
||||
currency?: 'MXN' | 'USD';
|
||||
/** Cita de origen (para atribuir la propina al personal que atendió). */
|
||||
cita_id?: number;
|
||||
/** Propina: porcentaje sugerido sobre los servicios, o monto libre. */
|
||||
propina_pct?: number;
|
||||
propina?: number;
|
||||
propina_employee_id?: number;
|
||||
cobrador_employee_id?: number;
|
||||
}
|
||||
|
||||
export interface ImpuestosDesglose {
|
||||
@@ -2884,13 +3054,20 @@ export interface PosCheckoutResult {
|
||||
puntos_usados: number;
|
||||
puntos_ganados: number;
|
||||
wallet_points: number;
|
||||
/** Propina registrada (0 si el módulo está apagado). */
|
||||
propina?: number;
|
||||
propina_moneda?: string;
|
||||
/** Descuento automático por nivel VIP. */
|
||||
nivel_discount?: number;
|
||||
nivel?: NivelPaciente | null;
|
||||
reparto_propina?: { employee_id: number | null; empleado: string; rol: string; monto: number; moneda: string }[];
|
||||
stock_warnings?: string[];
|
||||
}
|
||||
|
||||
export interface DailyReport {
|
||||
date: string;
|
||||
citas: { total: number; by_state: Record<string, number> };
|
||||
ventas: { count: number; total: number; total_paid: number; total_due: number };
|
||||
ventas: { count: number; total: number; total_paid: number; total_due: number; total_propinas?: number; total_servicios?: number };
|
||||
pagos: { total: number; count: number; by_method: Record<string, number> };
|
||||
inventario: { total: number; by_type: Record<string, number> };
|
||||
}
|
||||
@@ -3120,6 +3297,37 @@ export const odooApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
// ---- Lealtad: puntos y niveles ----
|
||||
async getPatientLealtad(patientId: number): Promise<LealtadPaciente> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/lealtad`);
|
||||
return data;
|
||||
},
|
||||
|
||||
async recalcularLealtad(patientId: number): Promise<{ status: string; nivel: NivelPaciente; gasto_ventana: number; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/lealtad/recalcular`, {});
|
||||
return data;
|
||||
},
|
||||
|
||||
async setNivelLealtad(patientId: number, codigo: string, revisar?: string): Promise<{ status: string; nivel: NivelPaciente; message?: string }> {
|
||||
const { data } = await axios.put(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/lealtad`, { codigo, revisar });
|
||||
return data;
|
||||
},
|
||||
|
||||
async adjustPatientPoints(patientId: number, points: number, motivo: string): Promise<{ status: string; points: number; applied: number; equivalente_mxn: number; message?: string }> {
|
||||
const { data } = await axios.post(`${ODOO_BASE}/skeen/frontend/v1/patients/${patientId}/points/adjust`, { points, motivo });
|
||||
return data;
|
||||
},
|
||||
|
||||
async getReportLealtad(start?: string, end?: string): Promise<ReporteLealtad> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/lealtad`, { params: { start, end } });
|
||||
return data;
|
||||
},
|
||||
|
||||
async getReportPropinas(start?: string, end?: string): Promise<ReportePropinas> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/propinas`, { params: { start, end } });
|
||||
return data;
|
||||
},
|
||||
|
||||
// ---- Portal del cliente (personal: invitar, accesos, sesiones) ----
|
||||
async getPortalAccesos(): Promise<{ status: string; activo: boolean; accesos: PortalAcceso[]; message?: string }> {
|
||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/portal/accesos`);
|
||||
|
||||
Reference in New Issue
Block a user