|
|
|
|
import { dataConfig } from './DataConfig';
|
|
|
|
|
import type {DataConfig, Rules, OptionGroup, View} from './DataConfigTypes';
|
|
|
|
|
import type { AppState } from './AppStateTypes';
|
|
|
|
|
|
|
|
|
|
class DataConfigStore {
|
|
|
|
|
|
|
|
|
|
imgSrc : string = dataConfig.sources.imgViewsHi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// ESTADO REACTIVO
|
|
|
|
|
// ============================================
|
|
|
|
|
selection = $state<AppState>({
|
|
|
|
|
currentViewId: '',
|
|
|
|
|
general: {},
|
|
|
|
|
views: {}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// DEBUGGING
|
|
|
|
|
// ============================================
|
|
|
|
|
private debugLog(method: string, data: any) {
|
|
|
|
|
console.log(`[AppState.${method}]`, {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
currentViewId: this.selection.currentViewId,
|
|
|
|
|
...data
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
// Método público para inspeccionar el estado completo
|
|
|
|
|
inspect() {
|
|
|
|
|
console.group('🔍 AppState Inspector');
|
|
|
|
|
console.log('Current View ID:', this.selection.currentViewId);
|
|
|
|
|
console.log('Current View:', this.currentView);
|
|
|
|
|
console.log('General Options:', this.selection.general);
|
|
|
|
|
console.log('All Views State:', this.selection.views);
|
|
|
|
|
console.log('Available Views:', Object.keys(dataConfig.options.views));
|
|
|
|
|
console.groupEnd();
|
|
|
|
|
return this.selection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// CONSTRUCTOR E INICIALIZACIÓN
|
|
|
|
|
// ============================================
|
|
|
|
|
constructor() {
|
|
|
|
|
this.init();
|
|
|
|
|
|
|
|
|
|
// Exponer en window para debug desde consola
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
(window as any).appState = this;
|
|
|
|
|
console.log('✅ appState disponible en window.appState');
|
|
|
|
|
console.log('🔍 Usa: window.appState.inspect() para ver el estado');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private init() {
|
|
|
|
|
const viewIds = Object.keys(dataConfig.options.views);
|
|
|
|
|
|
|
|
|
|
if (viewIds.length === 0) {
|
|
|
|
|
throw new Error('Configuración inválida: debe existir al menos una vista');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state: AppState = {
|
|
|
|
|
currentViewId: viewIds[0],
|
|
|
|
|
general: {},
|
|
|
|
|
views: {}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Inicializar generales con defaults
|
|
|
|
|
for (const [key, group] of Object.entries(dataConfig.options.general)) {
|
|
|
|
|
const g = group as OptionGroup;
|
|
|
|
|
state.general[key] = g.default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inicializar vistas con defaults
|
|
|
|
|
Object.keys(dataConfig.options.views).forEach((viewId) => {
|
|
|
|
|
const view = dataConfig.options.views[viewId];
|
|
|
|
|
state.views[viewId] = {}
|
|
|
|
|
view.options.forEach((group: OptionGroup) => {
|
|
|
|
|
state.views[viewId][group.id] = group.default;
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.selection = state;
|
|
|
|
|
|
|
|
|
|
this.debugLog('init', {
|
|
|
|
|
initialViewId: state.currentViewId,
|
|
|
|
|
viewsCount: viewIds.length,
|
|
|
|
|
availableViews: viewIds
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// ACCIONES PÚBLICAS (Cambios de Estado)
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
/** Cambiar de habitación/estancia */
|
|
|
|
|
setView(viewId: string) {
|
|
|
|
|
const previousViewId = this.selection.currentViewId;
|
|
|
|
|
|
|
|
|
|
if (!dataConfig.options.views[viewId]) {
|
|
|
|
|
console.error(`❌ Vista ${viewId} no existe`);
|
|
|
|
|
this.debugLog('setView', { error: 'View not found', viewId });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.selection.currentViewId = viewId;
|
|
|
|
|
|
|
|
|
|
this.debugLog('setView', {
|
|
|
|
|
from: previousViewId,
|
|
|
|
|
to: viewId,
|
|
|
|
|
success: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Cambiar una opción (General o de Vista) */
|
|
|
|
|
setOption(viewId: string | 'general', groupId: string, valueId: string) {
|
|
|
|
|
if (viewId === 'general') {
|
|
|
|
|
if (!(groupId in dataConfig.options.general)) {
|
|
|
|
|
console.warn(`Grupo ${groupId} no existe en general`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previousValue = this.selection.general[groupId];
|
|
|
|
|
this.selection.general[groupId] = valueId;
|
|
|
|
|
|
|
|
|
|
this.debugLog('setOption', {
|
|
|
|
|
type: 'general',
|
|
|
|
|
groupId,
|
|
|
|
|
from: previousValue,
|
|
|
|
|
to: valueId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.revalidateAll();
|
|
|
|
|
} else {
|
|
|
|
|
if (!(viewId in this.selection.views)) {
|
|
|
|
|
console.warn(`Vista ${viewId} no existe`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!(groupId in this.selection.views[viewId])) {
|
|
|
|
|
console.warn(`Grupo ${groupId} no existe en vista ${viewId}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previousValue = this.selection.views[viewId][groupId];
|
|
|
|
|
this.selection.views[viewId][groupId] = valueId;
|
|
|
|
|
|
|
|
|
|
this.debugLog('setOption', {
|
|
|
|
|
type: 'view',
|
|
|
|
|
viewId,
|
|
|
|
|
groupId,
|
|
|
|
|
from: previousValue,
|
|
|
|
|
to: valueId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.revalidateView(viewId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Resetear todo o una vista específica */
|
|
|
|
|
reset(viewId?: string) {
|
|
|
|
|
if (viewId) {
|
|
|
|
|
const view = dataConfig.options.views[viewId];
|
|
|
|
|
if (!view) {
|
|
|
|
|
console.warn(`Vista ${viewId} no existe`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
view.options.forEach((group: OptionGroup) => {
|
|
|
|
|
this.selection.views[viewId][group.id] = group.default;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.debugLog('reset', { viewId, type: 'single' });
|
|
|
|
|
} else {
|
|
|
|
|
this.init();
|
|
|
|
|
this.debugLog('reset', { type: 'all' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// GETTERS PÚBLICOS (Datos Derivados)
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
get currentView(): View {
|
|
|
|
|
const view = dataConfig.options.views[this.selection.currentViewId];
|
|
|
|
|
if (!view) {
|
|
|
|
|
console.error(`❌ Vista ${this.selection.currentViewId} no existe en la configuración`);
|
|
|
|
|
throw new Error(`Vista ${this.selection.currentViewId} no existe en la configuración`);
|
|
|
|
|
}
|
|
|
|
|
return view;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Nombre de la imagen de la vista ACTUAL */
|
|
|
|
|
get currentFileName() {
|
|
|
|
|
const viewId = this.selection.currentViewId;
|
|
|
|
|
|
|
|
|
|
if (!dataConfig.options.views[viewId]) {
|
|
|
|
|
console.error(`Vista ${viewId} no existe`);
|
|
|
|
|
return 'error.jpg';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parts: string[] = [viewId];
|
|
|
|
|
|
|
|
|
|
// 1. Generales
|
|
|
|
|
Object.keys(dataConfig.options.general).forEach(genKey => {
|
|
|
|
|
parts.push(this.selection.general[genKey]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. Específicos de la vista actual
|
|
|
|
|
const viewConfig = dataConfig.options.views[viewId];
|
|
|
|
|
if (viewConfig?.options) {
|
|
|
|
|
viewConfig.options.forEach((group: OptionGroup) => {
|
|
|
|
|
const val = this.selection.views[viewId][group.id];
|
|
|
|
|
if (val) parts.push(val);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.imgSrc + `${parts.join('_')}.jpg`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Obtener el OptionValue completo por ID */
|
|
|
|
|
getOptionValue(optionId: string) {
|
|
|
|
|
return dataConfig.options.values[optionId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Obtener todas las opciones de un grupo con sus valores completos */
|
|
|
|
|
getGroupOptions(viewId: string | 'general', groupId: string) {
|
|
|
|
|
let group: OptionGroup | undefined;
|
|
|
|
|
|
|
|
|
|
if (viewId === 'general') {
|
|
|
|
|
group = dataConfig.options.general[groupId];
|
|
|
|
|
} else {
|
|
|
|
|
const view = dataConfig.options.views[viewId];
|
|
|
|
|
group = view?.options.find(g => g.id === groupId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!group) return [];
|
|
|
|
|
|
|
|
|
|
return group.values.map(ref => ({
|
|
|
|
|
...ref,
|
|
|
|
|
...this.getOptionValue(ref.id),
|
|
|
|
|
isValid: this.isValid(ref.rules)
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Verificar si una opción requiere consulta */
|
|
|
|
|
requiresConsultation(viewId: string, groupId: string, optionId: string): {
|
|
|
|
|
required: boolean;
|
|
|
|
|
message?: string;
|
|
|
|
|
} {
|
|
|
|
|
const view = dataConfig.options.views[viewId];
|
|
|
|
|
const group = view?.options.find(g => g.id === groupId);
|
|
|
|
|
const option = group?.values.find(v => v.id === optionId);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
required: !!option?.warn || option?.amount === 'consultar',
|
|
|
|
|
message: option?.warn
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Obtener el valor actual de una opción */
|
|
|
|
|
getCurrentOption(viewId: string | 'general', groupId: string): string | undefined {
|
|
|
|
|
if (viewId === 'general') {
|
|
|
|
|
return this.selection.general[groupId];
|
|
|
|
|
}
|
|
|
|
|
return this.selection.views[viewId]?.[groupId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Obtener todas las vistas disponibles */
|
|
|
|
|
getAllViews(): View[] {
|
|
|
|
|
return Object.values(dataConfig.options.views);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Obtener los IDs de todas las vistas */
|
|
|
|
|
getAllViewIds(): string[] {
|
|
|
|
|
return Object.keys(dataConfig.options.views);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// VALIDACIÓN
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
/** Validar reglas de una opción */
|
|
|
|
|
isValid(rules?: Rules): boolean {
|
|
|
|
|
if (!rules) return true;
|
|
|
|
|
|
|
|
|
|
// Validar reglas generales
|
|
|
|
|
if (rules.general) {
|
|
|
|
|
for (const [key, constraint] of Object.entries(rules.general)) {
|
|
|
|
|
const currentVal = this.selection.general[key];
|
|
|
|
|
if (constraint.forbid?.includes(currentVal)) return false;
|
|
|
|
|
if (constraint.allow && !constraint.allow.includes(currentVal)) return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validar reglas de vistas
|
|
|
|
|
if (rules.views) {
|
|
|
|
|
for (const [viewId, groupConstraints] of Object.entries(rules.views)) {
|
|
|
|
|
for (const [groupId, constraint] of Object.entries(groupConstraints)) {
|
|
|
|
|
const currentVal = this.selection.views[viewId]?.[groupId];
|
|
|
|
|
if (!currentVal) continue;
|
|
|
|
|
if (constraint.forbid?.includes(currentVal)) return false;
|
|
|
|
|
if (constraint.allow && !constraint.allow.includes(currentVal)) return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// MÉTODOS PRIVADOS (Helpers Internos)
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
/** Revalidar todas las vistas */
|
|
|
|
|
private revalidateAll() {
|
|
|
|
|
this.debugLog('revalidateAll', { trigger: 'general option changed' });
|
|
|
|
|
|
|
|
|
|
Object.keys(dataConfig.options.views).forEach(vId => {
|
|
|
|
|
this.revalidateView(vId);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Revalidar solo una vista específica */
|
|
|
|
|
private revalidateView(viewId: string) {
|
|
|
|
|
const view = dataConfig.options.views[viewId];
|
|
|
|
|
if (!view) return;
|
|
|
|
|
|
|
|
|
|
const changes: any[] = [];
|
|
|
|
|
|
|
|
|
|
view.options.forEach((group: OptionGroup) => {
|
|
|
|
|
const currentId = this.selection.views[viewId][group.id];
|
|
|
|
|
const currentOpt = group.values.find(v => v.id === currentId);
|
|
|
|
|
|
|
|
|
|
// Si la opción actual no es válida
|
|
|
|
|
if (currentOpt?.rules && !this.isValid(currentOpt.rules)) {
|
|
|
|
|
// Buscar la primera opción válida en el grupo
|
|
|
|
|
const validOption = this.findFirstValidOption(group);
|
|
|
|
|
|
|
|
|
|
if (validOption) {
|
|
|
|
|
changes.push({
|
|
|
|
|
group: group.id,
|
|
|
|
|
from: currentId,
|
|
|
|
|
to: validOption,
|
|
|
|
|
reason: 'rule validation failed'
|
|
|
|
|
});
|
|
|
|
|
this.selection.views[viewId][group.id] = validOption;
|
|
|
|
|
} else {
|
|
|
|
|
// Si no hay ninguna opción válida, usar default como fallback
|
|
|
|
|
console.warn(`No se encontró ninguna opción válida para ${viewId}.${group.id}, usando default`);
|
|
|
|
|
changes.push({
|
|
|
|
|
group: group.id,
|
|
|
|
|
from: currentId,
|
|
|
|
|
to: group.default,
|
|
|
|
|
reason: 'no valid option found, using default'
|
|
|
|
|
});
|
|
|
|
|
this.selection.views[viewId][group.id] = group.default;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (changes.length > 0) {
|
|
|
|
|
this.debugLog('revalidateView', { viewId, changes });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Encontrar la primera opción válida en un grupo */
|
|
|
|
|
private findFirstValidOption(group: OptionGroup): string | null {
|
|
|
|
|
// Primero intentar con el default si es válido
|
|
|
|
|
const defaultOpt = group.values.find(v => v.id === group.default);
|
|
|
|
|
if (defaultOpt && this.isValid(defaultOpt.rules)) {
|
|
|
|
|
return group.default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Si el default no es válido, buscar la primera opción válida
|
|
|
|
|
for (const opt of group.values) {
|
|
|
|
|
if (this.isValid(opt.rules)) {
|
|
|
|
|
return opt.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Si no hay ninguna opción válida, retornar null
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const appState = new DataConfigStore();
|