177 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
};
|