243 lines
10 KiB
TypeScript
243 lines
10 KiB
TypeScript
import { APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
|
|
import { createAdminClient, getLoggedInUser } 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';
|
|
import { ErrorBoundary } from '@/components/publications/ErrorBoundary';
|
|
import { saveEntityAction } from '@/app/actions/entities';
|
|
import type { Post, AuthUser } from '@/types/publications';
|
|
import { PublicationsList } from '@/components/publications/PublicationsList';
|
|
|
|
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: 'asc' | 'desc' = sParams.dir === 'asc' ? 'asc' : 'desc';
|
|
const limit: number = entity === 'posts' ? 12 : 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">← Volver al inicio</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const user = JSON.parse(JSON.stringify(await getLoggedInUser()));
|
|
const prefs = await getUserPreferences();
|
|
const hiddenColumns = prefs.hiddenColumns?.[entity] || [];
|
|
|
|
let documents: any[] = [];
|
|
let total = 0;
|
|
let relationData: Record<string, any[]> = {};
|
|
|
|
try {
|
|
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 (q) {
|
|
const searchableFields = fieldsConfig
|
|
.filter((f: any) => f.type === 'text' || f.type === 'textarea')
|
|
.map((f: any) => f.name)
|
|
.slice(0, 2);
|
|
|
|
if (searchableFields.length > 0) {
|
|
queries.push(Query.contains(searchableFields[0], q));
|
|
}
|
|
}
|
|
|
|
const response = await client.databases.listDocuments(
|
|
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
|
collectionId,
|
|
queries
|
|
);
|
|
|
|
documents = JSON.parse(JSON.stringify(response.documents));
|
|
total = response.total;
|
|
|
|
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;
|
|
|
|
const ids = Array.from(new Set(
|
|
response.documents
|
|
.map(d => typeof d[field.name] === 'object' && d[field.name] !== null ? d[field.name].$id : d[field.name])
|
|
.filter(id => typeof id === 'string' && id.length > 0)
|
|
));
|
|
|
|
if (ids.length === 0) {
|
|
relationData[field.name] = [];
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const relRes = await client.databases.listDocuments(
|
|
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
|
relColId,
|
|
[Query.equal('$id', ids), Query.limit(100)]
|
|
);
|
|
relationData[field.name] = JSON.parse(JSON.stringify(relRes.documents));
|
|
} catch { }
|
|
})
|
|
);
|
|
|
|
// Enrich documents with resolved relation data for DataTable
|
|
documents = documents.map(doc => {
|
|
const enriched = { ...doc };
|
|
fields.forEach(field => {
|
|
if (field.type === 'relation' && typeof enriched[field.name] === 'string' && relationData[field.name]) {
|
|
const relDoc = relationData[field.name].find(r => r.$id === enriched[field.name]);
|
|
if (relDoc) {
|
|
enriched[field.name] = relDoc;
|
|
}
|
|
}
|
|
});
|
|
return enriched;
|
|
});
|
|
documents = JSON.parse(JSON.stringify(documents));
|
|
} catch (error) {
|
|
console.error(`Error fetching entity ${entity}:`, error);
|
|
}
|
|
|
|
const boundEditAction = async (id: string, formData: FormData) => {
|
|
'use server';
|
|
const { updateEntityAction } = await import('@/app/actions/entities');
|
|
return updateEntityAction(entity, id, formData);
|
|
};
|
|
|
|
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>
|
|
|
|
{entity === 'posts' ? (
|
|
<ErrorBoundary>
|
|
<PublicationsList
|
|
key={`list-${entity}-${page}-${q}-${sort}-${dir}`}
|
|
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}
|
|
/>
|
|
)}
|
|
|
|
{total > limit && (() => {
|
|
const totalPages = Math.ceil(total / limit);
|
|
return (
|
|
<div className="flex justify-center items-center gap-2 mt-8 flex-wrap">
|
|
<Link
|
|
href={`/dashboard/${entity}?page=1${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
|
className={`px-4 py-2 rounded-xl font-bold text-xs 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'}`}
|
|
title="Ir a la primera página"
|
|
>
|
|
« INICIO
|
|
</Link>
|
|
<Link
|
|
href={`/dashboard/${entity}?page=${page - 1}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
|
className={`px-5 py-2 rounded-xl 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-[10px] md:text-xs uppercase text-center px-2 md:px-4 bg-slate-900 py-2 rounded-xl border border-slate-800">
|
|
Página {page} de {totalPages}
|
|
</span>
|
|
|
|
<Link
|
|
href={`/dashboard/${entity}?page=${page + 1}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
|
className={`px-5 py-2 rounded-xl font-bold text-sm transition-all border border-slate-800 ${page >= totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
|
|
>
|
|
SIGUIENTE
|
|
</Link>
|
|
<Link
|
|
href={`/dashboard/${entity}?page=${totalPages}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
|
className={`px-4 py-2 rounded-xl font-bold text-xs transition-all border border-slate-800 ${page >= totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
|
|
title="Ir a la última página"
|
|
>
|
|
FINAL »
|
|
</Link>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
);
|
|
}
|