You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
11 KiB

import type {
ID,
Section,
Attribute,
JsonLogicEvaluator,
JsonLogic,
Value, AttrID, OptionID, SectionID, ObjectID, RuleID, ViewID, HotspotID
} from '../types/all.ts';
// ============================================================================
// UTILITIES (Con Mejoras)
// ============================================================================
/**
* ✅ MEJORADO: Evalúa display flags con soporte para condicionales
*/
export function evaluateDisplayFlag(
flag: boolean | JsonLogic | undefined,
defaultValue: boolean,
attributeValues: Record<ID<'attr'>, Value>,
evaluator: JsonLogicEvaluator
): boolean {
if (flag === undefined) return defaultValue;
if (typeof flag === 'boolean') return flag;
// Es JsonLogic: evaluar
const data = {
attributes: Object.fromEntries(
Object.entries(attributeValues).map(([id, value]) => [
id.split(':')[1], // "attr:suelo" → "suelo"
value
])
)
};
return evaluator.evaluate(flag, data);
}
/**
* ✅ MEJORADO: Generación de URL con validación y fallbacks
*/
export function generateSectionImageUrl(
section: Section,
globalAttributes : Record<AttrID, Value>,
sectionAttributes: Record<AttrID, Value>
): string {
const config = section.visualConfig;
if (!config.imageUrlTemplate) {
console.warn(`[Render] No imageUrlTemplate for section ${section.id}`);
return config.fallbackImage || '/images/fallback.jpg';
}
let url = config.imageUrlTemplate;
// Reemplazar {basePath}
if (config.basePath) {
url = url.replace('{basePath}', config.basePath);
}
// Reemplazar {sectionId}
const sectionId = section.id.split(':')[1];
if (sectionId) {
url = url.replace('{sectionId}', sectionId);
}
/* No overload matches this call.
/The last overload gave the following error.
Argument of type 'string | undefined' is not assignable to parameter of type '(substring: string, ...args: any[]) => string'.
Type 'undefined' is not assignable to type '(substring: string, ...args: any[]) => string'.ts(2769)
lib.es5.d.ts(470, 5): The last overload is declared here.
url = url.replace('{sectionId}', sectionId);
*/
// Reemplazar {global.attributeId}
config.globalAttributeDependencies?.forEach(attrId => {
const attrName = attrId.split(':')[1];
const placeholder = `{global.${attrName}}`;
const value = globalAttributes[attrId];
if (value !== undefined) {
// Normalizar valor para URL
const normalizedValue = String(value)
.toLowerCase()
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_]/g, '');
url = url.replace(placeholder, normalizedValue);
} else {
// Atributo faltante: usar 'default'
console.warn(`[Render] Missing global attribute ${attrId}, using 'default'`);
url = url.replace(placeholder, 'default');
}
});
// Reemplazar {section.attributeId}
config.sectionAttributeDependencies?.forEach(attrId => {
const attrName = attrId.split(':')[1];
const placeholder = `{section.${attrName}}`;
const value = sectionAttributes[attrId];
if (value !== undefined) {
const normalizedValue = String(value)
.toLowerCase()
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_]/g, '');
url = url.replace(placeholder, normalizedValue);
} else {
console.warn(`[Render] Missing section attribute ${attrId}, using 'default'`);
url = url.replace(placeholder, 'default');
}
});
// ✅ NUEVO: Validar que no queden placeholders sin reemplazar
const unresolvedPlaceholders = url.match(/\{[^}]+}/g);
if (unresolvedPlaceholders) {
console.error(
`[Render] Unresolved placeholders in URL: ${unresolvedPlaceholders.join(', ')}`,
`Original template: ${config.imageUrlTemplate}`,
`Partial URL: ${url}`
);
return config.fallbackImage || '/images/fallback.jpg';
}
return url;
}
/**
* ✅ NUEVO: Generación async de imagen via API
*/
export async function generateSectionImageAsync(
section: Section,
globalAttributes : Record<AttrID, Value>,
sectionAttributes: Record<AttrID, Value>
): Promise<string> {
const config = section.visualConfig;
const fallback = config.fallbackImage || '/images/fallback.jpg';
if (config.strategy !== 'api_generated' || !config.apiConfig) {
return generateSectionImageUrl(section, globalAttributes, sectionAttributes);
}
try {
const params: Record<string, Value> = {};
config.apiConfig.params?.forEach(attrId => {
const value = globalAttributes[attrId] ?? sectionAttributes[attrId];
if (value !== undefined) params[attrId] = value;
});
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.apiConfig.timeout || 5000);
const response = await fetch(config.apiConfig.endpoint, {
method: config.apiConfig.method || 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sectionId: section.id, attributes: params }),
signal: controller.signal
});
clearTimeout(timeoutId);
// ✅ SOLUCIÓN: En lugar de 'throw', manejamos el error y retornamos el fallback
if (!response.ok) {
console.error(`[Render] API ${config.apiConfig.endpoint} returned ${response.status}`);
return fallback;
}
const data = await response.json();
return data.imageUrl || fallback;
} catch (error) {
// El catch ahora solo se encarga de errores reales:
// fallos de red, JSON malformado o el Timeout (AbortError)
console.error(`[Render] Network or Timeout error for ${section.id}:`, error);
return fallback;
}
}
/**
* ✅ MEJORADO: Deep equality para valores complejos
*/
export function shouldRerenderSection(
section: Section,
changedAttributeId: AttrID,
oldValue: Value,
newValue: Value,
isGlobalAttribute: boolean
): boolean {
const config = section.visualConfig;
// Verificar si el atributo afecta a esta sección
const affectsThisSection = isGlobalAttribute
? config.globalAttributeDependencies?.includes(changedAttributeId)
: config.sectionAttributeDependencies?.includes(changedAttributeId);
if (!affectsThisSection) return false;
// Deep equality para valores complejos
return !deepEqual(oldValue, newValue);
}
/**
* Deep equality check
*/
export function deepEqual(a: Value, b: Value): boolean {
// 1. Chequeo de identidad simple (cubre primitivos iguales)
if (a === b) return true;
// 2. Si alguno de los dos NO es un objeto o es null, devuelve false
// (Esto descarta string, number, boolean, ID y null de un golpe)
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
return false;
}
// =========================================================
// AHORA TypeScript SABE que 'a' y 'b' son OBJETOS (Date o Array)
// =========================================================
// 3. Ahora es seguro preguntar por Date
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// 4. Chequeo de Arrays
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((val, idx) => deepEqual(val, b[idx]));
}
// 5. Si son objetos pero no Dates ni Arrays, comparamos sus claves (opcional, pero recomendado)
// Esto es útil si tu 'Value' llega a incluir objetos planos en el futuro.
/*
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => deepEqual((a as any)[key], (b as any)[key]));
*/
return false;
}
// ============================================================================
// VALIDATION (Runtime)
// ============================================================================
/**
* ✅ NUEVO: Validar modelo en runtime
*/
export function validateAttribute(attr: Attribute): ValidationError[] {
const errors: ValidationError[] = [];
// Computed debe ser readonly
if (attr.type === 'computed' && !attr.display.readonly) {
errors.push({
attributeId: attr.id,
message: 'Computed attributes should be readonly',
severity: 'error'
});
}
// Fixed no debería afectar precio
if (attr.type === 'fixed' && attr.display.affectsPrice) {
errors.push({
attributeId: attr.id,
message: 'Fixed attributes should not affect price',
severity: 'warning'
});
}
// Verificar que dependencies existen
if (attr.type === 'computed') {
if (!attr.dependencies || attr.dependencies.length === 0) {
errors.push({
attributeId: attr.id,
message: 'Computed attribute has no dependencies',
severity: 'warning'
});
}
}
return errors;
}
export function validateSection(section: Section): ValidationError[] {
const errors: ValidationError[] = [];
// Validar que imageUrlTemplate tiene placeholders válidos
if (section.visualConfig.imageUrlTemplate) {
const template = section.visualConfig.imageUrlTemplate;
const placeholders = template.match(/\{[^}]+}/g) || [];
placeholders.forEach(placeholder => {
const cleanName = placeholder.slice(1, -1); // Remove { }
if (cleanName.startsWith('global.')) {
const attrName = cleanName.replace('global.', '');
const attrId = `at:${attrName}` as AttrID;
if (!section.visualConfig.globalAttributeDependencies?.includes(attrId)) {
errors.push({
sectionId: section.id,
message: `Placeholder ${placeholder} not in globalAttributeDependencies`,
severity: 'error'
});
}
}
});
}
return errors;
}
export interface ValidationError {
attributeId?: ID<'at'>;
sectionId?: ID<'sc'>;
message: string;
severity: 'error' | 'warning';
}

Powered by TurnKey Linux.