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