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.
+
+
+
+
+
+
+
+ {/* 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;