Files
SKEEN-Proyecto/frontend/src/lib/ticketConfig.ts

204 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'),
};
};