From 81ccb397f58b92e671eef0079b663c3359acacc5 Mon Sep 17 00:00:00 2001 From: "Carlos (Almeira)" Date: Sun, 21 Jun 2026 08:32:13 -0500 Subject: [PATCH] Fix PostCard image rendering and ID display, plus image backfill automation --- check_config.js | 74 -------- check_types.js | 18 -- import_openclaw.js | 140 +++++++++++++++ import_pendientes.js | 166 ++++++++++++++++++ setup_live_chat.js | 4 +- setup_stream_state.js | 4 +- src/app/dashboard/[entity]/page.tsx | 14 +- src/components/publications/PostCard.tsx | 4 +- .../publications/PublicationsList.tsx | 38 ++-- test_html.js | 3 - 10 files changed, 351 insertions(+), 114 deletions(-) delete mode 100644 check_config.js delete mode 100644 check_types.js create mode 100644 import_openclaw.js create mode 100644 import_pendientes.js delete mode 100644 test_html.js diff --git a/check_config.js b/check_config.js deleted file mode 100644 index 56156b6..0000000 --- a/check_config.js +++ /dev/null @@ -1,74 +0,0 @@ -const { Client, Databases } = require('node-appwrite'); - -async function checkConfig() { - console.log('🔍 Validando configuración de Almeira Web...\n'); - - const requiredVars = [ - 'NEXT_PUBLIC_APPWRITE_ENDPOINT', - 'NEXT_PUBLIC_APPWRITE_PROJECT_ID', - 'APPWRITE_API_KEY', - 'APPWRITE_DATABASE_ID' - ]; - - let missing = false; - requiredVars.forEach(v => { - if (!process.env[v]) { - console.error(`❌ Faltante: Variable de entorno ${v}`); - missing = true; - } else { - console.log(`✅ Presente: ${v}`); - } - }); - - if (missing) { - console.error('\n🛑 Error: Faltan variables de entorno críticas. Revisa tu archivo .env.local.'); - process.exit(1); - } - - const client = new Client() - .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) - .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) - .setKey(process.env.APPWRITE_API_KEY); - - const databases = new Databases(client); - const databaseId = process.env.APPWRITE_DATABASE_ID; - - try { - console.log(`\n📡 Conectando a Appwrite (${process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT})...`); - const db = await databases.get(databaseId); - console.log(`✅ Conexión exitosa a la base de datos: "${db.name}" (${databaseId})`); - - const collectionsToCheck = [ - 'posts', - 'settings', - 'publication_types', - 'categories', - 'whatsapp_messages', - 'entidad' - ]; - - console.log('\n📦 Verificando colecciones críticas...'); - for (const colId of collectionsToCheck) { - try { - const col = await databases.getCollection(databaseId, colId); - console.log(`✅ Colección encontrada: ${col.name} (${colId})`); - } catch (err) { - console.error(`❌ Error en colección ${colId}: ${err.message}`); - missing = true; - } - } - - } catch (err) { - console.error(`❌ Error de conexión a Appwrite: ${err.message}`); - process.exit(1); - } - - if (missing) { - console.error('\n🛑 Algunas colecciones no fueron encontradas. Revisa la base de datos en Appwrite.'); - process.exit(1); - } - - console.log('\n✨ ¡Configuración validada correctamente para producción!'); -} - -checkConfig(); diff --git a/check_types.js b/check_types.js deleted file mode 100644 index 67ef595..0000000 --- a/check_types.js +++ /dev/null @@ -1,18 +0,0 @@ -const sdk = require('node-appwrite'); -const client = new sdk.Client(); -const databases = new sdk.Databases(client); - -const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; -const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; -const API_KEY = process.env.APPWRITE_API_KEY || 'standard_ee81a4ca6784c0fa6a23abab0a983dbba9cd0c2d8e211a00c759d9801f5d8e27b43a605bfa373e13106da87acfc7673608f0ffb0eb04e10cae3dae93d2404bfda8676f96e3fcfd6bd642f6e12d69367916b796c54e98e2e4c4bc0b7a27aa42ba5048582fcbd45b1365b51c5014cb939c1853923c418048ff0e39c2237065da3c'; - -client.setEndpoint(ENDPOINT).setProject(PROJECT_ID).setKey(API_KEY); - -async function check() { - try { - console.log("Chequeando Tipos de Publicacion..."); - const res = await databases.listDocuments('almeira', 'publication_types', []); - console.log(JSON.stringify(res.documents.map(d => ({id: d.$id, c: d.codigo, d: d.descripcion})), null, 2)); - } catch(e) { console.error(e.message); } -} -check(); diff --git a/import_openclaw.js b/import_openclaw.js new file mode 100644 index 0000000..bf707b6 --- /dev/null +++ b/import_openclaw.js @@ -0,0 +1,140 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { Client, Databases, ID } = require('node-appwrite'); +require('dotenv').config({ path: '.env.local' }); +const mammoth = require('mammoth'); + +const client = new Client() + .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) + .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) + .setKey(process.env.APPWRITE_API_KEY); + +const databases = new Databases(client); +const dbId = process.env.APPWRITE_DATABASE_ID; +const collectionId = 'posts'; +const defaultTipoPublicacionId = '69c2b91400362d5d52b6'; // NOTICIA + +const DIR_PENDIENTES = '/home/carlos/Documentos/Pendientes'; + +function generateSlug(titulo) { + return titulo.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-').replace(/-+/g, '-').substring(0, 100); +} + +function truncateString(str, length) { + if (!str) return ''; + return str.length > length ? str.substring(0, length - 3) + '...' : str; +} + +function extractDate(filename) { + const regex = /(\d{1,2})\s+DE\s+([A-Z]+)\s+DEL?\s+(\d{4})/i; + const match = filename.match(regex); + if (match) { + const months = { 'enero': 0, 'febrero': 1, 'marzo': 2, 'abril': 3, 'mayo': 4, 'junio': 5, 'julio': 6, 'agosto': 7, 'septiembre': 8, 'octubre': 9, 'noviembre': 10, 'diciembre': 11 }; + const monthStr = match[2].toLowerCase(); + if (months[monthStr] !== undefined) { + return new Date(parseInt(match[3], 10), months[monthStr], parseInt(match[1], 10), 12, 0, 0).toISOString(); + } + } + return new Date().toISOString(); +} + +function extractDriveUrl(text) { + const match = text.match(/https:\/\/drive\.google\.com\/[^\s\n]+/); + return match ? match[0] : null; +} + +async function insertPost(title, contentBlocks, driveUrl, filename) { + if (!title || contentBlocks.length === 0) return; + + let pureContent = contentBlocks.filter(b => !b.includes('http')).map(p => `

