diff --git a/publish-cauca-web.js b/publish-cauca-web.js index e7382fd..649b18c 100644 --- a/publish-cauca-web.js +++ b/publish-cauca-web.js @@ -7,10 +7,12 @@ require('dotenv').config({ path: '.env.local' }); let InputFile; try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; } +const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim'); 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); +applyTablesDbShim(client); const databases = new Databases(client); const storage = new Storage(client); diff --git a/scrape-multivision.js b/scrape-multivision.js index 73ade7f..643fbb0 100644 --- a/scrape-multivision.js +++ b/scrape-multivision.js @@ -8,10 +8,12 @@ require('dotenv').config({ path: '.env.local' }); let InputFile; try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; } +const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim'); 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); +applyTablesDbShim(client); const databases = new Databases(client); const storage = new Storage(client); @@ -246,8 +248,9 @@ async function run() { const doc = { titulo: data.title.substring(0, 100), slug: `multivision-final-${Date.now()}-${count}`, + tipo: 'noticia', tipo_publicacion_id: typeId, - estado: 'Publicado', + estado: 'publicado', fecha_publicacion: data.date, contenido: data.content, resumen_redes: data.resumen, diff --git a/scripts/extract_wordpress_sql.js b/scripts/extract_wordpress_sql.js new file mode 100644 index 0000000..3dda3ba --- /dev/null +++ b/scripts/extract_wordpress_sql.js @@ -0,0 +1,283 @@ +const fs = require('fs'); +const zlib = require('zlib'); +const path = require('path'); + +const SQL_GZ_PATH = 'C:/Users/Carlos/Downloads/u858407019_Lq8sf.almeiraproducciones-com.20260607074508.sql.gz'; +const OUT_JSON_PATH = path.join(__dirname, 'wp_extracted_data.json'); + +// Parse SQL tuple lines/blocks for a given table +function createTableTupleParser(onRow) { + let inTuple = false; + let inString = false; + let escapeNext = false; + let currentField = ''; + let currentRow = []; + let tokenWasString = false; + + return function feedChunk(str) { + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + + if (!inTuple) { + if (ch === '(') { + // Make sure this '(' is start of a tuple (not CREATE TABLE or INSERT INTO col list) + inTuple = true; + inString = false; + escapeNext = false; + currentField = ''; + currentRow = []; + tokenWasString = false; + } + continue; + } + + if (inString) { + if (escapeNext) { + if (ch === 'n') currentField += '\n'; + else if (ch === 'r') currentField += '\r'; + else if (ch === 't') currentField += '\t'; + else if (ch === '0') currentField += '\0'; + else currentField += ch; + escapeNext = false; + } else if (ch === '\\') { + escapeNext = true; + } else if (ch === "'") { + inString = false; + } else { + currentField += ch; + } + } else { + if (ch === "'") { + inString = true; + tokenWasString = true; + } else if (ch === ',') { + const val = tokenWasString ? currentField : (currentField.trim() === 'NULL' ? null : currentField.trim()); + currentRow.push(val); + currentField = ''; + tokenWasString = false; + } else if (ch === ')') { + const val = tokenWasString ? currentField : (currentField.trim() === 'NULL' ? null : currentField.trim()); + currentRow.push(val); + onRow(currentRow); + inTuple = false; + currentField = ''; + currentRow = []; + tokenWasString = false; + } else { + currentField += ch; + } + } + } + }; +} + +async function run() { + console.log('Extrayendo datos históricos de WordPress desde:', SQL_GZ_PATH); + + const terms = {}; // term_id -> { id, name, slug } + const taxonomies = {}; // term_taxonomy_id -> { term_id, taxonomy } + const postCategories = {}; // post_id -> [term_taxonomy_id] + const posts = {}; // post_id -> post object + const attachments = {}; // attachment_id -> { id, title, guid, mime } + const postThumbnails = {}; // post_id -> attachment_id + const attachedFiles = {}; // attachment_id -> relative path in wp-content/uploads + const attachmentAlts = {}; // attachment_id -> alt text + + const targetTables = new Set([ + 'wp_terms', + 'wp_term_taxonomy', + 'wp_term_relationships', + 'wp_posts', + 'wp_postmeta', + ]); + + let currentTable = null; + let insideValues = false; + + const parsers = { + wp_terms: createTableTupleParser((row) => { + if (row.length >= 3) { + terms[row[0]] = { id: row[0], name: row[1], slug: row[2] }; + } + }), + wp_term_taxonomy: createTableTupleParser((row) => { + if (row.length >= 3 && row[2] === 'category') { + taxonomies[row[0]] = { term_taxonomy_id: row[0], term_id: row[1], taxonomy: row[2] }; + } + }), + wp_term_relationships: createTableTupleParser((row) => { + if (row.length >= 2) { + const objId = row[0]; + const taxId = row[1]; + if (!postCategories[objId]) postCategories[objId] = []; + postCategories[objId].push(taxId); + } + }), + wp_posts: createTableTupleParser((row) => { + if (row.length < 22) return; + const id = row[0]; + const postDate = row[2]; + const postDateGmt = row[3]; + const postContent = row[4]; + const postTitle = row[5]; + const postExcerpt = row[6]; + const postStatus = row[7]; + const postName = row[11]; + const postParent = row[17]; + const guid = row[18]; + const postType = row[20]; + const postMimeType = row[21]; + + if (postType === 'post' && postStatus === 'publish') { + posts[id] = { + id, + date: postDateGmt && postDateGmt !== '0000-00-00 00:00:00' ? postDateGmt : postDate, + title: postTitle, + content: postContent, + excerpt: postExcerpt, + slug: postName, + guid, + }; + } else if (postType === 'attachment') { + attachments[id] = { + id, + title: postTitle, + guid, + mime: postMimeType, + parent: postParent, + }; + } + }), + wp_postmeta: createTableTupleParser((row) => { + if (row.length < 4) return; + const postId = row[1]; + const metaKey = row[2]; + const metaValue = row[3]; + if (metaKey === '_thumbnail_id' && metaValue) { + postThumbnails[postId] = String(metaValue).trim(); + } else if (metaKey === '_wp_attached_file' && metaValue) { + attachedFiles[postId] = String(metaValue).trim(); + } else if (metaKey === '_wp_attachment_image_alt' && metaValue) { + attachmentAlts[postId] = String(metaValue).trim(); + } + }), + }; + + const stream = fs.createReadStream(SQL_GZ_PATH).pipe(zlib.createGunzip()); + let leftover = ''; + + for await (const chunk of stream) { + const text = leftover + chunk.toString('utf8'); + const lines = text.split('\n'); + leftover = lines.pop(); + + for (const line of lines) { + if (line.startsWith('-- Dumping data for table `')) { + const m = line.match(/^-- Dumping data for table `([^`]+)`/); + if (m && targetTables.has(m[1])) { + currentTable = m[1]; + insideValues = false; + console.log(`-> Leyendo tabla ${currentTable}...`); + } else { + currentTable = null; + insideValues = false; + } + continue; + } + + if (!currentTable) continue; + + if (line.startsWith('/*!40000 ALTER TABLE') && line.includes('ENABLE KEYS')) { + currentTable = null; + insideValues = false; + continue; + } + + if (line.startsWith('INSERT INTO `')) { + insideValues = true; + const idx = line.indexOf('VALUES'); + if (idx !== -1) { + const rest = line.slice(idx + 6); + if (rest.trim()) parsers[currentTable](rest + '\n'); + } + continue; + } + + if (insideValues) { + parsers[currentTable](line + '\n'); + } + } + } + + // Build categories list + const categoriesList = []; + const taxIdToCategorySlug = {}; + for (const taxId of Object.keys(taxonomies)) { + const tax = taxonomies[taxId]; + const term = terms[tax.term_id]; + if (term) { + categoriesList.push({ + term_id: term.id, + term_taxonomy_id: taxId, + nombre: term.name, + slug: term.slug, + }); + taxIdToCategorySlug[taxId] = term.slug; + } + } + + // Enrich posts with featured image info and category slugs + const postsList = Object.values(posts).map((p) => { + let thumbId = postThumbnails[p.id] || null; + let imageRelPath = thumbId ? attachedFiles[thumbId] || null : null; + let imageGuid = thumbId && attachments[thumbId] ? attachments[thumbId].guid : null; + let imageTitle = thumbId && attachments[thumbId] ? attachments[thumbId].title : p.title; + let imageAlt = thumbId ? attachmentAlts[thumbId] || p.title : p.title; + + // Fallback: if no _thumbnail_id, check if an attachment has post_parent == p.id or extract first wp-content/uploads img from content + if (!imageRelPath && p.content) { + const m = p.content.match(/wp-content\/uploads\/([^"'\s>]+)/i); + if (m) { + imageRelPath = m[1]; + } + } + + const catTaxIds = postCategories[p.id] || []; + const catSlugs = catTaxIds.map((tid) => taxIdToCategorySlug[tid]).filter(Boolean); + + return { + ...p, + thumbId, + imageRelPath, + imageGuid, + imageTitle, + imageAlt, + categories: catSlugs, + }; + }); + + // Sort posts chronologically + postsList.sort((a, b) => new Date(a.date) - new Date(b.date)); + + const withImageCount = postsList.filter((p) => p.imageRelPath).length; + console.log(`\n======================================================`); + console.log(`✅ Extracción completada:`); + console.log(` - Categorías de WordPress: ${categoriesList.length}`); + console.log(` - Publicaciones históricas (publish): ${postsList.length}`); + console.log(` - Publicaciones con imagen destacada identificada: ${withImageCount}`); + console.log(` - Total adjuntos (attachments) en WP: ${Object.keys(attachments).length}`); + console.log(`======================================================`); + + fs.writeFileSync( + OUT_JSON_PATH, + JSON.stringify({ categories: categoriesList, posts: postsList }, null, 2), + 'utf8' + ); + console.log(`Guardado en: ${OUT_JSON_PATH}`); +} + +run().catch((e) => { + console.error('Error:', e); + process.exit(1); +}); + diff --git a/scripts/extract_wp_images_from_tar.js b/scripts/extract_wp_images_from_tar.js new file mode 100644 index 0000000..a041889 --- /dev/null +++ b/scripts/extract_wp_images_from_tar.js @@ -0,0 +1,204 @@ +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); +const sharp = require('sharp'); + +const TAR_GZ_PATH = 'C:/Users/Carlos/Downloads/u858407019.almeiraproducciones-com.20260607074508.tar.gz'; +const WP_DATA_PATH = path.join(__dirname, 'wp_extracted_data.json'); +const OUT_DIR = 'C:/tmp/wp_images'; + +if (!fs.existsSync(OUT_DIR)) { + fs.mkdirSync(OUT_DIR, { recursive: true }); +} + +function safeFileKey(relPath) { + return relPath.replace(/[^a-zA-Z0-9._-]/g, '_'); +} + +async function run() { + const wpData = JSON.parse(fs.readFileSync(WP_DATA_PATH, 'utf8')); + const neededRelPaths = new Map(); // normalized lower relPath -> original relPath + const neededBasenames = new Map(); // lower basename -> original relPath + + for (const post of wpData.posts) { + if (post.imageRelPath) { + const clean = post.imageRelPath.replace(/^\/+/, ''); + neededRelPaths.set(clean.toLowerCase(), clean); + neededBasenames.set(path.posix.basename(clean).toLowerCase(), clean); + } + } + + console.log(`Total imágenes destacadas únicas requeridas por los ${wpData.posts.length} posts: ${neededRelPaths.size}`); + + let extractedCount = 0; + let scannedFiles = 0; + let nextLongName = null; + let nextPaxPath = null; + + // Streaming tar reader using a growable/ring buffer list + let chunks = []; + let totalBuffered = 0; + + function consumeBytes(n) { + const out = Buffer.allocUnsafe(n); + let offset = 0; + while (offset < n) { + const first = chunks[0]; + const toCopy = Math.min(first.length, n - offset); + first.copy(out, offset, 0, toCopy); + offset += toCopy; + if (toCopy === first.length) { + chunks.shift(); + } else { + chunks[0] = first.subarray(toCopy); + } + } + totalBuffered -= n; + return out; + } + + function skipBytes(n) { + let remaining = n; + while (remaining > 0) { + const first = chunks[0]; + if (first.length <= remaining) { + remaining -= first.length; + chunks.shift(); + } else { + chunks[0] = first.subarray(remaining); + remaining = 0; + } + } + totalBuffered -= n; + } + + let state = 'HEADER'; // 'HEADER' | 'DATA' | 'SKIP' + let currentEntrySize = 0; + let currentEntryPadded = 0; + let currentEntryType = '0'; + let currentMatchedRel = null; + + const pendingSharpPromises = []; + + const gunzip = zlib.createGunzip({ chunkSize: 1024 * 1024 }); + const readStream = fs.createReadStream(TAR_GZ_PATH, { highWaterMark: 1024 * 1024 }); + const stream = readStream.pipe(gunzip); + + for await (const chunk of stream) { + chunks.push(chunk); + totalBuffered += chunk.length; + + while (true) { + if (state === 'HEADER') { + if (totalBuffered < 512) break; + const header = consumeBytes(512); + + // Check empty block (end of archive) + let allZero = true; + for (let i = 0; i < 512; i++) { + if (header[i] !== 0) { + allZero = false; + break; + } + } + if (allZero) continue; + + const nameRaw = header.toString('utf8', 0, 100).replace(/\0.*$/, ''); + const sizeOctal = header.toString('ascii', 124, 136).replace(/\0.*$/, '').trim(); + const typeFlag = String.fromCharCode(header[156] || 48); + const prefixRaw = header.toString('utf8', 345, 500).replace(/\0.*$/, ''); + + let fullPath = prefixRaw ? `${prefixRaw}/${nameRaw}` : nameRaw; + if (nextLongName) { + fullPath = nextLongName; + nextLongName = null; + } + if (nextPaxPath) { + fullPath = nextPaxPath; + nextPaxPath = null; + } + + currentEntrySize = parseInt(sizeOctal, 8) || 0; + currentEntryPadded = Math.ceil(currentEntrySize / 512) * 512; + currentEntryType = typeFlag; + + if (typeFlag === 'L' || typeFlag === 'x') { + state = 'DATA'; + continue; + } + + if (typeFlag === '0' || typeFlag === '\0') { + scannedFiles++; + if (scannedFiles % 10000 === 0) { + console.log(`... escaneados ${scannedFiles} archivos en tar.gz | extraídas: ${extractedCount}/${neededRelPaths.size}`); + } + + const uploadsIdx = fullPath.indexOf('wp-content/uploads/'); + if (uploadsIdx !== -1 && currentEntrySize > 0) { + const relInUploads = fullPath.slice(uploadsIdx + 'wp-content/uploads/'.length); + const lowerRel = relInUploads.toLowerCase(); + const lowerBase = path.posix.basename(relInUploads).toLowerCase(); + + let matched = neededRelPaths.get(lowerRel) || neededBasenames.get(lowerBase) || null; + if (matched) { + const outFile = path.join(OUT_DIR, safeFileKey(matched) + '.jpg'); + if (!fs.existsSync(outFile)) { + currentMatchedRel = matched; + state = 'DATA'; + continue; + } + } + } + } + + state = 'SKIP'; + } else if (state === 'SKIP') { + if (totalBuffered < currentEntryPadded) break; + if (currentEntryPadded > 0) skipBytes(currentEntryPadded); + state = 'HEADER'; + } else if (state === 'DATA') { + if (totalBuffered < currentEntryPadded) break; + const dataBlock = consumeBytes(currentEntryPadded); + const fileData = dataBlock.subarray(0, currentEntrySize); + + if (currentEntryType === 'L') { + nextLongName = fileData.toString('utf8').replace(/\0.*$/, ''); + } else if (currentEntryType === 'x') { + const paxStr = fileData.toString('utf8'); + const m = paxStr.match(/\d+ path=([^\n]+)\n/); + if (m) nextPaxPath = m[1]; + } else if (currentMatchedRel) { + const targetRel = currentMatchedRel; + currentMatchedRel = null; + const outFile = path.join(OUT_DIR, safeFileKey(targetRel) + '.jpg'); + extractedCount++; + + const p = sharp(fileData) + .resize(800, 500, { fit: 'inside', withoutEnlargement: true }) + .jpeg({ quality: 80 }) + .toFile(outFile) + .catch(() => { + // Fallback: write raw buffer if sharp fails on an unusual image + try { fs.writeFileSync(outFile, fileData); } catch (_) {} + }); + pendingSharpPromises.push(p); + + if (pendingSharpPromises.length >= 50) { + await Promise.all(pendingSharpPromises.splice(0, pendingSharpPromises.length)); + } + } + + state = 'HEADER'; + } + } + } + + await Promise.all(pendingSharpPromises); + console.log(`\n✅ Extracción de imágenes de WordPress finalizada: ${extractedCount} imágenes listas en ${OUT_DIR}`); +} + +run().catch((e) => { + console.error('Error:', e); + process.exit(1); +}); + diff --git a/scripts/import_wp_full_to_appwrite.js b/scripts/import_wp_full_to_appwrite.js new file mode 100644 index 0000000..38c266b --- /dev/null +++ b/scripts/import_wp_full_to_appwrite.js @@ -0,0 +1,226 @@ +const fs = require('fs'); +const path = require('path'); +const { Client, Databases, Storage, ID, Query } = require('node-appwrite'); +const { InputFile } = require('node-appwrite/file'); +require('dotenv').config({ path: '.env.local' }); +const { applyTablesDbShim } = require('../src/lib/appwrite-tables-shim'); + +const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; +const API_KEY = process.env.APPWRITE_API_KEY; +const DB_ID = process.env.APPWRITE_DATABASE_ID || 'almeira'; +const BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID || 'media_bucket'; +const NOTICIA_TYPE_ID = '69c2b91400362d5d52b6'; + +const WP_DATA_PATH = path.join(__dirname, 'wp_extracted_data.json'); +const IMG_DIR = 'C:/tmp/wp_images'; + +const client = new Client().setEndpoint(ENDPOINT).setProject(PROJECT_ID).setKey(API_KEY); +applyTablesDbShim(client); +const databases = new Databases(client); +const storage = new Storage(client); + +function safeFileKey(relPath) { + return relPath.replace(/[^a-zA-Z0-9._-]/g, '_'); +} + +function stripHtml(html) { + if (!html) return ''; + return html + .replace(/<[^>]*>?/gm, ' ') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/"/gi, '"') + .replace(/“|”/gi, '"') + .replace(/‘|’/gi, "'") + .replace(/–|—/gi, '-') + .replace(/\s+/g, ' ') + .trim(); +} + +function formatWpContent(raw) { + if (!raw) return '

'; + let content = raw.trim(); + if (!content.includes(' p.trim()) + .filter(Boolean) + .map((p) => `

${p.replace(/\r?\n/g, '
')}

`) + .join('\n'); + } + return content.substring(0, 95000); +} + +async function runPool(items, concurrency, fn) { + let idx = 0; + const workers = Array.from({ length: concurrency }, async () => { + while (true) { + const current = idx++; + if (current >= items.length) break; + await fn(items[current], current); + } + }); + await Promise.all(workers); +} + +async function run() { + const wpData = JSON.parse(fs.readFileSync(WP_DATA_PATH, 'utf8')); + + console.log('==================================================================='); + console.log('🚀 IMPORTACIÓN HISTÓRICA DE WORDPRESS A APPWRITE CLOUD'); + console.log(`Categorías: ${wpData.categories.length} | Posts históricos: ${wpData.posts.length}`); + console.log('===================================================================\n'); + + // 1. Cargar categorías existentes + console.log('1️⃣ Sincronizando categorías de WordPress...'); + const existingCatSlugs = new Map(); + let cursor = null; + while (true) { + const q = [Query.limit(100)]; + if (cursor) q.push(Query.cursorAfter(cursor)); + const res = await databases.listDocuments(DB_ID, 'categories', q); + for (const c of res.documents) { + existingCatSlugs.set(c.slug, c.$id); + } + if (res.documents.length < 100) break; + cursor = res.documents[res.documents.length - 1].$id; + } + + let catsCreated = 0; + await runPool(wpData.categories, 8, async (cat) => { + const slug = (cat.slug || '').substring(0, 95); + const nombre = stripHtml(cat.nombre || slug).substring(0, 95); + if (!slug || existingCatSlugs.has(slug)) return; + try { + const doc = await databases.createDocument(DB_ID, 'categories', ID.unique(), { + nombre, + slug, + }); + existingCatSlugs.set(slug, doc.$id); + catsCreated++; + } catch (_) {} + }); + console.log(`✓ Categorías listas (nuevas creadas: ${catsCreated}, total: ${existingCatSlugs.size})\n`); + + // 2. Cargar slugs de posts existentes para evitar duplicados si se re-ejecuta + console.log('2️⃣ Verificando posts existentes en Appwrite Cloud...'); + const existingPostSlugs = new Set(); + cursor = null; + while (true) { + const q = [Query.limit(100), Query.select(['$id', 'slug'])]; + if (cursor) q.push(Query.cursorAfter(cursor)); + const res = await databases.listDocuments(DB_ID, 'posts', q); + for (const p of res.documents) { + if (p.slug) existingPostSlugs.add(p.slug); + } + if (res.documents.length < 100) break; + cursor = res.documents[res.documents.length - 1].$id; + } + console.log(`✓ Slugs ya existentes en Appwrite: ${existingPostSlugs.size}\n`); + + // 3. Ordenar posts del más reciente al más antiguo (2026 -> 2020) + const sortedPosts = [...wpData.posts].sort((a, b) => new Date(b.date) - new Date(a.date)); + const toImport = sortedPosts.filter((p) => p.slug && !existingPostSlugs.has(p.slug.substring(0, 250))); + console.log(`3️⃣ Importando ${toImport.length} publicaciones históricas con sus imágenes y links originales...\n`); + + const mediaCache = new Map(); // imageRelPath -> Promise + let importedPosts = 0; + let uploadedImages = 0; + let failedPosts = 0; + + async function getOrUploadMedia(post) { + if (!post.imageRelPath) return null; + const cleanRel = post.imageRelPath.replace(/^\/+/, ''); + if (mediaCache.has(cleanRel)) { + return mediaCache.get(cleanRel); + } + + const promise = (async () => { + const localImgPath = path.join(IMG_DIR, safeFileKey(cleanRel) + '.jpg'); + if (!fs.existsSync(localImgPath)) return null; + try { + const file = await storage.createFile( + BUCKET_ID, + ID.unique(), + InputFile.fromPath(localImgPath, path.posix.basename(cleanRel).replace(/[^a-zA-Z0-9._-]/g, '_') || 'wp_image.jpg') + ); + const tituloMedia = stripHtml(post.imageTitle || post.title || 'Imagen WordPress').substring(0, 250) || 'Imagen'; + const altMedia = stripHtml(post.imageAlt || post.title || '').substring(0, 490); + const mediaDoc = await databases.createDocument(DB_ID, 'media', ID.unique(), { + file_id: file.$id, + bucket_id: BUCKET_ID, + titulo: tituloMedia, + alt_text: altMedia, + tipo: 'imagen', + }); + uploadedImages++; + return mediaDoc.$id; + } catch (err) { + return null; + } + })(); + + mediaCache.set(cleanRel, promise); + return promise; + } + + await runPool(toImport, 6, async (post, i) => { + try { + const mediaDocId = await getOrUploadMedia(post); + const cleanTitle = stripHtml(post.title || 'Sin título').substring(0, 250) || 'Sin título'; + const plainExcerpt = stripHtml(post.excerpt || post.content || cleanTitle); + const resumen = plainExcerpt.substring(0, 480) || cleanTitle; + const seoDesc = plainExcerpt.substring(0, 280) || cleanTitle; + const slug = post.slug.substring(0, 250); + + let isoDate; + try { + isoDate = new Date(post.date.replace(' ', 'T') + 'Z').toISOString(); + } catch (_) { + isoDate = new Date().toISOString(); + } + + const data = { + titulo: cleanTitle, + slug, + tipo: 'noticia', + tipo_publicacion_id: NOTICIA_TYPE_ID, + estado: 'publicado', + fecha_publicacion: isoDate, + contenido: formatWpContent(post.content), + resumen_redes: resumen, + seo_titulo: cleanTitle.substring(0, 115), + seo_descripcion: seoDesc, + ...(mediaDocId ? { imagen_destacada_id: mediaDocId } : {}), + }; + + await databases.createDocument(DB_ID, 'posts', ID.unique(), data); + importedPosts++; + + if (importedPosts % 50 === 0 || importedPosts === toImport.length) { + console.log( + `[Progreso WP] ${importedPosts}/${toImport.length} posts publicados | Imágenes subidas: ${uploadedImages} | Última fecha: ${post.date.slice(0, 10)} (${slug.slice(0, 45)})` + ); + } + } catch (err) { + failedPosts++; + if (failedPosts <= 20) { + console.error(`! Error en post WP ${post.id} (${post.slug}):`, err.message); + } + } + }); + + console.log('\n==================================================================='); + console.log(`🎉 ¡MIGRACIÓN HISTÓRICA DE WORDPRESS COMPLETADA!`); + console.log(` - Publicaciones importadas: ${importedPosts}`); + console.log(` - Imágenes destacadas subidas: ${uploadedImages}`); + console.log(` - Fallos: ${failedPosts}`); + console.log('==================================================================='); +} + +run().catch((e) => { + console.error('Fatal error:', e); + process.exit(1); +}); + diff --git a/scripts/publish_cauca_sept17_to_date.js b/scripts/publish_cauca_sept17_to_date.js new file mode 100644 index 0000000..949c07d --- /dev/null +++ b/scripts/publish_cauca_sept17_to_date.js @@ -0,0 +1,338 @@ +const axios = require('axios'); +const cheerio = require('cheerio'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { Client, Databases, Storage, ID, Query } = require('node-appwrite'); +require('dotenv').config({ path: '.env.local' }); + +let InputFile; +try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; } + +const { applyTablesDbShim } = require('../src/lib/appwrite-tables-shim'); +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); +applyTablesDbShim(client); + +const databases = new Databases(client); +const storage = new Storage(client); +const DB_ID = process.env.APPWRITE_DATABASE_ID; +const BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID; + +function slugify(text) { + return text + .toString() + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9 -]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim(); +} + +function cleanContentHtml(html) { + if (!html) return ''; + const $ = cheerio.load(html, null, false); + + $('style, script, link, iframe, object, embed, video, audio, source, noscript').remove(); + + const allowedTags = ['p', 'b', 'i', 'strong', 'em', 'ul', 'ol', 'li', 'br', 'a']; + + function processNode(node) { + $(node).children().each((i, child) => { + processNode(child); + }); + + const tagName = node.tagName ? node.tagName.toLowerCase() : null; + if (tagName) { + const attribs = Object.keys(node.attribs || {}); + attribs.forEach(attr => { + if (tagName !== 'a' || attr !== 'href') { + $(node).removeAttr(attr); + } + }); + + if (!allowedTags.includes(tagName)) { + $(node).replaceWith($(node).html() || ''); + } + } + } + + $.root().children().each((i, child) => { + processNode(child); + }); + + return $.html().trim(); +} + +async function getCollectionIds() { + const list = await databases.listCollections(DB_ID); + const map = {}; + list.collections.forEach(c => { map[c.name.toLowerCase()] = c.$id; }); + return map; +} + +const sleep = ms => new Promise(res => setTimeout(res, ms)); + +async function run() { + console.log("==============================================================================="); + console.log("Iniciando publicación de noticias de la Gobernación del Cauca (17 Sep a la fecha)"); + console.log("==============================================================================="); + + try { + const collections = await getCollectionIds(); + const postsCollectionId = collections['posts'] || 'posts'; + const mediaCollectionId = collections['media'] || 'media'; + const pubTypesCollectionId = collections['publication types'] || collections['publication_types'] || 'publication_types'; + + // 1. Obtener ID del tipo de publicación NOTICIA + const pubTypesRes = await databases.listDocuments(DB_ID, pubTypesCollectionId); + const typeMap = {}; + pubTypesRes.documents.forEach(d => { + typeMap[d.codigo] = d.$id; + }); + const typeId = typeMap['NOTICIA']; + if (!typeId) { + throw new Error("No se encontró el tipo de publicación NOTICIA en Appwrite."); + } + console.log(`Tipo de publicación NOTICIA ID: ${typeId}`); + + // 2. Obtener lista de noticias desde la API SharePoint de la Gobernación + console.log("Consultando API SharePoint de cauca.gov.co..."); + const listUrl = "https://cauca.gov.co/Prensa/SaladePrensa/_api/web/lists(guid'e1840969-e886-4c1e-8cde-7498ab33f10a')/items?$select=Id,Title,ArticleStartDate,Created,Modified,File/ServerRelativeUrl&$expand=File&$orderby=Created desc&$top=100"; + const spRes = await axios.get(listUrl, { + headers: { 'Accept': 'application/json;odata=verbose', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, + timeout: 15000 + }); + + const rawItems = spRes.data.d?.results || []; + console.log(`Total artículos obtenidos de SharePoint: ${rawItems.length}`); + + // Filtrar desde el 21 de agosto de 2026 (00:00:00) hasta la fecha + const targetStartDate = new Date('2026-08-21T00:00:00'); + const filteredItems = rawItems.filter(it => { + const dateStr = it.ArticleStartDate || it.Created; + const d = new Date(dateStr); + return d >= targetStartDate; + }); + + console.log(`Artículos identificados desde el 17 de septiembre: ${filteredItems.length}`); + + // Ordenar cronológicamente (los más antiguos primero para que los más nuevos queden en la parte superior del portal) + filteredItems.sort((a, b) => { + const da = new Date(a.ArticleStartDate || a.Created).getTime(); + const db = new Date(b.ArticleStartDate || b.Created).getTime(); + return da - db; + }); + + // 3. Consultar slugs existentes en Appwrite para evitar duplicados + console.log("Verificando publicaciones existentes en Appwrite..."); + const existingSlugs = new Set(); + let cursor = null; + while (true) { + const queries = [Query.limit(100), Query.select(['slug'])]; + if (cursor) queries.push(Query.cursorAfter(cursor)); + const existingRes = await databases.listDocuments(DB_ID, postsCollectionId, queries); + existingRes.documents.forEach(d => { + if (d.slug) existingSlugs.add(d.slug); + }); + if (existingRes.documents.length < 100) break; + cursor = existingRes.documents[existingRes.documents.length - 1].$id; + } + console.log(`Slugs existentes en posts: ${existingSlugs.size}`); + + let publishedCount = 0; + let skippedCount = 0; + let errorsCount = 0; + + for (let i = 0; i < filteredItems.length; i++) { + const item = filteredItems[i]; + const pubDate = new Date(item.ArticleStartDate || item.Created); + const rawTitle = (item.Title || '').trim(); + const slug = `gobernacion-web-${slugify(rawTitle)}`.substring(0, 95); + + console.log(`\n-------------------------------------------------------------------------------`); + console.log(`[${i + 1}/${filteredItems.length}] Noticia ID: ${item.Id} | Fecha: ${pubDate.toISOString().split('T')[0]}`); + console.log(`Título: "${rawTitle}"`); + console.log(`Slug: ${slug}`); + + if (existingSlugs.has(slug)) { + console.log(`-> Ya existe en Appwrite con slug: ${slug}. Se omite duplicado.`); + skippedCount++; + continue; + } + + const relativeUrl = item.File?.ServerRelativeUrl; + if (!relativeUrl) { + console.error("-> Error: No se encontró URL relativa para este artículo."); + errorsCount++; + continue; + } + + const pageUrl = "https://cauca.gov.co" + encodeURI(decodeURI(relativeUrl)); + console.log(`-> Obteniendo contenido de página: ${pageUrl}`); + + try { + const pageRes = await axios.get(pageUrl, { + headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' }, + timeout: 15000 + }); + + const $ = cheerio.load(pageRes.data); + + // A. Extraer imagen destacada + let imageUrl = null; + $('.ms-rtestate-field img').each((idx, el) => { + const src = $(el).attr('src'); + if (src && !src.includes('logo') && !src.includes('icon')) { + imageUrl = src.startsWith('http') ? src : 'https://cauca.gov.co' + src; + return false; + } + }); + + if (!imageUrl) { + $('img').each((idx, el) => { + const src = $(el).attr('src'); + if (src && src.includes('/Prensa/PublishingImages/')) { + imageUrl = src.startsWith('http') ? src : 'https://cauca.gov.co' + src; + return false; + } + }); + } + + if (!imageUrl) { + $('img').each((idx, el) => { + const src = $(el).attr('src'); + if (src && !src.includes('logo') && !src.includes('icon') && !src.includes('menu') && src.length > 10) { + imageUrl = src.startsWith('http') ? src : 'https://cauca.gov.co' + src; + return false; + } + }); + } + + // B. Extraer contenido HTML + let contentHtml = null; + let maxLen = 0; + $('.ms-rtestate-field').each((idx, el) => { + const textLen = $(el).text().trim().length; + if (textLen > maxLen) { + maxLen = textLen; + contentHtml = $(el).html(); + } + }); + + if (!contentHtml) { + contentHtml = $('.lg\\:container.mx-auto.flex.items-center.justify-center.my-\\[80px\\]').html(); + } + + if (!contentHtml || contentHtml.trim().length === 0) { + console.error("-> Error: Contenido de la noticia vacío."); + errorsCount++; + continue; + } + + const cleanedContent = cleanContentHtml(contentHtml); + const plainText = cheerio.load(cleanedContent).text().replace(/\s+/g, ' ').trim(); + let summary = plainText.substring(0, 150); + if (plainText.length > 150) summary += '...'; + + // C. Descargar y subir imagen destacada a Appwrite Storage y Media + let mediaId = null; + if (imageUrl) { + try { + console.log(`-> Descargando imagen: ${imageUrl}`); + const imgRes = await axios.get(encodeURI(decodeURI(imageUrl)), { + responseType: 'arraybuffer', + timeout: 15000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' + } + }); + + const buffer = Buffer.from(imgRes.data, 'binary'); + const isHtml = buffer.toString('utf-8', 0, 50).trim().toLowerCase().startsWith(' 500) { + const urlParts = imageUrl.split('/'); + let filename = urlParts[urlParts.length - 1] || 'imagen.jpg'; + filename = decodeURIComponent(filename).replace(/[^a-zA-Z0-9._-]/g, '_'); + if (!filename.includes('.')) filename += '.jpg'; + + const tempFilePath = path.join(os.tmpdir(), `gob_${ID.unique()}_${filename}`); + fs.writeFileSync(tempFilePath, buffer); + + let fileParam; + if (InputFile && InputFile.fromPath) { + fileParam = InputFile.fromPath(tempFilePath, filename); + } else { + fileParam = fs.createReadStream(tempFilePath); + } + + console.log(`-> Subiendo imagen al bucket (${BUCKET_ID})...`); + const fileUploaded = await storage.createFile(BUCKET_ID, ID.unique(), fileParam); + + console.log(`-> Registrando en colección media...`); + const mediaDoc = await databases.createDocument(DB_ID, mediaCollectionId, ID.unique(), { + file_id: fileUploaded.$id, + bucket_id: BUCKET_ID, + titulo: rawTitle.substring(0, 245), + tipo: 'imagen' + }); + mediaId = mediaDoc.$id; + + try { fs.unlinkSync(tempFilePath); } catch (e) {} + } else { + console.log("-> Aviso: La imagen obtenida no es un binario válido."); + } + } catch (imgErr) { + console.error(`-> Error al procesar imagen: ${imgErr.message}`); + } + } + + // D. Crear publicación en Appwrite + const postDoc = { + titulo: rawTitle.substring(0, 245), + slug: slug, + tipo: 'noticia', + tipo_publicacion_id: typeId, + estado: 'publicado', + fecha_publicacion: pubDate.toISOString(), + contenido: cleanedContent, + resumen_redes: summary, + seo_descripcion: summary.substring(0, 155), + seo_titulo: rawTitle.substring(0, 58), + imagen_destacada_id: mediaId + }; + + console.log(`-> Guardando documento en Appwrite posts...`); + const createdPost = await databases.createDocument(DB_ID, postsCollectionId, ID.unique(), postDoc); + console.log(`✓ [ÉXITO] Publicada correctamente! Post ID: ${createdPost.$id}`); + existingSlugs.add(slug); + publishedCount++; + + // Pausa prudente entre peticiones + await sleep(1200); + + } catch (err) { + console.error(`-> Error publicando noticia: ${err.message}`); + errorsCount++; + } + } + + console.log("\n==============================================================================="); + console.log(`RESUMEN FINAL:`); + console.log(`Total identificadas: ${filteredItems.length}`); + console.log(`Publicadas exitosamente: ${publishedCount}`); + console.log(`Omitidas por ya existir: ${skippedCount}`); + console.log(`Errores: ${errorsCount}`); + console.log("==============================================================================="); + + } catch (e) { + console.error("Error fatal en proceso de publicación:", e.message); + } +} + +run(); diff --git a/scripts/rebuild_almeira_cloud.js b/scripts/rebuild_almeira_cloud.js new file mode 100644 index 0000000..563a983 --- /dev/null +++ b/scripts/rebuild_almeira_cloud.js @@ -0,0 +1,430 @@ +const sdk = require('node-appwrite'); +const axios = require('axios'); +require('dotenv').config({ path: '.env.local' }); + +const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID; +const API_KEY = process.env.APPWRITE_API_KEY; +const DB_ID = process.env.APPWRITE_DATABASE_ID || 'almeira'; +const BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID || 'media_bucket'; + +if (!PROJECT_ID || !API_KEY) { + console.error('❌ Faltan NEXT_PUBLIC_APPWRITE_PROJECT_ID o APPWRITE_API_KEY en .env.local'); + process.exit(1); +} + +const client = new sdk.Client() + .setEndpoint(ENDPOINT) + .setProject(PROJECT_ID) + .setKey(API_KEY); + +const databases = new sdk.Databases(client); +const storage = new sdk.Storage(client); + +const api = axios.create({ + baseURL: ENDPOINT, + headers: { + 'X-Appwrite-Project': PROJECT_ID, + 'X-Appwrite-Key': API_KEY, + 'Content-Type': 'application/json', + }, +}); + +const delay = (ms) => new Promise((res) => setTimeout(res, ms)); + +const PUBLIC_READ_WRITE = [ + sdk.Permission.read(sdk.Role.any()), + sdk.Permission.create(sdk.Role.any()), + sdk.Permission.update(sdk.Role.any()), + sdk.Permission.delete(sdk.Role.any()), +]; + +async function ensureCollection(colId, name, permissions = PUBLIC_READ_WRITE) { + try { + await api.get(`/tablesdb/${DB_ID}/tables/${colId}`); + console.log(` ✓ Tabla/Colección '${colId}' ya existe.`); + } catch (e) { + if (e.response?.status === 404) { + try { + await api.post(`/tablesdb/${DB_ID}/tables`, { + tableId: colId, + name, + permissions, + rowSecurity: false, + enabled: true, + }); + console.log(` + Tabla/Colección '${colId}' (${name}) creada.`); + await delay(300); + } catch (err) { + console.error(` ! Error creando '${colId}':`, err.response?.data?.message || err.message); + } + } else { + console.error(` ! Error verificando '${colId}':`, e.response?.data?.message || e.message); + } + } +} + +async function ensureAttr(colId, type, key, opts = {}) { + try { + const payload = { + key, + required: opts.required || false, + default: opts.default !== undefined ? opts.default : null, + array: opts.array || false, + }; + + let routeType = type; + if (type === 'string') { + payload.size = opts.size || 255; + } else if (type === 'integer' || type === 'float') { + if (opts.min !== undefined) payload.min = opts.min; + if (opts.max !== undefined) payload.max = opts.max; + } + + await api.post(`/tablesdb/${DB_ID}/tables/${colId}/columns/${routeType}`, payload); + console.log(` + Columna ${colId}.${key} (${type})`); + await delay(200); + } catch (e) { + if (e.response?.status !== 409) { + console.warn(` ! Aviso en ${colId}.${key}:`, e.response?.data?.message || e.message); + } + } +} + +async function ensureDoc(colId, docId, data) { + try { + await api.post(`/tablesdb/${DB_ID}/tables/${colId}/rows`, { + rowId: docId, + data, + }); + console.log(` + Registro sembrado: ${colId}/${docId}`); + } catch (e) { + if (e.response?.status !== 409) { + console.warn(` ! Aviso sembrando ${colId}/${docId}:`, e.response?.data?.message || e.message); + } + } +} + +async function run() { + console.log('==================================================================='); + console.log('🚀 RECONSTRUCCIÓN INTEGRAL DE ALMEIRA PRODUCCIONES EN APPWRITE'); + console.log(`Endpoint: ${ENDPOINT} | Project: ${PROJECT_ID}`); + console.log('===================================================================\n'); + + // 1. Crear Base de Datos + try { + await databases.get(DB_ID); + console.log(`✓ Base de datos '${DB_ID}' ya existe.`); + } catch (e) { + if (e.code === 404) { + await databases.create(DB_ID, 'Almeira Producciones'); + console.log(`+ Base de datos '${DB_ID}' creada exitosamente.`); + } else { + throw e; + } + } + + // 2. Crear Bucket de Medios + try { + await storage.getBucket(BUCKET_ID); + console.log(`✓ Bucket '${BUCKET_ID}' ya existe.`); + } catch (e) { + if (e.code === 404) { + await storage.createBucket( + BUCKET_ID, + 'Biblioteca de Medios', + PUBLIC_READ_WRITE, + false, + true, + 50000000, + ['jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'webm', 'mov', 'jfif'] + ); + console.log(`+ Bucket '${BUCKET_ID}' creado exitosamente.`); + } else { + console.warn(`! Aviso en Bucket '${BUCKET_ID}':`, e.message); + } + } + + // 3. Crear todas las colecciones y atributos + console.log('\n--- Configurando Colecciones y Atributos ---'); + + // posts + await ensureCollection('posts', 'Publicaciones'); + await ensureAttr('posts', 'string', 'titulo', { size: 255, required: true }); + await ensureAttr('posts', 'string', 'contenido', { size: 50000, required: true }); + await ensureAttr('posts', 'string', 'resumen_redes', { size: 1000, required: false }); + await ensureAttr('posts', 'string', 'tipo', { size: 50, required: false, default: 'noticia' }); + await ensureAttr('posts', 'string', 'tipo_publicacion_id', { size: 255, required: false }); + await ensureAttr('posts', 'string', 'estado', { size: 50, required: true }); + await ensureAttr('posts', 'string', 'imagen_destacada_id', { size: 255, required: false }); + await ensureAttr('posts', 'string', 'video_url', { size: 2048, required: false }); + await ensureAttr('posts', 'string', 'cliente_id', { size: 255, required: false }); + await ensureAttr('posts', 'datetime', 'fecha_publicacion', { required: true }); + await ensureAttr('posts', 'datetime', 'fecha_finalizacion', { required: false }); + await ensureAttr('posts', 'string', 'url_destino', { size: 2048, required: false }); + await ensureAttr('posts', 'string', 'slug', { size: 255, required: true }); + await ensureAttr('posts', 'string', 'seo_titulo', { size: 120, required: false }); + await ensureAttr('posts', 'string', 'seo_descripcion', { size: 300, required: false }); + await ensureAttr('posts', 'string', 'seo_keywords', { size: 500, required: false }); + await ensureAttr('posts', 'string', 'og_image_url', { size: 2048, required: false }); + + // categories & posts_categories + await ensureCollection('categories', 'Categorías'); + await ensureAttr('categories', 'string', 'nombre', { size: 100, required: true }); + await ensureAttr('categories', 'string', 'slug', { size: 100, required: true }); + + await ensureCollection('posts_categories', 'Publicaciones Categorías'); + await ensureAttr('posts_categories', 'string', 'post_id', { size: 255, required: true }); + await ensureAttr('posts_categories', 'string', 'categoria_id', { size: 255, required: true }); + + // publication_types + await ensureCollection('publication_types', 'Tipos de Publicación'); + await ensureAttr('publication_types', 'string', 'codigo', { size: 50, required: true }); + await ensureAttr('publication_types', 'string', 'descripcion', { size: 255, required: true }); + + // client_types + await ensureCollection('client_types', 'Tipos de Cliente'); + await ensureAttr('client_types', 'string', 'codigo', { size: 50, required: true }); + await ensureAttr('client_types', 'string', 'descripcion', { size: 100, required: true }); + + // clients + await ensureCollection('clients', 'Clientes'); + await ensureAttr('clients', 'string', 'nombre_entidad', { size: 255, required: true }); + await ensureAttr('clients', 'string', 'contacto_nombre', { size: 255, required: true }); + await ensureAttr('clients', 'string', 'numero_whatsapp', { size: 50, required: true }); + await ensureAttr('clients', 'boolean', 'estado', { required: true }); + await ensureAttr('clients', 'string', 'slug', { size: 255, required: true }); + await ensureAttr('clients', 'string', 'seo_descripcion', { size: 255, required: false }); + await ensureAttr('clients', 'string', 'nit', { size: 50, required: false }); + await ensureAttr('clients', 'string', 'tipo_cliente_id', { size: 255, required: false }); + await ensureAttr('clients', 'string', 'direccion', { size: 500, required: false }); + await ensureAttr('clients', 'string', 'ciudad_id', { size: 255, required: false }); + await ensureAttr('clients', 'string', 'correo_electronico', { size: 255, required: false }); + await ensureAttr('clients', 'string', 'sitio_web', { size: 2048, required: false }); + + // countries, departments, cities + await ensureCollection('countries', 'Países'); + await ensureAttr('countries', 'string', 'codigo', { size: 10, required: true }); + await ensureAttr('countries', 'string', 'descripcion', { size: 100, required: true }); + + await ensureCollection('departments', 'Departamentos'); + await ensureAttr('departments', 'string', 'codigo', { size: 10, required: true }); + await ensureAttr('departments', 'string', 'descripcion', { size: 100, required: true }); + await ensureAttr('departments', 'string', 'pais_id', { size: 255, required: true }); + + await ensureCollection('cities', 'Ciudades'); + await ensureAttr('cities', 'string', 'codigo', { size: 10, required: true }); + await ensureAttr('cities', 'string', 'descripcion', { size: 100, required: true }); + await ensureAttr('cities', 'string', 'departamento_id', { size: 255, required: true }); + + // payment_methods, contracts, contract_details + await ensureCollection('payment_methods', 'Formas de Pago'); + await ensureAttr('payment_methods', 'string', 'codigo', { size: 50, required: true }); + await ensureAttr('payment_methods', 'string', 'descripcion', { size: 255, required: true }); + await ensureAttr('payment_methods', 'integer', 'numero_dias', { required: true }); + + await ensureCollection('contracts', 'Contratos'); + await ensureAttr('contracts', 'string', 'numero_contrato', { size: 100, required: true }); + await ensureAttr('contracts', 'string', 'eslogan', { size: 255, required: false }); + await ensureAttr('contracts', 'string', 'estado', { size: 50, required: true }); + await ensureAttr('contracts', 'string', 'cliente_id', { size: 255, required: true }); + await ensureAttr('contracts', 'string', 'descripcion', { size: 2000, required: true }); + await ensureAttr('contracts', 'datetime', 'fecha_inicial', { required: true }); + await ensureAttr('contracts', 'datetime', 'fecha_final', { required: true }); + await ensureAttr('contracts', 'float', 'valor_contrato', { required: true }); + await ensureAttr('contracts', 'string', 'forma_pago_id', { size: 255, required: true }); + + await ensureCollection('contract_details', 'Detalles de Contrato'); + await ensureAttr('contract_details', 'string', 'contrato_id', { size: 255, required: true }); + await ensureAttr('contract_details', 'string', 'tipo_publicacion_id', { size: 255, required: true }); + await ensureAttr('contract_details', 'string', 'cantidad_periodo', { size: 50, required: true }); + await ensureAttr('contract_details', 'integer', 'cantidad_total', { required: true }); + + // media + await ensureCollection('media', 'Medios y SEO'); + await ensureAttr('media', 'string', 'file_id', { size: 255, required: true }); + await ensureAttr('media', 'string', 'bucket_id', { size: 255, required: true }); + await ensureAttr('media', 'string', 'titulo', { size: 255, required: true }); + await ensureAttr('media', 'string', 'alt_text', { size: 500, required: false }); + await ensureAttr('media', 'string', 'descripcion_seo', { size: 2000, required: false }); + await ensureAttr('media', 'string', 'tipo', { size: 50, required: true }); + + // settings & entidad + await ensureCollection('settings', 'Configuración del Medio'); + await ensureAttr('settings', 'string', 'radio_stream_url', { size: 2048, required: false }); + await ensureAttr('settings', 'string', 'tv_stream_url', { size: 2048, required: false }); + await ensureAttr('settings', 'string', 'whatsapp_recepcion', { size: 50, required: false }); + await ensureAttr('settings', 'string', 'seo_site_name', { size: 255, required: false }); + await ensureAttr('settings', 'string', 'seo_default_image', { size: 2048, required: false }); + await ensureAttr('settings', 'integer', 'ticker_count', { required: false, default: 10 }); + await ensureAttr('settings', 'integer', 'rotation_seconds', { required: false, default: 5 }); + + await ensureCollection('entidad', 'Configuración de Entidad'); + await ensureAttr('entidad', 'string', 'logotipo', { size: 500, required: false }); + await ensureAttr('entidad', 'string', 'razon_social', { size: 255, required: true }); + await ensureAttr('entidad', 'string', 'direccion', { size: 255, required: false }); + await ensureAttr('entidad', 'string', 'numero_contacto', { size: 50, required: false }); + await ensureAttr('entidad', 'string', 'numero_whatsapp', { size: 50, required: false }); + await ensureAttr('entidad', 'string', 'ciudad', { size: 100, required: false }); + await ensureAttr('entidad', 'string', 'sitio_web', { size: 500, required: false }); + await ensureAttr('entidad', 'string', 'email', { size: 255, required: false }); + await ensureAttr('entidad', 'string', 'facebook_url', { size: 500, required: false }); + await ensureAttr('entidad', 'string', 'youtube_url', { size: 500, required: false }); + await ensureAttr('entidad', 'string', 'instagram_url', { size: 500, required: false }); + await ensureAttr('entidad', 'string', 'x_url', { size: 500, required: false }); + await ensureAttr('entidad', 'string', 'telegram_url', { size: 500, required: false }); + await ensureAttr('entidad', 'boolean', 'mostrar_texto_logo', { required: false, default: true }); + + // metrics, programming_matrices, whatsapp_messages, whatsapp_chats, live_chat, stream_state + await ensureCollection('metrics', 'Métricas'); + await ensureAttr('metrics', 'string', 'publicacion_id', { size: 255, required: false }); + await ensureAttr('metrics', 'string', 'post_id', { size: 255, required: false }); + await ensureAttr('metrics', 'string', 'plataforma', { size: 50, required: false }); + await ensureAttr('metrics', 'string', 'tipo_metrica', { size: 50, required: false }); + await ensureAttr('metrics', 'integer', 'vistas', { required: false }); + await ensureAttr('metrics', 'integer', 'interacciones', { required: false }); + await ensureAttr('metrics', 'datetime', 'timestamp', { required: false }); + + await ensureCollection('programming_matrices', 'Matrices de Programación'); + await ensureAttr('programming_matrices', 'string', 'media_type', { size: 50, required: true }); + await ensureAttr('programming_matrices', 'string', 'matrix_data', { size: 100000, required: true }); + + await ensureCollection('whatsapp_messages', 'Mensajes WhatsApp'); + await ensureAttr('whatsapp_messages', 'string', 'sender_name', { size: 255, required: true }); + await ensureAttr('whatsapp_messages', 'string', 'phone_number', { size: 50, required: true }); + await ensureAttr('whatsapp_messages', 'string', 'content', { size: 5000, required: true }); + await ensureAttr('whatsapp_messages', 'datetime', 'timestamp', { required: true }); + await ensureAttr('whatsapp_messages', 'string', 'status', { size: 50, required: false, default: 'pending' }); + await ensureAttr('whatsapp_messages', 'string', 'platform', { size: 50, required: false, default: 'whatsapp' }); + await ensureAttr('whatsapp_messages', 'string', 'avatar', { size: 1000, required: false }); + + await ensureCollection('whatsapp_chats', 'Chats Autorizados WhatsApp'); + await ensureAttr('whatsapp_chats', 'string', 'name', { size: 255, required: true }); + await ensureAttr('whatsapp_chats', 'string', 'jid', { size: 255, required: false }); + await ensureAttr('whatsapp_chats', 'boolean', 'enabled', { required: false, default: true }); + await ensureAttr('whatsapp_chats', 'string', 'notes', { size: 1000, required: false }); + + await ensureCollection('live_chat', 'Live Chat'); + await ensureAttr('live_chat', 'string', 'user_id', { size: 50, required: true }); + await ensureAttr('live_chat', 'string', 'user_name', { size: 100, required: true }); + await ensureAttr('live_chat', 'string', 'message', { size: 500, required: true }); + await ensureAttr('live_chat', 'datetime', 'timestamp', { required: true }); + + await ensureCollection('stream_state', 'Stream State'); + await ensureAttr('stream_state', 'string', 'active_overlays', { size: 50000, required: false }); + + await ensureCollection('entity_config', 'Configuración de Entidad'); + for (const col of ['razon_social', 'logotipo', 'ciudad', 'direccion', 'numero_contacto', 'numero_whatsapp', 'email', 'sitio_web', 'tv_url', 'radio_url', 'facebook_url', 'instagram_url', 'x_url', 'youtube_url', 'telegram_url']) { + await ensureAttr('entity_config', 'string', col, { size: 500, required: false }); + } + await ensureAttr('entity_config', 'boolean', 'mostrar_texto_logo', { required: false, default: true }); + + await ensureCollection('verification_tokens', 'Tokens de Verificación'); + await ensureAttr('verification_tokens', 'string', 'user_id', { size: 255, required: true }); + await ensureAttr('verification_tokens', 'string', 'otp_code', { size: 50, required: true }); + await ensureAttr('verification_tokens', 'string', 'expires_at', { size: 100, required: true }); + await ensureAttr('verification_tokens', 'string', 'pending_data', { size: 5000, required: true }); + + console.log('\n⏳ Esperando 4 segundos para que Appwrite indexe todas las columnas...'); + await delay(4000); + + // 4. Sembrar Catálogos Base + console.log('\n--- Sembrando Catálogos Base de Almeira Producciones ---'); + const tiposPublicacion = [ + { id: '69c2b91400362d5d52b6', codigo: 'NOTICIA', descripcion: 'Noticia' }, + { id: 't_evento', codigo: 'EVENTO', descripcion: 'Evento' }, + { id: 't_clasif', codigo: 'CLASIFICADO', descripcion: 'Clasificado' }, + { id: 't_dbanner', codigo: 'DISPLAY_BANNER', descripcion: 'Display Banner' }, + { id: 't_dslider', codigo: 'DISPLAY_SLIDER', descripcion: 'Display Slider' }, + ]; + for (const t of tiposPublicacion) { + await ensureDoc('publication_types', t.id, { codigo: t.codigo, descripcion: t.descripcion }); + } + + await ensureDoc('client_types', 'ct_institucional', { codigo: 'INSTITUCIONAL', descripcion: 'Institucional' }); + await ensureDoc('client_types', 'ct_comercial', { codigo: 'COMERCIAL', descripcion: 'Comercial' }); + + await ensureDoc('countries', 'co_colombia', { codigo: 'CO', descripcion: 'Colombia' }); + await ensureDoc('departments', 'dep_cauca', { codigo: '19', descripcion: 'Cauca', pais_id: 'co_colombia' }); + await ensureDoc('cities', 'cit_popayan', { codigo: '19001', descripcion: 'Popayán', departamento_id: 'dep_cauca' }); + await ensureDoc('cities', 'cit_quilichao', { codigo: '19698', descripcion: 'Santander de Quilichao', departamento_id: 'dep_cauca' }); + + await ensureDoc('payment_methods', 'pm_contado', { codigo: 'CONTADO', descripcion: 'Contado', numero_dias: 0 }); + await ensureDoc('payment_methods', 'pm_credito30', { codigo: 'CREDITO30', descripcion: 'Crédito 30 Días', numero_dias: 30 }); + + await ensureDoc('clients', 'cli_gob_cauca', { + nombre_entidad: 'Gobernación del Cauca', + nit: '891580016-8', + slug: 'gobernacion-del-cauca', + tipo_cliente_id: 'ct_institucional', + contacto_nombre: 'Oficina Asesora de Comunicaciones', + numero_whatsapp: '573134848203', + ciudad_id: 'cit_popayan', + estado: true, + sitio_web: 'https://www.cauca.gov.co', + }); + + await ensureDoc('clients', 'cli_alcaldia_quilichao', { + nombre_entidad: 'Alcaldía de Santander de Quilichao', + slug: 'alcaldia-santander-de-quilichao', + tipo_cliente_id: 'ct_institucional', + contacto_nombre: 'Prensa Alcaldía', + numero_whatsapp: '573167455877', + ciudad_id: 'cit_quilichao', + estado: true, + }); + + await ensureDoc('entidad', 'default', { + razon_social: 'Almeira Producciones', + ciudad: 'Santander de Quilichao - Cauca', + numero_whatsapp: '573167455877', + sitio_web: 'https://almeiraproducciones.com', + mostrar_texto_logo: true, + }); + + await ensureDoc('entity_config', 'main_profile_singleton', { + razon_social: 'Almeira Producciones', + ciudad: 'Santander de Quilichao - Cauca', + numero_contacto: '3167455877', + numero_whatsapp: '573167455877', + sitio_web: 'https://almeiraproducciones.com', + mostrar_texto_logo: true, + }); + + await ensureDoc('settings', 'global_config', { + seo_site_name: 'Almeira Producciones', + ticker_count: 10, + rotation_seconds: 5, + }); + + await ensureDoc('whatsapp_chats', 'wc_almeira', { + name: 'Publicaciones Almeira', + jid: '120363419329355281@g.us', + enabled: true, + notes: 'Grupo oficial de publicaciones de Almeira', + }); + + await ensureDoc('whatsapp_chats', 'wc_gob_cauca', { + name: 'Medios y Prensa Gob Cauca', + jid: '573134848203-1408546044@g.us', + enabled: true, + notes: 'Boletines de prensa y noticias de la Gobernación del Cauca', + }); + + await ensureDoc('programming_matrices', 'matrix_radio', { + media_type: 'radio', + matrix_data: '{}', + }); + + await ensureDoc('programming_matrices', 'matrix_tv', { + media_type: 'tv', + matrix_data: '{}', + }); + + console.log('\n✅ ¡Estructura y catálogos base de Almeira Producciones listos en Appwrite!'); +} + +run().catch((e) => { + console.error('❌ Error fatal:', e); + process.exit(1); +}); diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index be37a97..cdf6560 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -168,8 +168,8 @@ export async function generateMetadata( ? decodeHtmlEntities(post.resumen_redes) : (post.contenido?.replace(/<[^>]*>?/gm, '').substring(0, 160) + '...'); - const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; - const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; + const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; let imagenUrl = post.og_image_url || null; if (post.imagen_destacada_id) { @@ -219,8 +219,8 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug ); } - const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; - const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; + const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; let imagenUrl = post.og_image_url || null; const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' || (typeof post.tipo_publicacion_id === 'object' && (post.tipo_publicacion_id.codigo === 'EVENTO' || post.tipo_publicacion_id.codigo === 'CLASIFICADO')); diff --git a/src/app/actions/appwrite.ts b/src/app/actions/appwrite.ts index faf9055..cda4e39 100644 --- a/src/app/actions/appwrite.ts +++ b/src/app/actions/appwrite.ts @@ -6,8 +6,8 @@ import { patchClientInstance } from '@/lib/appwrite.server'; import { revalidatePath } from 'next/cache'; import { InputFile } from 'node-appwrite/file'; -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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; const DATABASE_ID = databaseId; function getAdminClient() { diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index f6b7e38..bcefbee 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -4,8 +4,8 @@ import { Client, Account } from 'node-appwrite'; import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; -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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; const COOKIE_NAME = `a_session_${PROJECT_ID.toLowerCase()}`; // Password de emergencia (Configurado en Coolify) diff --git a/src/app/actions/entity.ts b/src/app/actions/entity.ts index 3718e11..bbbee51 100644 --- a/src/app/actions/entity.ts +++ b/src/app/actions/entity.ts @@ -5,8 +5,8 @@ import { databaseId } from '@/lib/appwrite'; import { patchClientInstance } from '@/lib/appwrite.server'; import { revalidatePath } from 'next/cache'; -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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; const DATABASE_ID = databaseId; const COLLECTION_ID = 'entity_config'; diff --git a/src/app/actions/profile.ts b/src/app/actions/profile.ts index be8e331..c92f174 100644 --- a/src/app/actions/profile.ts +++ b/src/app/actions/profile.ts @@ -6,8 +6,8 @@ import { Client, Databases, Users, ID, Query } from 'node-appwrite'; function getAdminServices() { const client = new Client() - .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1') - .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c') + .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1') + .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40') .setKey(process.env.APPWRITE_API_KEY || ''); return { diff --git a/src/app/actions/user-admin.ts b/src/app/actions/user-admin.ts index 2b81a24..1a22077 100644 --- a/src/app/actions/user-admin.ts +++ b/src/app/actions/user-admin.ts @@ -33,8 +33,8 @@ async function checkAdminAccess(): Promise { // Inicialización de servicios administrativos de Appwrite con la API Key del servidor function getAdminServices() { const client = new Client() - .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1') - .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c') + .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1') + .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40') .setKey(process.env.APPWRITE_API_KEY || ''); return { diff --git a/src/app/api/publications/route.ts b/src/app/api/publications/route.ts index 20281b7..435e737 100644 --- a/src/app/api/publications/route.ts +++ b/src/app/api/publications/route.ts @@ -36,7 +36,7 @@ async function requireAuth() { try { // Si es modo bypass (admin-emergency), no podemos llamar a account.get() con la API Key const cookieStore = await cookies(); - const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; const sessionCookie = cookieStore.get(`a_session_${PROJECT_ID.toLowerCase()}`); if (sessionCookie?.value === "admin-bypass-session") { diff --git a/src/app/clasificados/page.tsx b/src/app/clasificados/page.tsx index bd3eb12..0734482 100644 --- a/src/app/clasificados/page.tsx +++ b/src/app/clasificados/page.tsx @@ -133,8 +133,8 @@ export default async function ClasificadosPage() { ) : (
{posts.map((post: Post) => { - const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; - const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; + const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; let imagenUrl = post.og_image_url || null; const mediaObj = post.resolved_media || (typeof post.imagen_destacada_id === 'object' ? post.imagen_destacada_id : null) as MediaItem | null; diff --git a/src/app/dashboard/entity/page.tsx b/src/app/dashboard/entity/page.tsx index 8e81cec..93b4f51 100644 --- a/src/app/dashboard/entity/page.tsx +++ b/src/app/dashboard/entity/page.tsx @@ -69,8 +69,8 @@ export default async function EntityPage() {
{(() => { - 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; let logoUrl = entity?.logotipo || ''; if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; diff --git a/src/app/dashboard/facturacion/page.tsx b/src/app/dashboard/facturacion/page.tsx new file mode 100644 index 0000000..b23e037 --- /dev/null +++ b/src/app/dashboard/facturacion/page.tsx @@ -0,0 +1,502 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { + FileText, + CheckCircle2, + Clock, + ExternalLink, + Search, + Filter, + DollarSign, + Truck, + Building2, + Download, + RefreshCw, + Sparkles, + ArrowUpRight, + ShieldCheck, + Receipt +} from 'lucide-react'; + +interface InvoiceItem { + id: string; + tenantId: string; + prefix: string; + consecutive: string; + fullNumber: string; + cufe?: string; + dianStatus: string; + subtotal: number | string; + taxAmount: number | string; + grandTotal: number | string; + createdAt: string; + customer?: { + legalOrFullName: string; + identificationNumber: string; + email: string; + }; +} + +interface RemisionItem { + id: string; + tenantId: string; + fullNumber: string; + consecutive: string; + status: string; + grandTotal: number | string; + carrierName?: string; + trackingNumber?: string; + createdAt: string; + customer?: { + legalOrFullName: string; + }; +} + +export default function FacturacionDashboardPage() { + const [activeTab, setActiveTab] = useState<'facturas' | 'remisiones'>('facturas'); + const [tenantFilter, setTenantFilter] = useState<'all' | 'dripzone' | 'pacifictrans'>('all'); + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [invoices, setInvoices] = useState([]); + const [remissions, setRemissions] = useState([]); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + + const HUB_BACKEND_URL = process.env.NEXT_PUBLIC_HUB_URL || 'http://localhost:3001'; + const HUB_PORTAL_URL = 'http://localhost:3000'; + + useEffect(() => { + fetchData(); + }, [tenantFilter, activeTab]); + + const fetchData = async () => { + setLoading(true); + try { + // 1. Cargar Métricas + const tenantParam = + tenantFilter === 'dripzone' + ? 'd0000001-0000-0000-0000-000000000001' + : tenantFilter === 'pacifictrans' + ? 'd0000002-0000-0000-0000-000000000002' + : ''; + + const metUrl = tenantParam + ? `${HUB_BACKEND_URL}/api/v1/invoices/metrics?tenantId=${tenantParam}` + : `${HUB_BACKEND_URL}/api/v1/invoices/metrics`; + + const metRes = await fetch(metUrl); + if (metRes.ok) { + const metData = await metRes.json(); + setMetrics(metData.data || null); + } + + // 2. Cargar Facturas + if (activeTab === 'facturas') { + const invUrl = tenantParam + ? `${HUB_BACKEND_URL}/api/v1/invoices?tenantId=${tenantParam}` + : `${HUB_BACKEND_URL}/api/v1/invoices`; + const invRes = await fetch(invUrl); + if (invRes.ok) { + const invData = await invRes.json(); + setInvoices(invData.data?.items || []); + } + } else { + // 3. Cargar Remisiones + const remUrl = tenantParam + ? `${HUB_BACKEND_URL}/api/v1/remissions?tenantId=${tenantParam}` + : `${HUB_BACKEND_URL}/api/v1/remissions`; + const remRes = await fetch(remUrl); + if (remRes.ok) { + const remData = await remRes.json(); + setRemissions(remData.data || []); + } + } + } catch (err) { + console.error('Error cargando comprobantes:', err); + } finally { + setLoading(false); + } + }; + + const formatCOP = (val: number | string) => { + const num = Number(val) || 0; + return `$${num.toLocaleString('es-CO')} COP`; + }; + + const getTenantName = (tId?: string) => { + if (tId === 'd0000001-0000-0000-0000-000000000001') return 'DripZone'; + if (tId === 'd0000002-0000-0000-0000-000000000002') return 'PacificTrans'; + return 'General'; + }; + + const filteredInvoices = invoices.filter((inv) => { + const term = search.toLowerCase(); + const matchesSearch = + inv.fullNumber?.toLowerCase().includes(term) || + inv.customer?.legalOrFullName?.toLowerCase().includes(term) || + inv.customer?.identificationNumber?.includes(term) || + inv.cufe?.toLowerCase().includes(term); + + const matchesStatus = statusFilter === 'all' || inv.dianStatus === statusFilter; + return matchesSearch && matchesStatus; + }); + + const filteredRemissions = remissions.filter((rem) => { + const term = search.toLowerCase(); + const matchesSearch = + rem.fullNumber?.toLowerCase().includes(term) || + rem.customer?.legalOrFullName?.toLowerCase().includes(term) || + rem.carrierName?.toLowerCase().includes(term) || + rem.trackingNumber?.toLowerCase().includes(term); + + const matchesStatus = statusFilter === 'all' || rem.status === statusFilter; + return matchesSearch && matchesStatus; + }); + + return ( +
+ {/* Top Banner Header */} +
+
+
+
+ + + Facturación Electrónica DIAN + + + + En Línea + +
+

+ Centro Fiscal Corporativo +

+

+ Consolidación y emisión de comprobantes fiscales electrónicos oficiales para DripZone y PacificTrans. +

+
+ +
+ + + + Abrir Portal de Facturación + + +
+
+
+ + {/* KPI Cards */} +
+
+
+ Total Facturado + +
+
+ {formatCOP(metrics?.totalGrandTotal || 0)} +
+

Monto consolidado con validación oficial

+
+ +
+
+ Facturas DIAN + +
+
+ {metrics?.totalInvoices || 0} +
+

+ + Aprobadas por la DIAN +

+
+ +
+
+ Remisiones de Despacho + +
+
+ {remissions.length || (metrics?.totalRemisiones || 0)} +
+

Guías de flete y entregas

+
+ +
+
+ Empresas Vinculadas + +
+
+ 2 Unidades Activas +
+

DripZone • PacificTrans

+
+
+ + {/* Empresa Switcher & Tabs */} +
+
+ + + +
+ + {/* Tenant selector pill buttons */} +
+ + + +
+
+ + {/* Filter and Search Bar */} +
+
+ + setSearch(e.target.value)} + placeholder={ + activeTab === 'facturas' + ? 'Buscar por número de factura, cliente, cédula o CUFE...' + : 'Buscar por número de remisión, cliente, transportadora...' + } + className="w-full rounded-xl border border-slate-800 bg-slate-900/80 pl-9 pr-4 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-cyan-500 transition-all" + /> +
+ + +
+ + {/* Data Table */} + {loading ? ( +
+ +

Cargando comprobantes fiscales...

+
+ ) : activeTab === 'facturas' ? ( + filteredInvoices.length === 0 ? ( +
+ +

No hay facturas que coincidan con la búsqueda.

+
+ ) : ( +
+ {filteredInvoices.map((inv) => ( +
+
+
+ + {inv.fullNumber} + + + {getTenantName(inv.tenantId)} + + + + {inv.dianStatus || 'ACEPTADA'} + + + {new Date(inv.createdAt).toLocaleString('es-CO')} + +
+ + +
+ +
+
+ Adquirente +

{inv.customer?.legalOrFullName || 'Consumidor Final'}

+

NIT / C.C.: {inv.customer?.identificationNumber || '222222222222'}

+

{inv.customer?.email || 'N/A'}

+
+ +
+ Valores de la Factura +
+
+ Subtotal: + {formatCOP(inv.subtotal)} +
+
+ IVA: + {formatCOP(inv.taxAmount)} +
+
+ Total: + {formatCOP(inv.grandTotal)} +
+
+
+ +
+ Identificador DIAN (CUFE) +

+ {inv.cufe || 'Comprobante procesado oficialmente'} +

+
+
+
+ ))} +
+ ) + ) : ( + filteredRemissions.length === 0 ? ( +
+ +

No hay remisiones que coincidan con la búsqueda.

+
+ ) : ( +
+ {filteredRemissions.map((rem) => ( +
+
+
+ {rem.fullNumber} + + {getTenantName(rem.tenantId)} + + + {rem.status} + + + {new Date(rem.createdAt).toLocaleString('es-CO')} + +
+ +
+ + {formatCOP(rem.grandTotal)} + +
+
+ +
+
+ Destinatario +

{rem.customer?.legalOrFullName || 'Cliente'}

+
+
+ Transporte & Guía +

{rem.carrierName || 'Mensajería Express'}

+

Guía: {rem.trackingNumber || 'N/A'}

+
+
+
+ ))} +
+ ) + )} +
+ ); +} diff --git a/src/app/dashboard/media/page.tsx b/src/app/dashboard/media/page.tsx index aac0c00..8fa5d83 100644 --- a/src/app/dashboard/media/page.tsx +++ b/src/app/dashboard/media/page.tsx @@ -51,8 +51,8 @@ export default async function MediaLibraryPage({ } // Appwrite public view format for our config - 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; const getFilePreviewUrl = (bucketId: string, fileId: string) => { // Usamos el endpoint público de Appwrite para obtener la imagen diff --git a/src/app/eventos/page.tsx b/src/app/eventos/page.tsx index a0c50a5..d8eacee 100644 --- a/src/app/eventos/page.tsx +++ b/src/app/eventos/page.tsx @@ -133,8 +133,8 @@ export default async function EventosPage() { ) : (
{posts.map((post: Post) => { - const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; - const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; + const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; let imagenUrl = post.og_image_url || null; const mediaObj = post.resolved_media || (typeof post.imagen_destacada_id === 'object' ? post.imagen_destacada_id : null) as MediaItem | null; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6ce1bdc..7cee09e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,8 +12,8 @@ export async function generateMetadata(): Promise { const entity = await getEntity(); const razonSocial = entity?.razon_social || "Almeira Producciones"; - 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; let logoUrl = entity?.logotipo || null; if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7d9e96c..eb134b6 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -5,8 +5,8 @@ import { Radio } from 'lucide-react'; export default async function LoginPage() { const entity = await getEntity(); - 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; let logoUrl = entity?.logotipo || null; if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; diff --git a/src/app/page.tsx b/src/app/page.tsx index 4574de2..5301538 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -213,8 +213,8 @@ export default async function Home(props: { searchParams: Promise<{ [key: string ) : (
{posts.map((post: Post) => { - const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; - const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; + const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; let imagenUrl = post.og_image_url || null; const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' || diff --git a/src/components/ClientSidebars.tsx b/src/components/ClientSidebars.tsx index b4fface..81a644e 100644 --- a/src/components/ClientSidebars.tsx +++ b/src/components/ClientSidebars.tsx @@ -44,8 +44,8 @@ function ClientCarouselGroup({ eslogan, ads, rotationSeconds }: { eslogan: strin const activeAd = ads[currentIndex]; if (!activeAd.imagen_destacada_id) return null; - 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; let imgUrl = activeAd.og_image_url || null; if (activeAd.imagen_destacada_id) { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 98d0eff..0838397 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -11,8 +11,8 @@ export const Navbar = ({ entity }: { entity?: any }) => { const [user, setUser] = useState(null); const pathname = usePathname(); - 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; let logoUrl = entity?.logotipo || null; if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index cf12763..9764c31 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -43,6 +43,7 @@ export const Sidebar = ({ user }: { user: any }) => { items: [ { name: 'Contratos', href: '/dashboard/contracts', icon: FileText }, { name: 'Formas de Pago', href: '/dashboard/payment_methods', icon: BadgeDollarSign }, + { name: 'Facturación Electrónica', href: '/dashboard/facturacion', icon: BadgeDollarSign }, ] }, { diff --git a/src/components/TopBanner.tsx b/src/components/TopBanner.tsx index 7028422..61c9979 100644 --- a/src/components/TopBanner.tsx +++ b/src/components/TopBanner.tsx @@ -26,8 +26,8 @@ export default function TopBanner({ initialBanners, rotationSeconds }: { initial if (!activeBanner.imagen_destacada_id && !activeBanner.og_image_url) return null; - 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; let imgUrl = activeBanner.og_image_url || null; diff --git a/src/components/publications/RelationSelectField.tsx b/src/components/publications/RelationSelectField.tsx index 7765894..7d889fa 100644 --- a/src/components/publications/RelationSelectField.tsx +++ b/src/components/publications/RelationSelectField.tsx @@ -183,8 +183,8 @@ function QuickCreatePortal({ collection, fieldName, onClose }: QuickCreatePortal function MediaThumbnail({ fileId }: { fileId: string }) { if (!fileId) return null; - const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; - const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; + const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; + const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; const url = `${endpoint}/storage/buckets/media/files/${fileId}/preview?project=${projectId}&width=400&height=250&quality=80`; diff --git a/src/lib/appwrite-tables-shim.js b/src/lib/appwrite-tables-shim.js new file mode 100644 index 0000000..6cb7d9c --- /dev/null +++ b/src/lib/appwrite-tables-shim.js @@ -0,0 +1,58 @@ +function applyTablesDbShim(clientInstance) { + if (clientInstance._hasTablesShim) return clientInstance; + const origCall = clientInstance.call.bind(clientInstance); + + clientInstance.call = async function (method, url, headers = {}, params = {}, responseType = 'json') { + let urlStr = url instanceof URL ? url.toString() : String(url); + + // Rewrite /databases/{db}/collections/{col}/documents/{docId} -> /tablesdb/{db}/tables/{col}/rows/{docId} + const isLegacyCollectionRoute = /\/databases\/([^/]+)\/collections/.test(urlStr); + if (isLegacyCollectionRoute) { + urlStr = urlStr + .replace(/\/databases\/([^/]+)\/collections\/([^/]+)\/documents/g, '/tablesdb/$1/tables/$2/rows') + .replace(/\/databases\/([^/]+)\/collections/g, '/tablesdb/$1/tables'); + + if (url instanceof URL) { + url = new URL(urlStr); + } else { + url = urlStr; + } + + if (params && typeof params === 'object') { + params = { ...params }; + if ('documentId' in params) { + params.rowId = params.documentId; + delete params.documentId; + } + if ('collectionId' in params) { + params.tableId = params.collectionId; + delete params.collectionId; + } + if ('documentSecurity' in params) { + params.rowSecurity = params.documentSecurity; + delete params.documentSecurity; + } + } + } + + const res = await origCall(method, url, headers, params, responseType); + if (res && typeof res === 'object') { + if (Array.isArray(res.rows) && !res.documents) { + res.documents = res.rows; + } + if (Array.isArray(res.tables) && !res.collections) { + res.collections = res.tables; + } + if (Array.isArray(res.columns) && !res.attributes) { + res.attributes = res.columns; + } + } + return res; + }; + + clientInstance._hasTablesShim = true; + return clientInstance; +} + +module.exports = { applyTablesDbShim }; + diff --git a/src/lib/appwrite.server.ts b/src/lib/appwrite.server.ts index 8164131..695492d 100644 --- a/src/lib/appwrite.server.ts +++ b/src/lib/appwrite.server.ts @@ -1,22 +1,46 @@ import { Client, Account, Databases, Storage } from 'node-appwrite'; import { cookies } from 'next/headers'; -// Robust instance-level patch for Appwrite Next.js Server Components +// Robust instance-level patch for Appwrite Next.js Server Components (supports both 1.x Databases and 2.x TablesDB) export function patchClientInstance(client: any) { if (client._isPatched) return client; - const originalCall = client.call.bind(client); - client.call = async function (...args: any[]) { - // Suppress Appwrite version mismatch warnings during this call + const originalCall = client.call; + client.call = async function (method: string, url: any, headers: any = {}, params: any = {}, responseType: string = 'json') { const oldWarn = console.warn; console.warn = (...warnArgs: any[]) => { if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return; oldWarn(...warnArgs); }; try { - const res = await originalCall(...args); - // Ensure all objects are fully plain JSON (strips getters/prototypes that break Server -> Client transitions) + let urlStr = url instanceof URL ? url.toString() : String(url); + if (/\/databases\/([^/]+)\/collections/.test(urlStr)) { + urlStr = urlStr + .replace(/\/databases\/([^/]+)\/collections\/([^/]+)\/documents/g, '/tablesdb/$1/tables/$2/rows') + .replace(/\/databases\/([^/]+)\/collections/g, '/tablesdb/$1/tables'); + url = url instanceof URL ? new URL(urlStr) : urlStr; + if (params && typeof params === 'object') { + params = { ...params }; + if ('documentId' in params) { + params.rowId = params.documentId; + delete params.documentId; + } + if ('collectionId' in params) { + params.tableId = params.collectionId; + delete params.collectionId; + } + if ('documentSecurity' in params) { + params.rowSecurity = params.documentSecurity; + delete params.documentSecurity; + } + } + } + const res = await originalCall.call(this, method, url, headers, params, responseType); if (res && typeof res === 'object') { - return JSON.parse(JSON.stringify(res)); + const plain = JSON.parse(JSON.stringify(res)); + if (Array.isArray(plain.rows) && !plain.documents) plain.documents = plain.rows; + if (Array.isArray(plain.tables) && !plain.collections) plain.collections = plain.tables; + if (Array.isArray(plain.columns) && !plain.attributes) plain.attributes = plain.columns; + return plain; } return res; } finally { @@ -27,8 +51,10 @@ export function patchClientInstance(client: any) { return 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'; +patchClientInstance(Client.prototype); + +const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; const COOKIE_NAME = `a_session_${PROJECT_ID.toLowerCase()}`; export async function createSessionClient() { diff --git a/src/lib/appwrite.ts b/src/lib/appwrite.ts index 4bdedb2..68136a1 100644 --- a/src/lib/appwrite.ts +++ b/src/lib/appwrite.ts @@ -1,21 +1,45 @@ import { Client, Databases, Storage, Query, Account } from 'appwrite'; -// Robust instance-level patch for Appwrite Client +// Robust instance-level patch for Appwrite Client (supports both Appwrite 1.x Databases and 2.x TablesDB) function patchClientInstance(clientInstance: any) { if (clientInstance._isPatched) return clientInstance; - const originalCall = clientInstance.call.bind(clientInstance); - clientInstance.call = async function (...args: any[]) { - // Suppress Appwrite version mismatch warnings during this call + const originalCall = clientInstance.call; + clientInstance.call = async function (method: string, url: any, headers: any = {}, params: any = {}, responseType: string = 'json') { const oldWarn = console.warn; console.warn = (...warnArgs: any[]) => { if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return; oldWarn(...warnArgs); }; try { - const res = await originalCall(...args); - // Ensure all objects are fully plain JSON (strips getters/prototypes that break Server -> Client transitions) + let urlStr = url instanceof URL ? url.toString() : String(url); + if (/\/databases\/([^/]+)\/collections/.test(urlStr)) { + urlStr = urlStr + .replace(/\/databases\/([^/]+)\/collections\/([^/]+)\/documents/g, '/tablesdb/$1/tables/$2/rows') + .replace(/\/databases\/([^/]+)\/collections/g, '/tablesdb/$1/tables'); + url = url instanceof URL ? new URL(urlStr) : urlStr; + if (params && typeof params === 'object') { + params = { ...params }; + if ('documentId' in params) { + params.rowId = params.documentId; + delete params.documentId; + } + if ('collectionId' in params) { + params.tableId = params.collectionId; + delete params.collectionId; + } + if ('documentSecurity' in params) { + params.rowSecurity = params.documentSecurity; + delete params.documentSecurity; + } + } + } + const res = await originalCall.call(this, method, url, headers, params, responseType); if (res && typeof res === 'object') { - return JSON.parse(JSON.stringify(res)); + const plain = JSON.parse(JSON.stringify(res)); + if (Array.isArray(plain.rows) && !plain.documents) plain.documents = plain.rows; + if (Array.isArray(plain.tables) && !plain.collections) plain.collections = plain.tables; + if (Array.isArray(plain.columns) && !plain.attributes) plain.attributes = plain.columns; + return plain; } return res; } finally { @@ -26,10 +50,12 @@ function patchClientInstance(clientInstance: any) { return clientInstance; } +patchClientInstance(Client.prototype); + const client = new Client(); -export const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; -export const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; +export const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1'; +export const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40'; export const databaseId = process.env.APPWRITE_DATABASE_ID || 'almeira'; client diff --git a/update-multivision-slugs.js b/update-multivision-slugs.js index 57d57d9..d4542cb 100644 --- a/update-multivision-slugs.js +++ b/update-multivision-slugs.js @@ -1,10 +1,12 @@ const { Client, Databases, Query } = require('node-appwrite'); require('dotenv').config({ path: '.env.local' }); +const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim'); 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); +applyTablesDbShim(client); const databases = new Databases(client); const DB_ID = process.env.APPWRITE_DATABASE_ID;