feat(almeira): migrate to Appwrite Cloud (nyc.cloud.appwrite.io) with TablesDB shim and full WP/Cauca/Multivision restoration

This commit is contained in:
AI Bot 2026-11-07 13:44:33 -05:00
parent 483090aa1e
commit 6c5037d07d
31 changed files with 2157 additions and 56 deletions

View File

@ -7,10 +7,12 @@ require('dotenv').config({ path: '.env.local' });
let InputFile; let InputFile;
try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; } try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; }
const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim');
const client = new Client() const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY); .setKey(process.env.APPWRITE_API_KEY);
applyTablesDbShim(client);
const databases = new Databases(client); const databases = new Databases(client);
const storage = new Storage(client); const storage = new Storage(client);

View File

@ -8,10 +8,12 @@ require('dotenv').config({ path: '.env.local' });
let InputFile; let InputFile;
try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; } try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; }
const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim');
const client = new Client() const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY); .setKey(process.env.APPWRITE_API_KEY);
applyTablesDbShim(client);
const databases = new Databases(client); const databases = new Databases(client);
const storage = new Storage(client); const storage = new Storage(client);
@ -246,8 +248,9 @@ async function run() {
const doc = { const doc = {
titulo: data.title.substring(0, 100), titulo: data.title.substring(0, 100),
slug: `multivision-final-${Date.now()}-${count}`, slug: `multivision-final-${Date.now()}-${count}`,
tipo: 'noticia',
tipo_publicacion_id: typeId, tipo_publicacion_id: typeId,
estado: 'Publicado', estado: 'publicado',
fecha_publicacion: data.date, fecha_publicacion: data.date,
contenido: data.content, contenido: data.content,
resumen_redes: data.resumen, resumen_redes: data.resumen,

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&quot;/gi, '"')
.replace(/&#8220;|&#8221;/gi, '"')
.replace(/&#8216;|&#8217;/gi, "'")
.replace(/&#8211;|&#8212;/gi, '-')
.replace(/\s+/g, ' ')
.trim();
}
function formatWpContent(raw) {
if (!raw) return '<p></p>';
let content = raw.trim();
if (!content.includes('<p') && !content.includes('<div') && !content.includes('<h')) {
content = content
.split(/\r?\n\r?\n/)
.map((p) => p.trim())
.filter(Boolean)
.map((p) => `<p>${p.replace(/\r?\n/g, '<br/>')}</p>`)
.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<mediaDocId>
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);
});

View File

@ -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('<!doctype') || buffer.toString('utf-8', 0, 20).trim().toLowerCase().startsWith('<html');
if (!isHtml && buffer.length > 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();

View File

@ -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);
});

View File

@ -168,8 +168,8 @@ export async function generateMetadata(
? decodeHtmlEntities(post.resumen_redes) ? decodeHtmlEntities(post.resumen_redes)
: (post.contenido?.replace(/<[^>]*>?/gm, '').substring(0, 160) + '...'); : (post.contenido?.replace(/<[^>]*>?/gm, '').substring(0, 160) + '...');
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
let imagenUrl = post.og_image_url || null; let imagenUrl = post.og_image_url || null;
if (post.imagen_destacada_id) { 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 PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
let imagenUrl = post.og_image_url || null; let imagenUrl = post.og_image_url || null;
const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' || 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')); (typeof post.tipo_publicacion_id === 'object' && (post.tipo_publicacion_id.codigo === 'EVENTO' || post.tipo_publicacion_id.codigo === 'CLASIFICADO'));

View File

@ -6,8 +6,8 @@ import { patchClientInstance } from '@/lib/appwrite.server';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { InputFile } from 'node-appwrite/file'; import { InputFile } from 'node-appwrite/file';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const DATABASE_ID = databaseId; const DATABASE_ID = databaseId;
function getAdminClient() { function getAdminClient() {

View File

@ -4,8 +4,8 @@ import { Client, Account } from 'node-appwrite';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const COOKIE_NAME = `a_session_${PROJECT_ID.toLowerCase()}`; const COOKIE_NAME = `a_session_${PROJECT_ID.toLowerCase()}`;
// Password de emergencia (Configurado en Coolify) // Password de emergencia (Configurado en Coolify)

View File

@ -5,8 +5,8 @@ import { databaseId } from '@/lib/appwrite';
import { patchClientInstance } from '@/lib/appwrite.server'; import { patchClientInstance } from '@/lib/appwrite.server';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const DATABASE_ID = databaseId; const DATABASE_ID = databaseId;
const COLLECTION_ID = 'entity_config'; const COLLECTION_ID = 'entity_config';

View File

@ -6,8 +6,8 @@ import { Client, Databases, Users, ID, Query } from 'node-appwrite';
function getAdminServices() { function getAdminServices() {
const client = new Client() const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1') .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1')
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c') .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40')
.setKey(process.env.APPWRITE_API_KEY || ''); .setKey(process.env.APPWRITE_API_KEY || '');
return { return {

View File

@ -33,8 +33,8 @@ async function checkAdminAccess(): Promise<boolean> {
// Inicialización de servicios administrativos de Appwrite con la API Key del servidor // Inicialización de servicios administrativos de Appwrite con la API Key del servidor
function getAdminServices() { function getAdminServices() {
const client = new Client() const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1') .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1')
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c') .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40')
.setKey(process.env.APPWRITE_API_KEY || ''); .setKey(process.env.APPWRITE_API_KEY || '');
return { return {

View File

@ -36,7 +36,7 @@ async function requireAuth() {
try { try {
// Si es modo bypass (admin-emergency), no podemos llamar a account.get() con la API Key // Si es modo bypass (admin-emergency), no podemos llamar a account.get() con la API Key
const cookieStore = await cookies(); 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()}`); const sessionCookie = cookieStore.get(`a_session_${PROJECT_ID.toLowerCase()}`);
if (sessionCookie?.value === "admin-bypass-session") { if (sessionCookie?.value === "admin-bypass-session") {

View File

@ -133,8 +133,8 @@ export default async function ClasificadosPage() {
) : ( ) : (
<div className={`grid grid-cols-1 md:grid-cols-2 ${hasSidebar ? 'lg:grid-cols-2 xl:grid-cols-3' : 'lg:grid-cols-3 xl:grid-cols-4'} gap-6`}> <div className={`grid grid-cols-1 md:grid-cols-2 ${hasSidebar ? 'lg:grid-cols-2 xl:grid-cols-3' : 'lg:grid-cols-3 xl:grid-cols-4'} gap-6`}>
{posts.map((post: Post) => { {posts.map((post: Post) => {
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
let imagenUrl = post.og_image_url || null; 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; const mediaObj = post.resolved_media || (typeof post.imagen_destacada_id === 'object' ? post.imagen_destacada_id : null) as MediaItem | null;

View File

@ -69,8 +69,8 @@ export default async function EntityPage() {
</div> </div>
{(() => { {(() => {
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
let logoUrl = entity?.logotipo || ''; let logoUrl = entity?.logotipo || '';
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;

View File

@ -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<InvoiceItem[]>([]);
const [remissions, setRemissions] = useState<RemisionItem[]>([]);
const [metrics, setMetrics] = useState<any>(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 (
<div className="space-y-6">
{/* Top Banner Header */}
<div className="relative overflow-hidden rounded-2xl border border-slate-800 bg-gradient-to-r from-slate-900 via-blue-950/30 to-slate-900 p-6 shadow-xl backdrop-blur-xl">
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/30 px-3 py-1 text-xs font-bold text-cyan-400">
<ShieldCheck className="w-3.5 h-3.5" />
<span>Facturación Electrónica DIAN</span>
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 px-2.5 py-0.5 text-[11px] font-semibold text-emerald-400">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
<span>En Línea</span>
</span>
</div>
<h1 className="text-2xl font-black tracking-tight text-white font-outfit">
Centro Fiscal Corporativo
</h1>
<p className="text-xs text-slate-400 max-w-2xl leading-relaxed">
Consolidación y emisión de comprobantes fiscales electrónicos oficiales para DripZone y PacificTrans.
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={fetchData}
disabled={loading}
className="inline-flex items-center gap-2 rounded-xl border border-slate-700 bg-slate-800/80 px-3.5 py-2 text-xs font-bold text-slate-200 hover:bg-slate-700 transition-all cursor-pointer disabled:opacity-50"
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
<span>Actualizar</span>
</button>
<a
href={HUB_PORTAL_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-blue-600 to-cyan-500 px-4 py-2 text-xs font-bold text-white shadow-lg shadow-cyan-500/20 hover:brightness-110 active:scale-95 transition-all cursor-pointer"
>
<span>Abrir Portal de Facturación</span>
<ArrowUpRight className="w-4 h-4" />
</a>
</div>
</div>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 space-y-1.5 shadow-sm">
<div className="flex items-center justify-between text-slate-400 text-xs font-bold">
<span>Total Facturado</span>
<DollarSign className="w-4 h-4 text-emerald-400" />
</div>
<div className="text-xl font-black text-white font-mono">
{formatCOP(metrics?.totalGrandTotal || 0)}
</div>
<p className="text-[11px] text-slate-500">Monto consolidado con validación oficial</p>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 space-y-1.5 shadow-sm">
<div className="flex items-center justify-between text-slate-400 text-xs font-bold">
<span>Facturas DIAN</span>
<CheckCircle2 className="w-4 h-4 text-cyan-400" />
</div>
<div className="text-xl font-black text-white font-mono">
{metrics?.totalInvoices || 0}
</div>
<p className="text-[11px] text-emerald-400 font-bold flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block"></span>
Aprobadas por la DIAN
</p>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 space-y-1.5 shadow-sm">
<div className="flex items-center justify-between text-slate-400 text-xs font-bold">
<span>Remisiones de Despacho</span>
<Truck className="w-4 h-4 text-amber-400" />
</div>
<div className="text-xl font-black text-white font-mono">
{remissions.length || (metrics?.totalRemisiones || 0)}
</div>
<p className="text-[11px] text-slate-500">Guías de flete y entregas</p>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 space-y-1.5 shadow-sm">
<div className="flex items-center justify-between text-slate-400 text-xs font-bold">
<span>Empresas Vinculadas</span>
<Building2 className="w-4 h-4 text-blue-400" />
</div>
<div className="text-sm font-bold text-white pt-1">
2 Unidades Activas
</div>
<p className="text-[11px] text-slate-400 font-medium">DripZone • PacificTrans</p>
</div>
</div>
{/* Empresa Switcher & Tabs */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-slate-800 pb-3">
<div className="flex items-center gap-2">
<button
onClick={() => setActiveTab('facturas')}
className={`flex items-center gap-2 px-3.5 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer ${
activeTab === 'facturas'
? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/30'
: 'text-slate-400 hover:text-white hover:bg-slate-800/60'
}`}
>
<Receipt className="w-4 h-4" />
<span>Facturas Emitidas ({invoices.length})</span>
</button>
<button
onClick={() => setActiveTab('remisiones')}
className={`flex items-center gap-2 px-3.5 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer ${
activeTab === 'remisiones'
? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/30'
: 'text-slate-400 hover:text-white hover:bg-slate-800/60'
}`}
>
<Truck className="w-4 h-4" />
<span>Remisiones de Despacho ({remissions.length})</span>
</button>
</div>
{/* Tenant selector pill buttons */}
<div className="flex items-center gap-1.5 bg-slate-900/80 p-1 rounded-xl border border-slate-800 text-xs">
<button
onClick={() => setTenantFilter('all')}
className={`px-3 py-1 rounded-lg font-bold transition-all cursor-pointer ${
tenantFilter === 'all'
? 'bg-blue-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white'
}`}
>
Todas
</button>
<button
onClick={() => setTenantFilter('dripzone')}
className={`px-3 py-1 rounded-lg font-bold transition-all cursor-pointer ${
tenantFilter === 'dripzone'
? 'bg-purple-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white'
}`}
>
👟 DripZone
</button>
<button
onClick={() => setTenantFilter('pacifictrans')}
className={`px-3 py-1 rounded-lg font-bold transition-all cursor-pointer ${
tenantFilter === 'pacifictrans'
? 'bg-cyan-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white'
}`}
>
🚛 PacificTrans
</button>
</div>
</div>
{/* Filter and Search Bar */}
<div className="flex flex-col sm:flex-row items-center gap-3">
<div className="relative flex-1 w-full">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
value={search}
onChange={(e) => 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"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="rounded-xl border border-slate-800 bg-slate-900/80 px-3 py-2 text-xs font-bold text-slate-200 outline-none cursor-pointer"
>
<option value="all">Todos los estados</option>
{activeTab === 'facturas' ? (
<>
<option value="ACEPTADA">Aceptada por la DIAN</option>
<option value="CON_RECHAZO">Con Rechazo</option>
</>
) : (
<>
<option value="CREADA">Creada</option>
<option value="DESPACHADO">Despachada</option>
<option value="FACTURADO">Facturada</option>
</>
)}
</select>
</div>
{/* Data Table */}
{loading ? (
<div className="rounded-2xl border border-slate-800 bg-slate-900/40 p-12 text-center text-slate-400 space-y-3">
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-cyan-400" />
<p className="text-xs">Cargando comprobantes fiscales...</p>
</div>
) : activeTab === 'facturas' ? (
filteredInvoices.length === 0 ? (
<div className="rounded-2xl border border-slate-800 bg-slate-900/40 p-12 text-center text-slate-500 space-y-2">
<Receipt className="w-8 h-8 mx-auto text-slate-600" />
<p className="text-xs">No hay facturas que coincidan con la búsqueda.</p>
</div>
) : (
<div className="space-y-3">
{filteredInvoices.map((inv) => (
<div
key={inv.id}
className="rounded-xl border border-slate-800/80 bg-slate-900/60 hover:border-slate-700 p-4 transition-all shadow-sm space-y-3"
>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/60 pb-2.5">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono font-bold text-white text-sm">
{inv.fullNumber}
</span>
<span className="rounded-md bg-blue-500/10 border border-blue-500/20 px-2 py-0.5 text-[10px] font-bold text-blue-300">
{getTenantName(inv.tenantId)}
</span>
<span className="rounded-md bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 text-[10px] font-bold text-emerald-400 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3" />
<span>{inv.dianStatus || 'ACEPTADA'}</span>
</span>
<span className="text-[11px] text-slate-500 font-mono">
{new Date(inv.createdAt).toLocaleString('es-CO')}
</span>
</div>
<div className="flex items-center gap-2">
<a
href={`${HUB_BACKEND_URL}/api/v1/invoices/${inv.id}/pdf`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-1 text-xs font-bold text-cyan-300 hover:bg-slate-700 transition-all cursor-pointer"
>
<Download className="w-3 h-3" />
<span>PDF</span>
</a>
<a
href={`${HUB_BACKEND_URL}/api/v1/invoices/${inv.id}/xml`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-2.5 py-1 text-xs font-bold text-slate-300 hover:bg-slate-700 transition-all cursor-pointer"
>
<FileText className="w-3 h-3 text-blue-400" />
<span>XML</span>
</a>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
<div>
<span className="text-[10px] font-bold uppercase text-slate-500 block">Adquirente</span>
<p className="font-bold text-white">{inv.customer?.legalOrFullName || 'Consumidor Final'}</p>
<p className="font-mono text-slate-400 text-[11px]">NIT / C.C.: {inv.customer?.identificationNumber || '222222222222'}</p>
<p className="text-slate-500 text-[11px]">{inv.customer?.email || 'N/A'}</p>
</div>
<div>
<span className="text-[10px] font-bold uppercase text-slate-500 block">Valores de la Factura</span>
<div className="space-y-0.5">
<div className="flex justify-between text-slate-400 text-[11px]">
<span>Subtotal:</span>
<span className="font-mono text-slate-200">{formatCOP(inv.subtotal)}</span>
</div>
<div className="flex justify-between text-slate-400 text-[11px]">
<span>IVA:</span>
<span className="font-mono text-slate-200">{formatCOP(inv.taxAmount)}</span>
</div>
<div className="flex justify-between text-white font-bold text-xs pt-1 border-t border-slate-800">
<span>Total:</span>
<span className="font-mono text-emerald-400">{formatCOP(inv.grandTotal)}</span>
</div>
</div>
</div>
<div>
<span className="text-[10px] font-bold uppercase text-slate-500 block">Identificador DIAN (CUFE)</span>
<p className="font-mono text-[10px] text-slate-400 break-all bg-slate-950/70 p-2 rounded-lg border border-slate-800">
{inv.cufe || 'Comprobante procesado oficialmente'}
</p>
</div>
</div>
</div>
))}
</div>
)
) : (
filteredRemissions.length === 0 ? (
<div className="rounded-2xl border border-slate-800 bg-slate-900/40 p-12 text-center text-slate-500 space-y-2">
<Truck className="w-8 h-8 mx-auto text-slate-600" />
<p className="text-xs">No hay remisiones que coincidan con la búsqueda.</p>
</div>
) : (
<div className="space-y-3">
{filteredRemissions.map((rem) => (
<div
key={rem.id}
className="rounded-xl border border-slate-800/80 bg-slate-900/60 hover:border-slate-700 p-4 transition-all shadow-sm space-y-3"
>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/60 pb-2.5">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono font-bold text-white text-sm">{rem.fullNumber}</span>
<span className="rounded-md bg-purple-500/10 border border-purple-500/20 px-2 py-0.5 text-[10px] font-bold text-purple-300">
{getTenantName(rem.tenantId)}
</span>
<span className="rounded-md bg-blue-500/10 border border-blue-500/20 px-2 py-0.5 text-[10px] font-bold text-blue-300">
{rem.status}
</span>
<span className="text-[11px] text-slate-500 font-mono">
{new Date(rem.createdAt).toLocaleString('es-CO')}
</span>
</div>
<div>
<span className="font-mono font-bold text-emerald-400 text-sm">
{formatCOP(rem.grandTotal)}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div>
<span className="text-[10px] font-bold uppercase text-slate-500 block">Destinatario</span>
<p className="font-bold text-white">{rem.customer?.legalOrFullName || 'Cliente'}</p>
</div>
<div>
<span className="text-[10px] font-bold uppercase text-slate-500 block">Transporte & Guía</span>
<p className="text-slate-300 font-medium">{rem.carrierName || 'Mensajería Express'}</p>
<p className="text-cyan-400 font-mono text-[11px]">Guía: {rem.trackingNumber || 'N/A'}</p>
</div>
</div>
</div>
))}
</div>
)
)}
</div>
);
}

View File

@ -51,8 +51,8 @@ export default async function MediaLibraryPage({
} }
// Appwrite public view format for our config // Appwrite public view format for our config
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const getFilePreviewUrl = (bucketId: string, fileId: string) => { const getFilePreviewUrl = (bucketId: string, fileId: string) => {
// Usamos el endpoint público de Appwrite para obtener la imagen // Usamos el endpoint público de Appwrite para obtener la imagen

View File

@ -133,8 +133,8 @@ export default async function EventosPage() {
) : ( ) : (
<div className={`grid grid-cols-1 md:grid-cols-2 ${hasSidebar ? 'lg:grid-cols-2 xl:grid-cols-3' : 'lg:grid-cols-3 xl:grid-cols-4'} gap-6`}> <div className={`grid grid-cols-1 md:grid-cols-2 ${hasSidebar ? 'lg:grid-cols-2 xl:grid-cols-3' : 'lg:grid-cols-3 xl:grid-cols-4'} gap-6`}>
{posts.map((post: Post) => { {posts.map((post: Post) => {
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
let imagenUrl = post.og_image_url || null; 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; const mediaObj = post.resolved_media || (typeof post.imagen_destacada_id === 'object' ? post.imagen_destacada_id : null) as MediaItem | null;

View File

@ -12,8 +12,8 @@ export async function generateMetadata(): Promise<Metadata> {
const entity = await getEntity(); const entity = await getEntity();
const razonSocial = entity?.razon_social || "Almeira Producciones"; const razonSocial = entity?.razon_social || "Almeira Producciones";
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
let logoUrl = entity?.logotipo || null; let logoUrl = entity?.logotipo || null;
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;

View File

@ -5,8 +5,8 @@ import { Radio } from 'lucide-react';
export default async function LoginPage() { export default async function LoginPage() {
const entity = await getEntity(); const entity = await getEntity();
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
let logoUrl = entity?.logotipo || null; let logoUrl = entity?.logotipo || null;
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;

View File

@ -213,8 +213,8 @@ export default async function Home(props: { searchParams: Promise<{ [key: string
) : ( ) : (
<div className={`grid grid-cols-1 md:grid-cols-2 ${hasSidebar ? 'lg:grid-cols-2 xl:grid-cols-3' : 'lg:grid-cols-3 xl:grid-cols-4'} gap-6`}> <div className={`grid grid-cols-1 md:grid-cols-2 ${hasSidebar ? 'lg:grid-cols-2 xl:grid-cols-3' : 'lg:grid-cols-3 xl:grid-cols-4'} gap-6`}>
{posts.map((post: Post) => { {posts.map((post: Post) => {
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
let imagenUrl = post.og_image_url || null; let imagenUrl = post.og_image_url || null;
const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' || const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' ||

View File

@ -44,8 +44,8 @@ function ClientCarouselGroup({ eslogan, ads, rotationSeconds }: { eslogan: strin
const activeAd = ads[currentIndex]; const activeAd = ads[currentIndex];
if (!activeAd.imagen_destacada_id) return null; if (!activeAd.imagen_destacada_id) return null;
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
let imgUrl = activeAd.og_image_url || null; let imgUrl = activeAd.og_image_url || null;
if (activeAd.imagen_destacada_id) { if (activeAd.imagen_destacada_id) {

View File

@ -11,8 +11,8 @@ export const Navbar = ({ entity }: { entity?: any }) => {
const [user, setUser] = useState<any>(null); const [user, setUser] = useState<any>(null);
const pathname = usePathname(); const pathname = usePathname();
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
let logoUrl = entity?.logotipo || null; let logoUrl = entity?.logotipo || null;
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) { if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`; logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;

View File

@ -43,6 +43,7 @@ export const Sidebar = ({ user }: { user: any }) => {
items: [ items: [
{ name: 'Contratos', href: '/dashboard/contracts', icon: FileText }, { name: 'Contratos', href: '/dashboard/contracts', icon: FileText },
{ name: 'Formas de Pago', href: '/dashboard/payment_methods', icon: BadgeDollarSign }, { name: 'Formas de Pago', href: '/dashboard/payment_methods', icon: BadgeDollarSign },
{ name: 'Facturación Electrónica', href: '/dashboard/facturacion', icon: BadgeDollarSign },
] ]
}, },
{ {

View File

@ -26,8 +26,8 @@ export default function TopBanner({ initialBanners, rotationSeconds }: { initial
if (!activeBanner.imagen_destacada_id && !activeBanner.og_image_url) return null; 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 ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
let imgUrl = activeBanner.og_image_url || null; let imgUrl = activeBanner.og_image_url || null;

View File

@ -183,8 +183,8 @@ function QuickCreatePortal({ collection, fieldName, onClose }: QuickCreatePortal
function MediaThumbnail({ fileId }: { fileId: string }) { function MediaThumbnail({ fileId }: { fileId: string }) {
if (!fileId) return null; if (!fileId) return null;
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; 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`; const url = `${endpoint}/storage/buckets/media/files/${fileId}/preview?project=${projectId}&width=400&height=250&quality=80`;

View File

@ -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 };

View File

@ -1,22 +1,46 @@
import { Client, Account, Databases, Storage } from 'node-appwrite'; import { Client, Account, Databases, Storage } from 'node-appwrite';
import { cookies } from 'next/headers'; 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) { export function patchClientInstance(client: any) {
if (client._isPatched) return client; if (client._isPatched) return client;
const originalCall = client.call.bind(client); const originalCall = client.call;
client.call = async function (...args: any[]) { client.call = async function (method: string, url: any, headers: any = {}, params: any = {}, responseType: string = 'json') {
// Suppress Appwrite version mismatch warnings during this call
const oldWarn = console.warn; const oldWarn = console.warn;
console.warn = (...warnArgs: any[]) => { console.warn = (...warnArgs: any[]) => {
if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return; if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return;
oldWarn(...warnArgs); oldWarn(...warnArgs);
}; };
try { try {
const res = await originalCall(...args); let urlStr = url instanceof URL ? url.toString() : String(url);
// Ensure all objects are fully plain JSON (strips getters/prototypes that break Server -> Client transitions) 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') { 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; return res;
} finally { } finally {
@ -27,8 +51,10 @@ export function patchClientInstance(client: any) {
return client; return client;
} }
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; patchClientInstance(Client.prototype);
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()}`; const COOKIE_NAME = `a_session_${PROJECT_ID.toLowerCase()}`;
export async function createSessionClient() { export async function createSessionClient() {

View File

@ -1,21 +1,45 @@
import { Client, Databases, Storage, Query, Account } from 'appwrite'; 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) { function patchClientInstance(clientInstance: any) {
if (clientInstance._isPatched) return clientInstance; if (clientInstance._isPatched) return clientInstance;
const originalCall = clientInstance.call.bind(clientInstance); const originalCall = clientInstance.call;
clientInstance.call = async function (...args: any[]) { clientInstance.call = async function (method: string, url: any, headers: any = {}, params: any = {}, responseType: string = 'json') {
// Suppress Appwrite version mismatch warnings during this call
const oldWarn = console.warn; const oldWarn = console.warn;
console.warn = (...warnArgs: any[]) => { console.warn = (...warnArgs: any[]) => {
if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return; if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return;
oldWarn(...warnArgs); oldWarn(...warnArgs);
}; };
try { try {
const res = await originalCall(...args); let urlStr = url instanceof URL ? url.toString() : String(url);
// Ensure all objects are fully plain JSON (strips getters/prototypes that break Server -> Client transitions) 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') { 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; return res;
} finally { } finally {
@ -26,10 +50,12 @@ function patchClientInstance(clientInstance: any) {
return clientInstance; return clientInstance;
} }
patchClientInstance(Client.prototype);
const client = new Client(); const client = new Client();
export const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; 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 || '69b84df5003bd8957c5c'; export const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
export const databaseId = process.env.APPWRITE_DATABASE_ID || 'almeira'; export const databaseId = process.env.APPWRITE_DATABASE_ID || 'almeira';
client client

View File

@ -1,10 +1,12 @@
const { Client, Databases, Query } = require('node-appwrite'); const { Client, Databases, Query } = require('node-appwrite');
require('dotenv').config({ path: '.env.local' }); require('dotenv').config({ path: '.env.local' });
const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim');
const client = new Client() const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY); .setKey(process.env.APPWRITE_API_KEY);
applyTablesDbShim(client);
const databases = new Databases(client); const databases = new Databases(client);
const DB_ID = process.env.APPWRITE_DATABASE_ID; const DB_ID = process.env.APPWRITE_DATABASE_ID;