almeira-web/src/app/api/reports/posts/route.tsx

222 lines
10 KiB
TypeScript

import { NextResponse } from 'next/server';
import { ImageResponse } from 'next/og';
import { Document, Paragraph, TextRun, Packer, ImageRun, HeadingLevel, ExternalHyperlink, AlignmentType } from 'docx';
import { createAdminClient } from '@/lib/appwrite.server';
import { Query } from 'node-appwrite';
import { APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
import { collectionMap } from '@/lib/schema';
function getAppwriteImageUrl(fileId: string, bucketId?: string, width = 400, height = 250, isVideo = false) {
if (!fileId) return '';
const finalBucketId = bucketId || APPWRITE_ALMEIRA_CONFIG.mediaBucketId;
if (isVideo) {
return `${endpoint}/storage/buckets/${finalBucketId}/files/${fileId}/view?project=${projectId}`;
}
return `${endpoint}/storage/buckets/${finalBucketId}/files/${fileId}/preview?project=${projectId}&width=${width}&height=${height}&quality=80`;
}
export const runtime = 'nodejs';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const q = searchParams.get('q') || '';
const sort = searchParams.get('sort') || 'fecha_publicacion';
const dir = searchParams.get('dir') || 'desc';
const status = searchParams.get('status') || '';
const year = searchParams.get('year');
const month = searchParams.get('month');
const client = await createAdminClient();
const postsCollectionId = collectionMap['posts'];
const mediaCollectionId = collectionMap['media'];
if (!postsCollectionId) {
return NextResponse.json({ error: 'Configuración de colección incorrecta' }, { status: 500 });
}
const queries = [Query.limit(100), Query.offset(0)];
if (dir === 'asc') queries.push(Query.orderAsc(sort));
else queries.push(Query.orderDesc(sort));
if (q) queries.push(Query.contains('titulo', q));
if (status && status !== 'all') {
if (status === 'publicado') queries.push(Query.equal('estado', ['publicado', 'Publicado']));
else if (status === 'borrador') queries.push(Query.equal('estado', ['borrador', 'Borrador']));
else if (status === 'pendiente') queries.push(Query.equal('estado', ['pendiente', 'Pendiente']));
else if (status === 'archivado') queries.push(Query.equal('estado', ['archivado', 'Archivado']));
else queries.push(Query.equal('estado', status));
}
if (year || month) {
const filterYear = year ? Number(year) : new Date().getFullYear();
if (month) {
const startDate = new Date(Date.UTC(filterYear, Number(month) - 1, 1)).toISOString();
const endDate = new Date(Date.UTC(filterYear, Number(month), 1)).toISOString();
queries.push(Query.greaterThanEqual('fecha_publicacion', startDate));
queries.push(Query.lessThan('fecha_publicacion', endDate));
} else {
const startDate = new Date(Date.UTC(filterYear, 0, 1)).toISOString();
const endDate = new Date(Date.UTC(filterYear + 1, 0, 1)).toISOString();
queries.push(Query.greaterThanEqual('fecha_publicacion', startDate));
queries.push(Query.lessThan('fecha_publicacion', endDate));
}
}
const response = await client.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
postsCollectionId,
queries
);
const posts = response.documents;
const mediaIds = Array.from(new Set(posts.map(p => typeof p.imagen_destacada_id === 'object' && p.imagen_destacada_id !== null ? p.imagen_destacada_id.$id : p.imagen_destacada_id).filter(id => typeof id === 'string' && id.length > 0) as string[]));
let mediaDocs: Record<string, any> = {};
if (mediaIds.length > 0 && mediaCollectionId) {
try {
const relRes = await client.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
mediaCollectionId,
[Query.equal('$id', mediaIds), Query.limit(100)]
);
relRes.documents.forEach(doc => {
mediaDocs[doc.$id] = doc;
});
} catch (err) {
console.error('Error resolviendo las imágenes para el reporte', err);
}
}
const docChildren: any[] = [
new Paragraph({
text: `Reporte de Publicaciones - Almeira Web`,
heading: HeadingLevel.HEADING_1,
alignment: AlignmentType.CENTER,
spacing: { after: 400 }
})
];
for (const post of posts) {
const fechaFormat = post.fecha_publicacion ? new Date(post.fecha_publicacion).toLocaleDateString('es-CO') : 'Sin fecha';
const mediaId = typeof post.imagen_destacada_id === 'object' && post.imagen_destacada_id !== null ? post.imagen_destacada_id.$id : post.imagen_destacada_id;
let imageUrl = '';
if (mediaId && mediaDocs[mediaId]) {
const mediaInfo = mediaDocs[mediaId];
if (mediaInfo.tipo === 'imagen') {
imageUrl = getAppwriteImageUrl(mediaInfo.file_id, mediaInfo.bucket_id, 600, 400, false);
}
}
let imageAdded = false;
try {
const plainTextResumen = (post.resumen_redes || post.contenido || '').replace(/<[^>]*>?/gm, ' ').replace(/\s+/g, ' ').trim();
const safeResumen = plainTextResumen.length > 120 ? plainTextResumen.substring(0, 120) + '...' : plainTextResumen;
// Use native JSX now that the file is renamed to route.tsx
const cardImage = new ImageResponse(
(
<div style={{
display: 'flex', flexDirection: 'column', width: 600, height: 600,
backgroundColor: '#0f172a', borderRadius: 24,
overflow: 'hidden', border: '1px solid rgba(255,255,255,0.1)', fontFamily: 'sans-serif'
}}>
<div style={{ display: 'flex', width: 600, height: 337, backgroundColor: '#1e293b', position: 'relative' }}>
{imageUrl ? (
<img src={imageUrl} style={{ width: 600, height: 337, objectFit: 'cover' }} />
) : (
<div style={{ width: 600, height: 337, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: '#475569', fontSize: 24, fontWeight: 900, textTransform: 'uppercase', letterSpacing: 4 }}>Almeira Web</span>
</div>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', padding: 24, flexGrow: 1 }}>
<h3 style={{ fontSize: 24, fontWeight: 'bold', margin: '0 0 12px 0', lineHeight: 1.3, color: 'white' }}>{post.titulo || 'Sin título'}</h3>
<p style={{ fontSize: 16, color: '#94a3b8', margin: 0, lineHeight: 1.6 }}>{safeResumen}</p>
</div>
</div>
),
{ width: 600, height: 600 }
);
const imgBuffer = await cardImage.arrayBuffer();
docChildren.push(
new Paragraph({
children: [
new ImageRun({
data: imgBuffer,
transformation: { width: 500, height: 333 },
type: 'png'
})
],
alignment: AlignmentType.CENTER,
spacing: { after: 200 }
})
);
imageAdded = true;
} catch (err) {
console.error('Error fetching image for Word Document', err);
}
if (!imageAdded) {
docChildren.push(
new Paragraph({
children: [
new TextRun({ text: "[No se pudo generar la tarjeta virtual]", italics: true })
],
alignment: AlignmentType.CENTER,
spacing: { after: 200 }
})
);
}
docChildren.push(
new Paragraph({
children: [
new ExternalHyperlink({
children: [
new TextRun({ text: `https://almeira.com.co/${post.slug || post.$id}`, style: "Hyperlink" }),
],
link: `https://almeira.com.co/${post.slug || post.$id}`,
})
],
spacing: { after: 400 }
})
);
docChildren.push(
new Paragraph({ text: "______________________________________________________", alignment: AlignmentType.CENTER, spacing: { after: 200 } })
);
}
if (posts.length === 0) {
docChildren.push(new Paragraph({ text: "No se encontraron publicaciones con los filtros seleccionados." }));
}
const doc = new Document({
sections: [{
properties: {},
children: docChildren,
}]
});
const buffer = await Packer.toBuffer(doc);
const dataArray = new Uint8Array(buffer);
return new NextResponse(dataArray, {
status: 200,
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': `attachment; filename=reporte_publicaciones_${new Date().toISOString().split('T')[0]}.docx`,
},
});
} catch (error: any) {
console.error('General error Word API:', error);
return NextResponse.json({ error: 'Error al generar el documento', details: error?.message || 'Error desconocido' }, { status: 500 });
}
}