From 3c169eab97f1fa3c1d3d251c60be9b9201fbfc4c Mon Sep 17 00:00:00 2001 From: "Carlos (Almeira)" Date: Sun, 23 Aug 2026 08:34:18 -0500 Subject: [PATCH] feat: add publication filters and sorting options --- src/app/dashboard/[entity]/page.tsx | 136 +++++++--- .../publications/PublicationsList.tsx | 240 +++++++++++++----- 2 files changed, 272 insertions(+), 104 deletions(-) diff --git a/src/app/dashboard/[entity]/page.tsx b/src/app/dashboard/[entity]/page.tsx index 0026317..f7b2f79 100644 --- a/src/app/dashboard/[entity]/page.tsx +++ b/src/app/dashboard/[entity]/page.tsx @@ -15,13 +15,14 @@ export default async function EntityPage({ searchParams, }: { params: Promise<{ entity: string }>; - searchParams: Promise<{ q?: string; page?: string; sort?: string; dir?: string }>; + searchParams: Promise<{ q?: string; page?: string; sort?: string; dir?: string; year?: string; month?: string; status?: 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 defaultSort = entity === 'posts' ? 'fecha_publicacion' : '$createdAt'; + const sort = sParams.sort || defaultSort; const dir: 'asc' | 'desc' = sParams.dir === 'asc' ? 'asc' : 'desc'; const limit: number = entity === 'posts' ? 12 : 15; const offset = (page - 1) * limit; @@ -73,46 +74,81 @@ export default async function EntityPage({ } } + if (entity === 'posts') { + const status = sParams.status || ''; + 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)); + } + } + + const year = sParams.year ? Number(sParams.year) : null; + const month = sParams.month ? Number(sParams.month) : null; + + if (year || month) { + const filterYear = year || new Date().getFullYear(); + if (month) { + const startDate = new Date(Date.UTC(filterYear, month - 1, 1, 0, 0, 0)).toISOString(); + const endDate = new Date(Date.UTC(filterYear, month, 1, 0, 0, 0)).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, 0, 0, 0)).toISOString(); + const endDate = new Date(Date.UTC(filterYear + 1, 0, 1, 0, 0, 0)).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, 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; + 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) as string[] - )); + 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) as string[] + )); - if (ids.length === 0) { - relationData[field.name] = []; - return; - } + 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 { } + }) + ); - 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 }; @@ -137,6 +173,18 @@ export default async function EntityPage({ return updateEntityAction(entity, id, formData); }; + const buildQueryString = (p: number) => { + const paramsObj = new URLSearchParams(); + paramsObj.set('page', p.toString()); + if (q) paramsObj.set('q', q); + if (sort) paramsObj.set('sort', sort); + if (dir) paramsObj.set('dir', dir); + if (sParams.year) paramsObj.set('year', sParams.year); + if (sParams.month) paramsObj.set('month', sParams.month); + if (sParams.status) paramsObj.set('status', sParams.status); + return paramsObj.toString(); + }; + return (
@@ -149,7 +197,7 @@ export default async function EntityPage({

-
+ + {sort && } + {dir && } + {sParams.year && } + {sParams.month && } + {sParams.status && }
@@ -173,7 +226,7 @@ export default async function EntityPage({ {entity === 'posts' ? ( ) : ( @@ -204,31 +262,31 @@ export default async function EntityPage({ return (
« INICIO ANTERIOR - + Página {page} de {totalPages} - + = totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`} > SIGUIENTE = 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" > diff --git a/src/components/publications/PublicationsList.tsx b/src/components/publications/PublicationsList.tsx index 92ae738..9c6ac52 100644 --- a/src/components/publications/PublicationsList.tsx +++ b/src/components/publications/PublicationsList.tsx @@ -20,7 +20,7 @@ import React, { useState, useCallback, useMemo, useTransition } from 'react'; import { useRouter } from 'next/navigation'; -import { Search, Plus, Filter, Loader2, FileText } from 'lucide-react'; +import { Search, Plus, Filter, Loader2, FileText, Calendar, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import Link from 'next/link'; import type { Post, AuthUser, UserRole } from '@/types/publications'; import PostCard from './PostCard'; @@ -43,10 +43,13 @@ interface PublicationsListProps { }; globalSettings?: Record; entity: string; + currentSort?: string; + currentDir?: 'asc' | 'desc'; + currentYear?: string; + currentMonth?: string; + currentStatus?: string; } -type SortKey = 'fecha_publicacion' | '$createdAt' | 'titulo' | 'estado'; - // ───────────────────────────────────────────── // COMPONENT // ───────────────────────────────────────────── @@ -59,6 +62,11 @@ export const PublicationsList: React.FC = ({ appwriteConfig, globalSettings, entity, + currentSort = 'fecha_publicacion', + currentDir = 'desc', + currentYear = '', + currentMonth = '', + currentStatus = 'all', }) => { const router = useRouter(); const toast = useToast(); @@ -67,40 +75,79 @@ export const PublicationsList: React.FC = ({ // Local state const [posts, setPosts] = useState(initialPosts); - const [statusFilter, setStatusFilter] = useState('all'); - const [sortBy, setSortBy] = useState('fecha_publicacion'); - const canEdit = user?.$id === 'admin-emergency' || user?.labels?.includes('admin') || user?.labels?.includes('editor'); const userRole = canEdit ? 'admin' : 'viewer'; - // ── DERIVED STATE ──────────────────────────── + // ── FILTER & SORT ACTIONS ───────────────────── + const updateFilters = useCallback((updates: { + sort?: string; + dir?: 'asc' | 'desc'; + year?: string; + month?: string; + status?: string; + page?: number; + }) => { + const params = new URLSearchParams(window.location.search); + + // Default page to 1 on filter changes + params.set('page', (updates.page ?? 1).toString()); - const filteredAndSortedPosts = useMemo(() => { - let result = [...posts]; - - - // Filter by status - if (statusFilter !== 'all') { - result = result.filter((p) => p.estado === statusFilter); + if (updates.sort !== undefined) { + if (updates.sort) params.set('sort', updates.sort); + else params.delete('sort'); + } + if (updates.dir !== undefined) { + if (updates.dir) params.set('dir', updates.dir); + else params.delete('dir'); + } + if (updates.year !== undefined) { + if (updates.year) params.set('year', updates.year); + else params.delete('year'); + } + if (updates.month !== undefined) { + if (updates.month) params.set('month', updates.month); + else params.delete('month'); + } + if (updates.status !== undefined) { + if (updates.status && updates.status !== 'all') params.set('status', updates.status); + else params.delete('status'); } - // Sort - result.sort((a, b) => { - if (sortBy === 'titulo') return a.titulo.localeCompare(b.titulo); - if (sortBy === 'estado') return (a.estado || '').localeCompare(b.estado || ''); - const aDate = new Date(a[sortBy] || a.fecha_publicacion || a.$createdAt || 0).getTime(); - const bDate = new Date(b[sortBy] || b.fecha_publicacion || b.$createdAt || 0).getTime(); - return bDate - aDate; // Strictly Newest first - }); + router.push(`/dashboard/${entity}?${params.toString()}`); + }, [entity, router]); - return result; - }, [posts, statusFilter, sortBy]); + const years = useMemo(() => { + const currentYear = new Date().getFullYear(); + const startYear = 2024; + const list = []; + for (let y = currentYear; y >= startYear; y--) { + list.push(y); + } + return list; + }, []); + + const months = useMemo(() => [ + { value: '1', label: 'Enero' }, + { value: '2', label: 'Febrero' }, + { value: '3', label: 'Marzo' }, + { value: '4', label: 'Abril' }, + { value: '5', label: 'Mayo' }, + { value: '6', label: 'Junio' }, + { value: '7', label: 'Julio' }, + { value: '8', label: 'Agosto' }, + { value: '9', label: 'Septiembre' }, + { value: '10', label: 'Octubre' }, + { value: '11', label: 'Noviembre' }, + { value: '12', label: 'Diciembre' }, + ], []); + + const filteredAndSortedPosts = posts; const stats = useMemo(() => ({ total: posts.length, - published: posts.filter((p) => p.estado === 'publicado').length, - draft: posts.filter((p) => p.estado === 'borrador').length, - pending: posts.filter((p) => p.estado === 'pendiente').length, + published: posts.filter((p) => (p.estado || '').toLowerCase() === 'publicado').length, + draft: posts.filter((p) => (p.estado || '').toLowerCase() === 'borrador').length, + pending: posts.filter((p) => (p.estado || '').toLowerCase() === 'pendiente').length, }), [posts]); // ── HANDLERS ──────────────────────────────── @@ -153,50 +200,113 @@ export const PublicationsList: React.FC = ({
{/* Toolbar */} -
+
+
+ {/* Status Filter */} +
+ + +
+ {/* Year Filter */} +
+ + +
- {/* Status Filter */} -
- - + {/* Month Filter */} +
+ + +
- {/* Sort */} - +
+ {/* Sort By Field */} +
+ + +
- {/* New post button */} - - updateFilters({ dir: currentDir === 'asc' ? 'desc' : 'asc' })} + className="flex items-center gap-2 bg-slate-950/50 border border-white/5 hover:border-cyan-500/50 text-white text-sm px-4 py-2.5 rounded-xl transition-all active:scale-95 cursor-pointer" + title={currentDir === 'asc' ? 'Orden Ascendente' : 'Orden Descendente'} > - - Nueva publicación - - + {currentDir === 'asc' ? ( + <> + + Ascendente + + ) : ( + <> + + Descendente + + )} + + + {/* Clear Filters Button (only show if any filter is active) */} + {(currentStatus !== 'all' || currentYear || currentMonth) && ( + + )} + + {/* New post button */} + + + + Nueva publicación + + +
{/* Grid de publicaciones */}