204 lines
5.8 KiB
TypeScript
204 lines
5.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { odooApi } from '../services/odoo';
|
||
import { useAuth } from './auth';
|
||
|
||
// Configuración del ticket (diseño + datos de la clínica + logo).
|
||
//
|
||
// Vive en el servidor (ir.config_parameter) para que todas las cajas impriman
|
||
// igual. Antes los datos de la clínica se guardaban en el localStorage de cada
|
||
// navegador (clave skeen_clinic_settings), así que cada equipo imprimía una
|
||
// dirección distinta. Este módulo cachea la configuración en memoria, la
|
||
// comparte con quien la pida y deja el localStorage como respaldo de arranque.
|
||
|
||
export interface TicketClinica {
|
||
nombre: string;
|
||
direccion: string;
|
||
telefono: string;
|
||
email: string;
|
||
sitio: string;
|
||
rfc: string;
|
||
sucursal: string;
|
||
tiene_logo: boolean;
|
||
}
|
||
|
||
export interface TicketLayout {
|
||
ancho: '58mm' | '80mm';
|
||
fuente: 'monoespaciada' | 'sans';
|
||
letra: 'compacta' | 'normal' | 'grande';
|
||
separador: 'guiones' | 'solida' | 'ninguna';
|
||
mayusculas: boolean;
|
||
bloques: string[];
|
||
secciones: Record<string, boolean>;
|
||
textos: { encabezado: string; pie: string; politica: string };
|
||
}
|
||
|
||
export interface TicketConfig {
|
||
layout: TicketLayout;
|
||
clinica: TicketClinica;
|
||
logo: string;
|
||
}
|
||
|
||
const CACHE_KEY = 'skeen_ticket_config';
|
||
|
||
// Respaldo por si el API no responde (sin sesión, red caída): el último config
|
||
// leído, o el formato histórico del ticket si nunca se leyó nada.
|
||
export const FALLBACK_CONFIG: TicketConfig = {
|
||
layout: {
|
||
ancho: '80mm',
|
||
fuente: 'monoespaciada',
|
||
letra: 'normal',
|
||
separador: 'guiones',
|
||
mayusculas: true,
|
||
bloques: ['encabezado', 'cliente', 'conceptos', 'totales', 'pago', 'monedero', 'pie'],
|
||
secciones: {
|
||
nombre_clinica: true,
|
||
direccion: true,
|
||
telefono_clinica: true,
|
||
sucursal: true,
|
||
rfc: false,
|
||
mensaje_encabezado: true,
|
||
folio: true,
|
||
fecha: true,
|
||
cajero: true,
|
||
paciente: true,
|
||
telefono_paciente: false,
|
||
doctor: true,
|
||
conceptos: true,
|
||
detalle_conceptos: true,
|
||
subtotal: true,
|
||
descuento: true,
|
||
puntos_canjeados: true,
|
||
impuesto: true,
|
||
total: true,
|
||
total_usd: true,
|
||
metodo_pago: true,
|
||
recibido: true,
|
||
cambio: true,
|
||
pagado: true,
|
||
saldo_pendiente: true,
|
||
puntos_ganados: true,
|
||
monedero: true,
|
||
notas: true,
|
||
politica: false,
|
||
mensaje_pie: true,
|
||
},
|
||
textos: {
|
||
encabezado: '',
|
||
pie: '¡Gracias por su visita!',
|
||
politica: 'Conserve este ticket. No tiene validez fiscal.',
|
||
},
|
||
},
|
||
clinica: {
|
||
// Neutro a propósito: cada clínica configura su perfil en
|
||
// Configuración › Tickets. Si `nombre` queda vacío se usa el nombre del
|
||
// brand. Antes traía los datos de SKEEN y se filtraban a otras instancias.
|
||
nombre: '',
|
||
direccion: '',
|
||
telefono: '',
|
||
email: '',
|
||
sitio: '',
|
||
rfc: '',
|
||
sucursal: '',
|
||
tiene_logo: false,
|
||
},
|
||
logo: '',
|
||
};
|
||
|
||
const clonar = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
||
|
||
const leerLocal = (): TicketConfig | null => {
|
||
try {
|
||
const raw = localStorage.getItem(CACHE_KEY);
|
||
if (!raw) return null;
|
||
const parsed = JSON.parse(raw) as TicketConfig;
|
||
if (!parsed?.layout?.secciones || !parsed?.clinica) return null;
|
||
return parsed;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
let actual: TicketConfig = leerLocal() || clonar(FALLBACK_CONFIG);
|
||
const oyentes = new Set<(c: TicketConfig) => void>();
|
||
let peticion: Promise<TicketConfig> | null = null;
|
||
let yaCargo = false;
|
||
|
||
const publicar = (c: TicketConfig) => {
|
||
actual = c;
|
||
try {
|
||
localStorage.setItem(CACHE_KEY, JSON.stringify(c));
|
||
} catch {
|
||
// localStorage lleno o bloqueado: seguimos en memoria
|
||
}
|
||
oyentes.forEach((f) => f(c));
|
||
};
|
||
|
||
/** Fuerza la recarga (tras guardar en Configuración). */
|
||
export const recargarTicketConfig = async (): Promise<TicketConfig> => {
|
||
peticion = null;
|
||
yaCargo = false;
|
||
return cargarTicketConfig();
|
||
};
|
||
|
||
export const cargarTicketConfig = (): Promise<TicketConfig> => {
|
||
if (peticion) return peticion;
|
||
if (yaCargo) return Promise.resolve(actual);
|
||
peticion = odooApi.getTicketConfig()
|
||
.then((res) => {
|
||
if (res.status === 'success' && res.layout && res.clinica) {
|
||
publicar({
|
||
layout: res.layout as TicketConfig['layout'],
|
||
clinica: res.clinica as TicketConfig['clinica'],
|
||
logo: res.logo || '',
|
||
});
|
||
}
|
||
return actual;
|
||
})
|
||
.catch(() => actual)
|
||
.finally(() => {
|
||
peticion = null;
|
||
yaCargo = true;
|
||
});
|
||
return peticion;
|
||
};
|
||
|
||
/** Config actual dentro de React; se refresca sola cuando cambia. */
|
||
export const useTicketConfig = (): TicketConfig => {
|
||
const [cfg, setCfg] = useState<TicketConfig>(actual);
|
||
useEffect(() => {
|
||
const f = (c: TicketConfig) => setCfg(clonar(c));
|
||
oyentes.add(f);
|
||
cargarTicketConfig().then((c) => setCfg(clonar(c)));
|
||
return () => {
|
||
oyentes.delete(f);
|
||
};
|
||
}, []);
|
||
return cfg;
|
||
};
|
||
|
||
/** Versión no-React para componentes que solo leen (impresión puntual). */
|
||
export const ticketConfigActual = (): TicketConfig => actual;
|
||
|
||
/**
|
||
* Datos de la clínica para los documentos impresos (ticket, recibo, estado de
|
||
* cuenta, receta, corte, conteo). Sale de la configuración del servidor; si
|
||
* nunca se configuró, cae al skeen_clinic_settings de este navegador y de ahí
|
||
* al nombre del brand (SKEEN por omisión, o la clínica white-label).
|
||
*/
|
||
export const useClinicaPrint = () => {
|
||
const { brand } = useAuth();
|
||
const c = useTicketConfig().clinica;
|
||
const local = leerLocal()?.clinica;
|
||
const mix = (campo: keyof TicketClinica, porDefecto = '') =>
|
||
String(c[campo] || local?.[campo] || porDefecto || '');
|
||
return {
|
||
nombre: mix('nombre', brand.nombre),
|
||
direccion: mix('direccion', 'Playas de Rosarito, Baja California'),
|
||
telefono: mix('telefono'),
|
||
email: mix('email'),
|
||
sitio: mix('sitio'),
|
||
rfc: mix('rfc'),
|
||
sucursal: mix('sucursal'),
|
||
};
|
||
};
|