${p}

`).join(''); + + const docData = { + titulo: truncateString(title, 70), + slug: generateSlug(title) + '-' + Math.random().toString(36).substring(2, 7), + contenido: pureContent, + resumen_redes: truncateString(contentBlocks[0], 155), + seo_titulo: truncateString(title, 60), + seo_descripcion: truncateString(contentBlocks[0], 155), + estado: 'borrador', + tipo_publicacion_id: defaultTipoPublicacionId, + fecha_publicacion: extractDate(filename), + ...(driveUrl && { url_destino: driveUrl }) + }; + + try { + await databases.createDocument(dbId, collectionId, ID.unique(), docData); + console.log(`✅ Creado: ${docData.titulo}`); + } catch (e) { + console.error(`❌ Error en ${title}:`, e.message); + } +} + +async function processFile(filePath, filename) { + const ext = path.extname(filename).toLowerCase(); + + if (ext === '.docx') { + const result = await mammoth.extractRawText({ path: filePath }); + const lines = result.value.split('\n').map(l => l.trim()).filter(l => l.length > 0); + if (lines.length > 0) { + const title = lines[0]; + const contentBlocks = lines.slice(1); + const driveUrl = extractDriveUrl(result.value); + await insertPost(title, contentBlocks, driveUrl, filename); + } + } else if (ext === '.pdf') { + const text = execSync(`pdftotext "${filePath}" -`).toString(); + const pages = text.split('\x0C').map(p => p.trim()).filter(p => p.length > 0); + + for (const page of pages) { + const lines = page.split('\n').map(l => l.trim()).filter(l => l.length > 0); + if (lines.length < 3) continue; + + let startIndex = 0; + if (lines[0].toLowerCase().includes('boletín')) { + startIndex = 1; // skip header + } + + const title = lines[startIndex]; + const contentBlocks = []; + let currentBlock = []; + for (let i = startIndex + 1; i < lines.length; i++) { + currentBlock.push(lines[i]); + if (lines[i].endsWith('.') || lines[i].includes('http')) { + contentBlocks.push(currentBlock.join(' ')); + currentBlock = []; + } + } + if (currentBlock.length > 0) contentBlocks.push(currentBlock.join(' ')); + + const driveUrl = extractDriveUrl(page); + await insertPost(title, contentBlocks, driveUrl, filename); + } + } +} + +async function main() { + let files = fs.readdirSync(DIR_PENDIENTES).filter(f => f.endsWith('.pdf') || f.endsWith('.docx')); + + files.sort((a, b) => { + const numA = parseInt(a.match(/\d+/)) || 0; + const numB = parseInt(b.match(/\d+/)) || 0; + return numA - numB; + }); + + // Only March and April: Boletines 22 to 37 + files = files.filter(f => { + const num = parseInt(f.match(/\d+/)) || 0; + return num >= 22 && num <= 37; + }); + + console.log(`Procesando ${files.length} archivos de Marzo y Abril...`); + + for (const file of files) { + console.log(`\n📄 Iniciando: ${file}`); + await processFile(path.join(DIR_PENDIENTES, file), file); + } + console.log('\n¡Proceso finalizado!'); +} + +main(); diff --git a/import_pendientes.js b/import_pendientes.js new file mode 100644 index 0000000..40505bc --- /dev/null +++ b/import_pendientes.js @@ -0,0 +1,166 @@ +const fs = require('fs'); +const path = require('path'); +const { Client, Databases, ID } = require('node-appwrite'); +require('dotenv').config({ path: '.env.local' }); +const pdfParse = require('pdf-parse'); +const mammoth = require('mammoth'); + +const client = new Client() + .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) + .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) + .setKey(process.env.APPWRITE_API_KEY); + +const databases = new Databases(client); +const dbId = process.env.APPWRITE_DATABASE_ID; +const collectionId = 'posts'; +const defaultTipoPublicacionId = '69c2b91400362d5d52b6'; // ID de NOTICIA obtenido anteriormente + +const DIR_PENDIENTES = '/home/carlos/Documentos/Pendientes'; +const DIR_PROCESADOS = path.join(DIR_PENDIENTES, 'Procesados'); + +if (!fs.existsSync(DIR_PROCESADOS)) { + fs.mkdirSync(DIR_PROCESADOS); +} + +function generateSlug(titulo) { + return titulo + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9\s-]/g, '') + .trim() + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .substring(0, 100); +} + +function formatContent(text) { + const paragraphs = text.split(/\n+/); + const html = paragraphs + .map(p => p.trim()) + .filter(p => p.length > 0) + .map(p => `

