almeira-web/src/components/dashboard/DataTable.tsx

177 lines
6.7 KiB
TypeScript

'use client';
import { useState, useTransition } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Pencil, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
import Link from 'next/link';
import { DeleteButton } from '@/components/DeleteButton';
import { ColumnFilter } from './ColumnFilter';
import { FieldDefinition } from '@/lib/schema';
import { updateUserPreferencesAction } from '@/app/actions/user';
interface DataTableProps {
entity: string;
collectionId: string;
documents: any[];
fieldsConfig: FieldDefinition[];
initialHiddenColumns: string[];
currentSortField: string;
currentSortDir: 'asc' | 'desc';
user: any;
}
export const DataTable = ({
entity,
collectionId,
documents,
fieldsConfig,
initialHiddenColumns,
currentSortField,
currentSortDir,
user
}: DataTableProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => {
const allFields = fieldsConfig.map(f => f.name);
return allFields.filter(f => !initialHiddenColumns.includes(f));
});
const handleColumnChange = async (newVisible: string[]) => {
setVisibleColumns(newVisible);
// Calculate hidden columns to persist
const allFields = fieldsConfig.map(f => f.name);
const hidden = allFields.filter(f => !newVisible.includes(f));
// Persist to Appwrite Prefs
await updateUserPreferencesAction({
hiddenColumns: {
[entity]: hidden
}
});
};
const toggleSort = (field: string) => {
const params = new URLSearchParams(searchParams.toString());
let newDir: 'asc' | 'desc' = 'desc';
if (currentSortField === field) {
newDir = currentSortDir === 'desc' ? 'asc' : 'desc';
}
params.set('sort', field);
params.set('dir', newDir);
startTransition(() => {
router.push(`?${params.toString()}`);
});
};
const getHeaderLabel = (key: string) => {
const field = fieldsConfig.find(f => f.name === key);
return field ? field.label : key.replace(/_/g, ' ');
};
const renderSortIcon = (field: string) => {
if (currentSortField !== field) return <ArrowUpDown size={14} className="opacity-20 group-hover:opacity-100 transition-opacity" />;
return currentSortDir === 'asc' ? <ArrowUp size={14} className="text-cyan-400" /> : <ArrowDown size={14} className="text-cyan-400" />;
};
return (
<div className="space-y-4">
<div className="flex justify-end pr-4">
<ColumnFilter
fields={fieldsConfig}
visibleColumns={visibleColumns}
onChange={handleColumnChange}
/>
</div>
<div className="bg-slate-900/50 backdrop-blur-xl border border-slate-800 rounded-2xl overflow-hidden shadow-2xl relative">
{isPending && (
<div className="absolute inset-0 bg-slate-950/20 backdrop-blur-[1px] z-50 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
</div>
)}
<div className="overflow-x-auto p-4 custom-scrollbar">
<table className="w-full text-left text-sm whitespace-nowrap">
<thead className="bg-slate-950 border-b border-slate-800 text-cyan-500 uppercase text-xs font-black tracking-widest">
<tr>
{fieldsConfig.map(field => {
if (!visibleColumns.includes(field.name)) return null;
return (
<th
key={field.name}
className="px-6 py-5 cursor-pointer group select-none hover:bg-slate-900/50 transition-colors"
onClick={() => toggleSort(field.name)}
>
<div className="flex items-center gap-2">
{field.label}
{renderSortIcon(field.name)}
</div>
</th>
);
})}
<th className="px-6 py-5 text-center">Acciones</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{documents.map((doc) => {
const canEdit = user?.$id === 'admin-emergency' || user?.labels?.includes('admin') || user?.labels?.includes('editor');
return (
<tr
key={doc.$id}
className={`transition-colors group ${canEdit ? 'hover:bg-cyan-500/5 cursor-pointer' : 'hover:bg-slate-800/50'}`}
onClick={() => {
if (canEdit) {
router.push(`/dashboard/${entity}/${doc.$id}`);
}
}}
>
{fieldsConfig.map(field => {
if (!visibleColumns.includes(field.name)) return null;
let value = doc[field.name];
// Resolve Relations
if (typeof value === 'object' && value !== null) {
if (field.type === 'relation' && field.relationLabel) {
value = value[field.relationLabel] || value.$id || '[Vinculado]';
} else {
value = Array.isArray(value) ? `[Lista (${value.length})]` : value.$id || '[Vinculado]';
}
}
if (typeof value === 'boolean') {
value = value ? 'Sí' : 'No';
}
if (typeof value === 'string' && value.length > 60) {
value = value.substring(0, 60) + '...';
}
return (
<td key={field.name} className="px-6 py-4 text-slate-300 font-medium group-hover:text-white transition-colors">
{String(value || '')}
</td>
)
})}
<td className="px-6 py-4 text-center space-x-2 w-24">
<Link href={`/dashboard/${entity}/${doc.$id}`} className="inline-flex p-2 text-slate-400 hover:text-cyan-400 hover:bg-cyan-500/10 rounded-lg transition-colors border border-transparent hover:border-cyan-500/20" title="Editar Registro">
<Pencil size={16} />
</Link>
<DeleteButton collectionId={collectionId} documentId={doc.$id} entityAlias={entity} />
</td>
</tr>
)})}
</tbody>
</table>
</div>
</div>
</div>
);
};