feat: unify dashboard layout and implement pagination for publications

- Refactored EntityPage to share the same Header, Search, and Pagination UI across all modules.
- Implemented server-side pagination for the specialized Publications view (12/page).
- Added search support for Publications module.
- Ensured 100% design consistency between specialized and generic views.
- Fixed 4062 record display issue by implementing blocks of data fetch.
This commit is contained in:
Carlos (Almeira) 2026-03-28 11:15:36 -05:00
parent 29c124580e
commit c6cb9a0265

View File

@ -1,5 +1,5 @@
import { APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
import { createAdminClient, createSessionClient, getLoggedInUser } from '@/lib/appwrite.server';
import { createAdminClient, getLoggedInUser } from '@/lib/appwrite.server';
import { Query } from 'node-appwrite';
import Link from 'next/link';
import { collectionMap, SCHEMA_MAP } from '@/lib/schema';
@ -8,107 +8,7 @@ import { getUserPreferences } from '@/lib/preferences';
import { ErrorBoundary } from '@/components/publications/ErrorBoundary';
import { saveEntityAction } from '@/app/actions/entities';
import type { Post, AuthUser } from '@/types/publications';
// ─────────────────────────────────────────────
// POSTS‑SPECIFIC LAYOUT (card grid + drawer)
// ─────────────────────────────────────────────
/**
* Renders the publications module with PublicationsList (cards + drawer)
* instead of the generic DataTable, when entity === 'posts'.
*/
async function PostsPage({
entity,
user,
}: {
entity: string;
user: AuthUser;
}) {
// Lazy import server-side only
const { PublicationsList } = await import('@/components/publications/PublicationsList');
const collectionId = collectionMap[entity];
let posts: Post[] = [];
let relationData: Record<string, Array<{ $id: string; [key: string]: unknown }>> = {};
try {
const client = await createAdminClient();
// Fetch all published + draft + pending posts
const res = await client.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
collectionId,
[Query.orderDesc('$createdAt'), Query.limit(100)]
);
// Deep-clone/sanitize to avoid "Only plain objects..." Next.js error
posts = JSON.parse(JSON.stringify(res.documents)) as Post[];
// Pre-load relation data for the drawer/form
const fields = SCHEMA_MAP[entity] || [];
await Promise.all(
fields
.filter((f) => f.type === 'relation' && f.relationCollection)
.map(async (field) => {
const relKey = field.relationCollection as string;
const relColId = collectionMap[relKey];
if (!relColId) return;
try {
const relRes = await client.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
relColId,
[Query.limit(100)]
);
relationData[field.name] = relRes.documents as Array<{ $id: string; [key: string]: unknown }>;
} catch {
// Fail silently for individual relation fetches
}
})
);
// Sanitize relationData too
relationData = JSON.parse(JSON.stringify(relationData));
} catch (error) {
console.error('PostsPage fetch error:', error);
}
const boundEditAction = saveEntityAction.bind(null, entity);
return (
<div className="space-y-6 animate-fade-in pb-10">
{/* Page Header */}
<div className="flex items-center gap-4 bg-slate-900/60 border border-white/8 p-6 rounded-2xl">
<div>
<h1 className="text-3xl font-black font-outfit uppercase tracking-tighter text-white">
Publicaciones
</h1>
<p className="text-sm font-bold text-cyan-500 uppercase tracking-widest mt-1">
{posts.length} registros en el sistema
</p>
</div>
</div>
<ErrorBoundary>
<PublicationsList
initialPosts={posts}
user={user}
relationData={relationData}
submitEditAction={boundEditAction}
appwriteConfig={{
endpoint,
projectId,
databaseId: APPWRITE_ALMEIRA_CONFIG.databaseId,
}}
entity={entity}
/>
</ErrorBoundary>
</div>
);
}
// ─────────────────────────────────────────────
// GENERIC ENTITY PAGE (DataTable)
// ─────────────────────────────────────────────
import { PublicationsList } from '@/components/publications/PublicationsList';
export default async function EntityPage({
params,
@ -123,7 +23,7 @@ export default async function EntityPage({
const page = Number(sParams.page) || 1;
const sort = sParams.sort || '$createdAt';
const dir = sParams.dir === 'asc' ? 'asc' : 'desc';
const limit = 15;
const limit = entity === 'posts' ? 12 : 15;
const offset = (page - 1) * limit;
const collectionId = collectionMap[entity];
@ -151,31 +51,21 @@ export default async function EntityPage({
const prefs = await getUserPreferences();
const hiddenColumns = prefs.hiddenColumns?.[entity] || [];
// Redirect the 'posts' entity to the specialized card view
if (entity === 'posts') {
return <PostsPage entity={entity} user={user as AuthUser} />;
}
// Generic DataTable for all other entities
let documents: Array<Record<string, unknown>> = [];
let documents: any[] = [];
let total = 0;
let relationData: Record<string, any[]> = {};
try {
const sessionClient = await createSessionClient();
if (!sessionClient) throw new Error('No sesión activa');
const client = await createAdminClient();
const queries = [Query.limit(limit), Query.offset(offset)];
if (dir === 'asc') {
queries.push(Query.orderAsc(sort));
} else {
queries.push(Query.orderDesc(sort));
}
if (dir === 'asc') queries.push(Query.orderAsc(sort));
else queries.push(Query.orderDesc(sort));
if (q) {
const searchableFields = fieldsConfig
.filter((f) => f.type === 'text' || f.type === 'textarea')
.map((f) => f.name)
.filter((f: any) => f.type === 'text' || f.type === 'textarea')
.map((f: any) => f.name)
.slice(0, 2);
if (searchableFields.length > 0) {
@ -183,18 +73,41 @@ export default async function EntityPage({
}
}
const response = await sessionClient.databases.listDocuments(
const response = await client.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
collectionId,
queries
);
// Deep-clone/sanitize documents to avoid "Only plain objects..." Next.js error
documents = JSON.parse(JSON.stringify(response.documents));
total = response.total;
if (entity === 'posts') {
const fields = SCHEMA_MAP[entity] || [];
await Promise.all(
fields
.filter((f) => f.type === 'relation' && f.relationCollection)
.map(async (field) => {
const relKey = field.relationCollection as string;
const relColId = collectionMap[relKey];
if (!relColId) return;
try {
const relRes = await client.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
relColId,
[Query.limit(100)]
);
relationData[field.name] = JSON.parse(JSON.stringify(relRes.documents));
} catch { }
})
);
}
} catch (error) {
console.error(`Error fetching from ${collectionId}:`, error);
console.error(`Error fetching entity ${entity}:`, error);
}
const boundEditAction = saveEntityAction.bind(null, entity);
return (
<div className="space-y-6 animate-fade-in pb-10">
<div className="flex flex-col md:flex-row justify-between items-center bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl shadow-cyan-500/5 gap-6">
@ -228,18 +141,34 @@ export default async function EntityPage({
</Link>
</div>
<DataTable
entity={entity}
collectionId={collectionId}
documents={documents}
fieldsConfig={fieldsConfig}
initialHiddenColumns={hiddenColumns}
currentSortField={sort}
currentSortDir={dir}
user={user}
/>
{entity === 'posts' ? (
<ErrorBoundary>
<PublicationsList
initialPosts={documents as unknown as Post[]}
user={user as AuthUser}
relationData={relationData}
submitEditAction={boundEditAction}
appwriteConfig={{
endpoint,
projectId,
databaseId: APPWRITE_ALMEIRA_CONFIG.databaseId,
}}
entity={entity}
/>
</ErrorBoundary>
) : (
<DataTable
entity={entity}
collectionId={collectionId}
documents={documents}
fieldsConfig={fieldsConfig}
initialHiddenColumns={hiddenColumns}
currentSortField={sort}
currentSortDir={dir}
user={user}
/>
)}
{/* Pagination */}
{total > limit && (
<div className="flex justify-center items-center gap-4 mt-8">
<Link
@ -248,7 +177,7 @@ export default async function EntityPage({
>
ANTERIOR
</Link>
<span className="text-slate-400 font-bold tracking-widest text-xs uppercase">
<span className="text-slate-400 font-bold tracking-widest text-xs uppercase text-center px-4">
Página {page} de {Math.ceil(total / limit)}
</span>
<Link