${p}

`) + .join(''); + return html; +} + +function extractSummary(text) { + const paragraphs = text.split(/\n+/).map(p => p.trim()).filter(p => p.length > 30); + if (paragraphs.length > 0) { + return paragraphs[0].substring(0, 200) + (paragraphs[0].length > 200 ? '...' : ''); + } + return ''; +} + +function determineTipo(filename) { + const lower = filename.toLowerCase(); + if (lower.includes('evento')) return 'evento'; + if (lower.includes('aviso')) return 'aviso'; + if (lower.includes('comunicado')) return 'comunicado'; + if (lower.includes('galeria')) return 'galeria'; + return 'noticia'; +} + +function formatTitle(filename) { + let base = path.basename(filename, path.extname(filename)).replace(/\(1\)/g, '').trim(); + // Capitalize first letter of each word to look better + return base.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); +} + +function extractDate(filename, text) { + const months = { + 'enero': 0, 'febrero': 1, 'marzo': 2, 'abril': 3, 'mayo': 4, 'junio': 5, + 'julio': 6, 'agosto': 7, 'septiembre': 8, 'octubre': 9, 'noviembre': 10, 'diciembre': 11 + }; + + // Attempt 1: From filename e.g. "28 DE MARZO DEL 2026" + const regex = /(\d{1,2})\s+DE\s+([A-Z]+)\s+(DEL?|DE)\s+(\d{4})/i; + let match = filename.match(regex); + if (!match) { + // Attempt 2: From the first few lines of text + const firstLines = text.split('\n').slice(0, 10).join(' '); + match = firstLines.match(regex); + } + + if (match) { + const day = parseInt(match[1], 10); + const monthStr = match[2].toLowerCase(); + const year = parseInt(match[4], 10); + + if (months[monthStr] !== undefined) { + const date = new Date(year, months[monthStr], day, 12, 0, 0); // Noon to avoid timezone issues + return date.toISOString(); + } + } + + return new Date().toISOString(); // fallback +} + +async function processFile(filePath, filename) { + const ext = path.extname(filename).toLowerCase(); + let text = ''; + + try { + if (ext === '.pdf') { + const { execSync } = require('child_process'); + text = execSync(`pdftotext "${filePath}" -`).toString(); + } else if (ext === '.docx') { + const result = await mammoth.extractRawText({ path: filePath }); + text = result.value; + } else if (ext === '.txt') { + text = fs.readFileSync(filePath, 'utf8'); + } else { + console.log(`Skipping unsupported file: ${filename}`); + return false; + } + + if (!text || text.trim().length === 0) { + console.log(`File empty or failed to extract: ${filename}`); + return false; + } + + const titulo = formatTitle(filename); + const slug = generateSlug(titulo) + '-' + Math.random().toString(36).substring(2, 7); + const contenido = formatContent(text); + const tipo = determineTipo(filename); + const resumen_redes = extractSummary(text); + + const docData = { + titulo, + slug, + contenido, + resumen_redes, + estado: 'publicado', + tipo_publicacion_id: defaultTipoPublicacionId, + fecha_publicacion: extractDate(filename, text), + }; + + await databases.createDocument(dbId, collectionId, ID.unique(), docData); + console.log(`✅ Publicado: ${titulo}`); + + // Mover a procesados + fs.renameSync(filePath, path.join(DIR_PROCESADOS, filename)); + return true; + } catch (error) { + console.error(`❌ Error procesando ${filename}:`, error.message); + return false; + } +} + +async function main() { + const files = fs.readdirSync(DIR_PENDIENTES); + let successCount = 0; + + for (const file of files) { + const filePath = path.join(DIR_PENDIENTES, file); + if (fs.statSync(filePath).isFile()) { + console.log(`Procesando ${file}...`); + const ok = await processFile(filePath, file); + if (ok) successCount++; + } + } + + console.log(`\nImportación finalizada. ${successCount} publicaciones procesadas correctamente.`); +} + +main(); diff --git a/setup_live_chat.js b/setup_live_chat.js index 5d03e38..6287e20 100644 --- a/setup_live_chat.js +++ b/setup_live_chat.js @@ -5,11 +5,11 @@ async function setupLiveChat() { console.log("Configuring Live Chat collection..."); const client = new Client() .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) - .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT) + .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) .setKey(process.env.APPWRITE_API_KEY); const databases = new Databases(client); - const dbId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE; + const dbId = process.env.APPWRITE_DATABASE_ID; try { const collection = await databases.createCollection( diff --git a/setup_stream_state.js b/setup_stream_state.js index 105912f..258481d 100644 --- a/setup_stream_state.js +++ b/setup_stream_state.js @@ -5,11 +5,11 @@ async function setupStreamState() { console.log("Configuring Stream State collection..."); const client = new Client() .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) - .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT) + .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) .setKey(process.env.APPWRITE_API_KEY); const databases = new Databases(client); - const dbId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE; + const dbId = process.env.APPWRITE_DATABASE_ID; try { const collection = await databases.createCollection( diff --git a/src/app/dashboard/[entity]/page.tsx b/src/app/dashboard/[entity]/page.tsx index fb21396..571a438 100644 --- a/src/app/dashboard/[entity]/page.tsx +++ b/src/app/dashboard/[entity]/page.tsx @@ -91,11 +91,23 @@ export default async function EntityPage({ const relKey = field.relationCollection as string; const relColId = collectionMap[relKey]; if (!relColId) return; + + const ids = Array.from(new Set( + response.documents + .map(d => typeof d[field.name] === 'object' && d[field.name] !== null ? d[field.name].$id : d[field.name]) + .filter(id => typeof id === 'string' && id.length > 0) + )); + + if (ids.length === 0) { + relationData[field.name] = []; + return; + } + try { const relRes = await client.databases.listDocuments( APPWRITE_ALMEIRA_CONFIG.databaseId, relColId, - [Query.limit(100)] + [Query.equal('$id', ids), Query.limit(100)] ); relationData[field.name] = JSON.parse(JSON.stringify(relRes.documents)); } catch { } diff --git a/src/components/publications/PostCard.tsx b/src/components/publications/PostCard.tsx index 1a7b509..702b0b0 100644 --- a/src/components/publications/PostCard.tsx +++ b/src/components/publications/PostCard.tsx @@ -77,9 +77,9 @@ export const PostCard: React.FC = ({ ); const postType = - typeof post.tipo_publicacion_id === 'object' + typeof post.tipo_publicacion_id === 'object' && post.tipo_publicacion_id !== null ? post.tipo_publicacion_id.descripcion - : post.tipo_publicacion_id; + : null; const clickTimer = useRef(null); diff --git a/src/components/publications/PublicationsList.tsx b/src/components/publications/PublicationsList.tsx index 9ab9df5..3bf0837 100644 --- a/src/components/publications/PublicationsList.tsx +++ b/src/components/publications/PublicationsList.tsx @@ -226,18 +226,32 @@ export const PublicationsList: React.FC = ({ {filteredAndSortedPosts.length > 0 ? (
- {filteredAndSortedPosts.map((post) => ( - - ))} + {filteredAndSortedPosts.map((post) => { + const enrichedPost = { ...post }; + + if (typeof enrichedPost.imagen_destacada_id === 'string' && relationData['imagen_destacada_id']) { + const mediaDoc = relationData['imagen_destacada_id'].find(m => m.$id === enrichedPost.imagen_destacada_id); + if (mediaDoc) enrichedPost.imagen_destacada_id = mediaDoc as any; + } + + if (typeof enrichedPost.tipo_publicacion_id === 'string' && relationData['tipo_publicacion_id']) { + const typeDoc = relationData['tipo_publicacion_id'].find(t => t.$id === enrichedPost.tipo_publicacion_id); + if (typeDoc) enrichedPost.tipo_publicacion_id = typeDoc as any; + } + + return ( + + ); + })}
) : (
diff --git a/test_html.js b/test_html.js deleted file mode 100644 index 3eb9e56..0000000 --- a/test_html.js +++ /dev/null @@ -1,3 +0,0 @@ -const DOMPurify = require('isomorphic-dompurify'); -const txt = "Line 1\nLine 2\n\nLine 3\nBold text"; -console.log(DOMPurify.sanitize(txt));