almeira-web/src/app/dashboard/[entity]/page.tsx

154 lines
6.2 KiB
TypeScript

import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
import { createSessionClient } from '@/lib/appwrite.server';
import { Query } from 'node-appwrite';
import Link from 'next/link';
import { collectionMap, SCHEMA_MAP } from '@/lib/schema';
import { DataTable } from '@/components/dashboard/DataTable';
import { getUserPreferences } from '@/lib/preferences';
export default async function EntityPage({
params,
searchParams
}: {
params: Promise<{ entity: string }>,
searchParams: Promise<{ q?: string, page?: string, sort?: string, dir?: string }>
}) {
const { entity } = await params;
const sParams = await searchParams;
const q = sParams.q || '';
const page = Number(sParams.page) || 1;
const sort = sParams.sort || '$createdAt';
const dir = (sParams.dir === 'asc' || sParams.dir === 'desc') ? sParams.dir : 'desc';
const limit = 15;
const offset = (page - 1) * limit;
const collectionId = collectionMap[entity];
const fieldsConfig = SCHEMA_MAP[entity] || [];
const entityNames: Record<string, string> = {
posts: 'Publicaciones', categories: 'Categorías', clients: 'Clientes', client_types: 'Tipos de Cliente',
countries: 'Países', departments: 'Departamentos', cities: 'Ciudades', contracts: 'Contratos',
payment_methods: 'Formas de Pago', publication_types: 'Tipos de Publicación'
};
const entityName = entityNames[entity] || entity.replace(/_/g, ' ');
if (!collectionId) {
return (
<div className="p-8">
<h1 className="text-2xl font-bold text-red-500">Colección no encontrada</h1>
<p className="text-slate-400 mt-2">La ruta /dashboard/{entity} no está configurada.</p>
<Link href="/dashboard" className="text-blue-400 mt-4 inline-block">&larr; Volver al inicio</Link>
</div>
);
}
// Fetch preferences
const prefs = await getUserPreferences();
const hiddenColumns = prefs.hiddenColumns?.[entity] || [];
let documents: any[] = [];
let total = 0;
try {
const sessionClient = await createSessionClient();
if (!sessionClient) {
throw new Error("No sesión activa");
}
const queries = [
Query.limit(limit),
Query.offset(offset),
];
// Apply Sorting
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)
.slice(0, 2);
if (searchableFields.length > 0) {
queries.push(Query.contains(searchableFields[0], q));
}
}
const response = await sessionClient.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
collectionId,
queries
);
documents = response.documents;
total = response.total;
} catch (error) {
console.error(`Error fetching from ${collectionId}:`, error);
}
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">
<div>
<h1 className="text-3xl font-black capitalize font-outfit uppercase tracking-tighter text-white">
{entityName}
</h1>
<p className="text-sm font-bold tracking-widest text-cyan-500 mt-2 uppercase">
Total en sistema: <span className="text-white bg-slate-800 px-2 py-0.5 rounded-full">{total}</span>
</p>
</div>
<form className="flex-1 max-w-md w-full relative group">
<input
type="text"
name="q"
defaultValue={q}
placeholder={`Buscar en ${entityName}...`}
className="w-full bg-slate-950 border border-slate-800 rounded-full px-6 py-3 text-slate-200 focus:outline-none focus:border-cyan-500 transition-all font-medium placeholder:text-slate-600"
/>
<button type="submit" className="absolute right-2 top-2 bg-slate-800 p-1.5 rounded-full hover:bg-slate-700 transition-colors text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</button>
</form>
<Link href={`/dashboard/${entity}/new`} className="group flex items-center gap-2 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white text-sm font-bold px-6 py-3 rounded-full shadow-[0_0_20px_rgba(6,182,212,0.3)] hover:shadow-[0_0_30px_rgba(6,182,212,0.5)] transition-all hover:scale-105">
<span className="text-xl leading-none -mt-1">+</span> Nuevo
</Link>
</div>
<DataTable
entity={entity}
collectionId={collectionId}
documents={documents}
fieldsConfig={fieldsConfig}
initialHiddenColumns={hiddenColumns}
currentSortField={sort}
currentSortDir={dir}
/>
{/* Pagination */}
{total > limit && (
<div className="flex justify-center items-center gap-4 mt-8">
<Link
href={`/dashboard/${entity}?page=${page - 1}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
className={`px-6 py-2 rounded-full font-bold text-sm transition-all border border-slate-800 ${page <= 1 ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
>
ANTERIOR
</Link>
<span className="text-slate-400 font-bold tracking-widest text-xs uppercase">
Página {page} de {Math.ceil(total / limit)}
</span>
<Link
href={`/dashboard/${entity}?page=${page + 1}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
className={`px-6 py-2 rounded-full font-bold text-sm transition-all border border-slate-800 ${page >= Math.ceil(total / limit) ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
>
SIGUIENTE
</Link>
</div>
)}
</div>
);
}