feat(almeira): migrate to Appwrite Cloud (nyc.cloud.appwrite.io) with TablesDB shim and full WP/Cauca/Multivision restoration
This commit is contained in:
parent
483090aa1e
commit
6c5037d07d
@ -7,10 +7,12 @@ require('dotenv').config({ path: '.env.local' });
|
||||
let InputFile;
|
||||
try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; }
|
||||
|
||||
const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim');
|
||||
const client = new Client()
|
||||
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
|
||||
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
|
||||
.setKey(process.env.APPWRITE_API_KEY);
|
||||
applyTablesDbShim(client);
|
||||
|
||||
const databases = new Databases(client);
|
||||
const storage = new Storage(client);
|
||||
|
||||
@ -8,10 +8,12 @@ require('dotenv').config({ path: '.env.local' });
|
||||
let InputFile;
|
||||
try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; }
|
||||
|
||||
const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim');
|
||||
const client = new Client()
|
||||
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
|
||||
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
|
||||
.setKey(process.env.APPWRITE_API_KEY);
|
||||
applyTablesDbShim(client);
|
||||
|
||||
const databases = new Databases(client);
|
||||
const storage = new Storage(client);
|
||||
@ -246,8 +248,9 @@ async function run() {
|
||||
const doc = {
|
||||
titulo: data.title.substring(0, 100),
|
||||
slug: `multivision-final-${Date.now()}-${count}`,
|
||||
tipo: 'noticia',
|
||||
tipo_publicacion_id: typeId,
|
||||
estado: 'Publicado',
|
||||
estado: 'publicado',
|
||||
fecha_publicacion: data.date,
|
||||
contenido: data.content,
|
||||
resumen_redes: data.resumen,
|
||||
|
||||
283
scripts/extract_wordpress_sql.js
Normal file
283
scripts/extract_wordpress_sql.js
Normal 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);
|
||||
});
|
||||
|
||||
204
scripts/extract_wp_images_from_tar.js
Normal file
204
scripts/extract_wp_images_from_tar.js
Normal 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);
|
||||
});
|
||||
|
||||
226
scripts/import_wp_full_to_appwrite.js
Normal file
226
scripts/import_wp_full_to_appwrite.js
Normal 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(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/“|”/gi, '"')
|
||||
.replace(/‘|’/gi, "'")
|
||||
.replace(/–|—/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);
|
||||
});
|
||||
|
||||
338
scripts/publish_cauca_sept17_to_date.js
Normal file
338
scripts/publish_cauca_sept17_to_date.js
Normal 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();
|
||||
430
scripts/rebuild_almeira_cloud.js
Normal file
430
scripts/rebuild_almeira_cloud.js
Normal 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);
|
||||
});
|
||||
@ -168,8 +168,8 @@ export async function generateMetadata(
|
||||
? decodeHtmlEntities(post.resumen_redes)
|
||||
: (post.contenido?.replace(/<[^>]*>?/gm, '').substring(0, 160) + '...');
|
||||
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
let imagenUrl = post.og_image_url || null;
|
||||
|
||||
if (post.imagen_destacada_id) {
|
||||
@ -219,8 +219,8 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug
|
||||
);
|
||||
}
|
||||
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
let imagenUrl = post.og_image_url || null;
|
||||
const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' ||
|
||||
(typeof post.tipo_publicacion_id === 'object' && (post.tipo_publicacion_id.codigo === 'EVENTO' || post.tipo_publicacion_id.codigo === 'CLASIFICADO'));
|
||||
|
||||
@ -6,8 +6,8 @@ import { patchClientInstance } from '@/lib/appwrite.server';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { InputFile } from 'node-appwrite/file';
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const DATABASE_ID = databaseId;
|
||||
|
||||
function getAdminClient() {
|
||||
|
||||
@ -4,8 +4,8 @@ import { Client, Account } from 'node-appwrite';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const COOKIE_NAME = `a_session_${PROJECT_ID.toLowerCase()}`;
|
||||
|
||||
// Password de emergencia (Configurado en Coolify)
|
||||
|
||||
@ -5,8 +5,8 @@ import { databaseId } from '@/lib/appwrite';
|
||||
import { patchClientInstance } from '@/lib/appwrite.server';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const DATABASE_ID = databaseId;
|
||||
const COLLECTION_ID = 'entity_config';
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import { Client, Databases, Users, ID, Query } from 'node-appwrite';
|
||||
|
||||
function getAdminServices() {
|
||||
const client = new Client()
|
||||
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1')
|
||||
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c')
|
||||
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1')
|
||||
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40')
|
||||
.setKey(process.env.APPWRITE_API_KEY || '');
|
||||
|
||||
return {
|
||||
|
||||
@ -33,8 +33,8 @@ async function checkAdminAccess(): Promise<boolean> {
|
||||
// Inicialización de servicios administrativos de Appwrite con la API Key del servidor
|
||||
function getAdminServices() {
|
||||
const client = new Client()
|
||||
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1')
|
||||
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c')
|
||||
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1')
|
||||
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40')
|
||||
.setKey(process.env.APPWRITE_API_KEY || '');
|
||||
|
||||
return {
|
||||
|
||||
@ -36,7 +36,7 @@ async function requireAuth() {
|
||||
try {
|
||||
// Si es modo bypass (admin-emergency), no podemos llamar a account.get() con la API Key
|
||||
const cookieStore = await cookies();
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const sessionCookie = cookieStore.get(`a_session_${PROJECT_ID.toLowerCase()}`);
|
||||
|
||||
if (sessionCookie?.value === "admin-bypass-session") {
|
||||
|
||||
@ -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`}>
|
||||
{posts.map((post: Post) => {
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
let imagenUrl = post.og_image_url || null;
|
||||
|
||||
const mediaObj = post.resolved_media || (typeof post.imagen_destacada_id === 'object' ? post.imagen_destacada_id : null) as MediaItem | null;
|
||||
|
||||
@ -69,8 +69,8 @@ export default async function EntityPage() {
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
let logoUrl = entity?.logotipo || '';
|
||||
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
|
||||
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;
|
||||
|
||||
502
src/app/dashboard/facturacion/page.tsx
Normal file
502
src/app/dashboard/facturacion/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -51,8 +51,8 @@ export default async function MediaLibraryPage({
|
||||
}
|
||||
|
||||
// Appwrite public view format for our config
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
|
||||
const getFilePreviewUrl = (bucketId: string, fileId: string) => {
|
||||
// Usamos el endpoint público de Appwrite para obtener la imagen
|
||||
|
||||
@ -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`}>
|
||||
{posts.map((post: Post) => {
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
let imagenUrl = post.og_image_url || null;
|
||||
|
||||
const mediaObj = post.resolved_media || (typeof post.imagen_destacada_id === 'object' ? post.imagen_destacada_id : null) as MediaItem | null;
|
||||
|
||||
@ -12,8 +12,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
const entity = await getEntity();
|
||||
const razonSocial = entity?.razon_social || "Almeira Producciones";
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
let logoUrl = entity?.logotipo || null;
|
||||
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
|
||||
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;
|
||||
|
||||
@ -5,8 +5,8 @@ import { Radio } from 'lucide-react';
|
||||
export default async function LoginPage() {
|
||||
const entity = await getEntity();
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
let logoUrl = entity?.logotipo || null;
|
||||
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
|
||||
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;
|
||||
|
||||
@ -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`}>
|
||||
{posts.map((post: Post) => {
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
let imagenUrl = post.og_image_url || null;
|
||||
|
||||
const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' ||
|
||||
|
||||
@ -44,8 +44,8 @@ function ClientCarouselGroup({ eslogan, ads, rotationSeconds }: { eslogan: strin
|
||||
const activeAd = ads[currentIndex];
|
||||
if (!activeAd.imagen_destacada_id) return null;
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
|
||||
let imgUrl = activeAd.og_image_url || null;
|
||||
if (activeAd.imagen_destacada_id) {
|
||||
|
||||
@ -11,8 +11,8 @@ export const Navbar = ({ entity }: { entity?: any }) => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const pathname = usePathname();
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
let logoUrl = entity?.logotipo || null;
|
||||
if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('/')) {
|
||||
logoUrl = `${ENDPOINT}/storage/buckets/media/files/${logoUrl}/view?project=${PROJECT_ID}`;
|
||||
|
||||
@ -43,6 +43,7 @@ export const Sidebar = ({ user }: { user: any }) => {
|
||||
items: [
|
||||
{ name: 'Contratos', href: '/dashboard/contracts', icon: FileText },
|
||||
{ name: 'Formas de Pago', href: '/dashboard/payment_methods', icon: BadgeDollarSign },
|
||||
{ name: 'Facturación Electrónica', href: '/dashboard/facturacion', icon: BadgeDollarSign },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@ -26,8 +26,8 @@ export default function TopBanner({ initialBanners, rotationSeconds }: { initial
|
||||
|
||||
if (!activeBanner.imagen_destacada_id && !activeBanner.og_image_url) return null;
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
|
||||
let imgUrl = activeBanner.og_image_url || null;
|
||||
|
||||
|
||||
@ -183,8 +183,8 @@ function QuickCreatePortal({ collection, fieldName, onClose }: QuickCreatePortal
|
||||
|
||||
function MediaThumbnail({ fileId }: { fileId: string }) {
|
||||
if (!fileId) return null;
|
||||
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
|
||||
const url = `${endpoint}/storage/buckets/media/files/${fileId}/preview?project=${projectId}&width=400&height=250&quality=80`;
|
||||
|
||||
|
||||
58
src/lib/appwrite-tables-shim.js
Normal file
58
src/lib/appwrite-tables-shim.js
Normal 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 };
|
||||
|
||||
@ -1,22 +1,46 @@
|
||||
import { Client, Account, Databases, Storage } from 'node-appwrite';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
// Robust instance-level patch for Appwrite Next.js Server Components
|
||||
// Robust instance-level patch for Appwrite Next.js Server Components (supports both 1.x Databases and 2.x TablesDB)
|
||||
export function patchClientInstance(client: any) {
|
||||
if (client._isPatched) return client;
|
||||
const originalCall = client.call.bind(client);
|
||||
client.call = async function (...args: any[]) {
|
||||
// Suppress Appwrite version mismatch warnings during this call
|
||||
const originalCall = client.call;
|
||||
client.call = async function (method: string, url: any, headers: any = {}, params: any = {}, responseType: string = 'json') {
|
||||
const oldWarn = console.warn;
|
||||
console.warn = (...warnArgs: any[]) => {
|
||||
if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return;
|
||||
oldWarn(...warnArgs);
|
||||
};
|
||||
try {
|
||||
const res = await originalCall(...args);
|
||||
// Ensure all objects are fully plain JSON (strips getters/prototypes that break Server -> Client transitions)
|
||||
let urlStr = url instanceof URL ? url.toString() : String(url);
|
||||
if (/\/databases\/([^/]+)\/collections/.test(urlStr)) {
|
||||
urlStr = urlStr
|
||||
.replace(/\/databases\/([^/]+)\/collections\/([^/]+)\/documents/g, '/tablesdb/$1/tables/$2/rows')
|
||||
.replace(/\/databases\/([^/]+)\/collections/g, '/tablesdb/$1/tables');
|
||||
url = url instanceof URL ? new URL(urlStr) : urlStr;
|
||||
if (params && typeof params === 'object') {
|
||||
params = { ...params };
|
||||
if ('documentId' in params) {
|
||||
params.rowId = params.documentId;
|
||||
delete params.documentId;
|
||||
}
|
||||
if ('collectionId' in params) {
|
||||
params.tableId = params.collectionId;
|
||||
delete params.collectionId;
|
||||
}
|
||||
if ('documentSecurity' in params) {
|
||||
params.rowSecurity = params.documentSecurity;
|
||||
delete params.documentSecurity;
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await originalCall.call(this, method, url, headers, params, responseType);
|
||||
if (res && typeof res === 'object') {
|
||||
return JSON.parse(JSON.stringify(res));
|
||||
const plain = JSON.parse(JSON.stringify(res));
|
||||
if (Array.isArray(plain.rows) && !plain.documents) plain.documents = plain.rows;
|
||||
if (Array.isArray(plain.tables) && !plain.collections) plain.collections = plain.tables;
|
||||
if (Array.isArray(plain.columns) && !plain.attributes) plain.attributes = plain.columns;
|
||||
return plain;
|
||||
}
|
||||
return res;
|
||||
} finally {
|
||||
@ -27,8 +51,10 @@ export function patchClientInstance(client: any) {
|
||||
return client;
|
||||
}
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
patchClientInstance(Client.prototype);
|
||||
|
||||
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
const COOKIE_NAME = `a_session_${PROJECT_ID.toLowerCase()}`;
|
||||
|
||||
export async function createSessionClient() {
|
||||
|
||||
@ -1,21 +1,45 @@
|
||||
import { Client, Databases, Storage, Query, Account } from 'appwrite';
|
||||
|
||||
// Robust instance-level patch for Appwrite Client
|
||||
// Robust instance-level patch for Appwrite Client (supports both Appwrite 1.x Databases and 2.x TablesDB)
|
||||
function patchClientInstance(clientInstance: any) {
|
||||
if (clientInstance._isPatched) return clientInstance;
|
||||
const originalCall = clientInstance.call.bind(clientInstance);
|
||||
clientInstance.call = async function (...args: any[]) {
|
||||
// Suppress Appwrite version mismatch warnings during this call
|
||||
const originalCall = clientInstance.call;
|
||||
clientInstance.call = async function (method: string, url: any, headers: any = {}, params: any = {}, responseType: string = 'json') {
|
||||
const oldWarn = console.warn;
|
||||
console.warn = (...warnArgs: any[]) => {
|
||||
if (warnArgs[0] && typeof warnArgs[0] === 'string' && warnArgs[0].includes('built for Appwrite')) return;
|
||||
oldWarn(...warnArgs);
|
||||
};
|
||||
try {
|
||||
const res = await originalCall(...args);
|
||||
// Ensure all objects are fully plain JSON (strips getters/prototypes that break Server -> Client transitions)
|
||||
let urlStr = url instanceof URL ? url.toString() : String(url);
|
||||
if (/\/databases\/([^/]+)\/collections/.test(urlStr)) {
|
||||
urlStr = urlStr
|
||||
.replace(/\/databases\/([^/]+)\/collections\/([^/]+)\/documents/g, '/tablesdb/$1/tables/$2/rows')
|
||||
.replace(/\/databases\/([^/]+)\/collections/g, '/tablesdb/$1/tables');
|
||||
url = url instanceof URL ? new URL(urlStr) : urlStr;
|
||||
if (params && typeof params === 'object') {
|
||||
params = { ...params };
|
||||
if ('documentId' in params) {
|
||||
params.rowId = params.documentId;
|
||||
delete params.documentId;
|
||||
}
|
||||
if ('collectionId' in params) {
|
||||
params.tableId = params.collectionId;
|
||||
delete params.collectionId;
|
||||
}
|
||||
if ('documentSecurity' in params) {
|
||||
params.rowSecurity = params.documentSecurity;
|
||||
delete params.documentSecurity;
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await originalCall.call(this, method, url, headers, params, responseType);
|
||||
if (res && typeof res === 'object') {
|
||||
return JSON.parse(JSON.stringify(res));
|
||||
const plain = JSON.parse(JSON.stringify(res));
|
||||
if (Array.isArray(plain.rows) && !plain.documents) plain.documents = plain.rows;
|
||||
if (Array.isArray(plain.tables) && !plain.collections) plain.collections = plain.tables;
|
||||
if (Array.isArray(plain.columns) && !plain.attributes) plain.attributes = plain.columns;
|
||||
return plain;
|
||||
}
|
||||
return res;
|
||||
} finally {
|
||||
@ -26,10 +50,12 @@ function patchClientInstance(clientInstance: any) {
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
patchClientInstance(Client.prototype);
|
||||
|
||||
const client = new Client();
|
||||
|
||||
export const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
export const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
export const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
|
||||
export const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
|
||||
export const databaseId = process.env.APPWRITE_DATABASE_ID || 'almeira';
|
||||
|
||||
client
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
const { Client, Databases, Query } = require('node-appwrite');
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
|
||||
const { applyTablesDbShim } = require('./src/lib/appwrite-tables-shim');
|
||||
const client = new Client()
|
||||
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
|
||||
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
|
||||
.setKey(process.env.APPWRITE_API_KEY);
|
||||
applyTablesDbShim(client);
|
||||
|
||||
const databases = new Databases(client);
|
||||
const DB_ID = process.env.APPWRITE_DATABASE_ID;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user