import { createFileRoute, Link, useRouter } from '@tanstack/react-router' import { useMemo, useState } from 'react' import { useSuspenseQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query' import * as Icons from 'lucide-react' import { supabase } from '@/auth/supabase' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select' import { toast } from 'sonner' /* -------------------- Tipos -------------------- */ export type Facultad = { id: string nombre: string icon: string color?: string | null } /* -------------------- Query Keys & Fetchers -------------------- */ const facultadKeys = { root: ['facultades'] as const, all: () => [...facultadKeys.root, 'all'] as const, } async function fetchFacultades(): Promise { const { data, error } = await supabase .from('facultades') .select('id, nombre, icon, color') .order('nombre') if (error) throw error return (data ?? []) as Facultad[] } const facultadesOptions = () => queryOptions({ queryKey: facultadKeys.all(), queryFn: fetchFacultades, staleTime: 60_000 }) /* -------------------- Ruta -------------------- */ export const Route = createFileRoute('/_authenticated/facultades')({ component: RouteComponent, loader: async ({ context: { queryClient } }) => { await queryClient.ensureQueryData(facultadesOptions()) return null }, }) /* ----------- Paleta curada ----------- */ const PALETTE: { name: string; hex: `#${string}` }[] = [ { name: 'Indigo', hex: '#4F46E5' }, { name: 'Blue', hex: '#2563EB' }, { name: 'Sky', hex: '#0EA5E9' }, { name: 'Teal', hex: '#14B8A6' }, { name: 'Emerald', hex: '#10B981' }, { name: 'Lime', hex: '#84CC16' }, { name: 'Amber', hex: '#F59E0B' }, { name: 'Orange', hex: '#F97316' }, { name: 'Red', hex: '#EF4444' }, { name: 'Rose', hex: '#F43F5E' }, { name: 'Violet', hex: '#7C3AED' }, { name: 'Purple', hex: '#9333EA' }, { name: 'Fuchsia', hex: '#C026D3' }, { name: 'Slate', hex: '#334155' }, { name: 'Zinc', hex: '#3F3F46' }, { name: 'Neutral', hex: '#404040' }, ] /* Un set corto y útil de íconos Lucide */ const ICON_CHOICES = [ 'Building2', 'Building', 'School', 'University', 'Landmark', 'Library', 'Layers', 'Atom', 'FlaskConical', 'Microscope', 'Cpu', 'Hammer', 'Palette', 'Shapes', 'BookOpen', 'GraduationCap', ] as const export type IconName = (typeof ICON_CHOICES)[number] function gradientFrom(color?: string | null) { const base = color && /^#([0-9a-f]{6}|[0-9a-f]{3})$/i.test(color) ? color : '#2563eb' return `linear-gradient(135deg, ${base} 0%, ${base}CC 40%, ${base}99 70%, ${base}66 100%)` } /* -------------------- Página -------------------- */ function RouteComponent() { const router = useRouter() const qc = useQueryClient() const { data: facultades } = useSuspenseQuery(facultadesOptions()) const [createOpen, setCreateOpen] = useState(false) const [editOpen, setEditOpen] = useState(false) const [form, setForm] = useState<{ nombre: string; icon: IconName; color: `#${string}` }>( { nombre: '', icon: 'Building2', color: '#2563EB' }, ) const [editing, setEditing] = useState(null) /* --------- Mutations (create / update) --------- */ const createFacultad = useMutation({ mutationFn: async (payload: { nombre: string; icon: IconName; color: `#${string}` }) => { const { error } = await supabase.from('facultades').insert(payload) if (error) throw new Error(error.message) }, onSuccess: async () => { toast.success('Facultad creada ✨') setCreateOpen(false) await qc.invalidateQueries({ queryKey: facultadKeys.root }) router.invalidate() }, onError: (e: any) => toast.error(e?.message || 'No se pudo crear'), }) const updateFacultad = useMutation({ mutationFn: async ({ id, ...payload }: { id: string; nombre: string; icon: IconName; color: `#${string}` }) => { const { error } = await supabase.from('facultades').update(payload).eq('id', id) if (error) throw new Error(error.message) }, onSuccess: async () => { toast.success('Cambios guardados ✅') setEditOpen(false) setEditing(null) await qc.invalidateQueries({ queryKey: facultadKeys.root }) router.invalidate() }, onError: (e: any) => toast.error(e?.message || 'No se pudo guardar'), }) function openCreate() { setForm({ nombre: '', icon: 'Building2', color: '#2563EB' }) setCreateOpen(true) } function openEdit(f: Facultad) { setEditing(f) setForm({ nombre: f.nombre, icon: (ICON_CHOICES.includes(f.icon as IconName) ? (f.icon as IconName) : 'Building2'), color: f.color && /^#([0-9a-f]{6}|[0-9a-f]{3})$/i.test(f.color) ? (f.color as `#${string}`) : '#2563EB', }) setEditOpen(true) } return (
{/* Header */}

Facultades

{/* Grid */}
{facultades.map((fac) => { const LucideIcon = (Icons as any)[fac.icon] || Icons.Building2 const bg = { background: gradientFrom(fac.color) } return (

{fac.nombre}

) })}
{/* Dialog Crear */} Nueva facultad {/* Dialog Editar */} { setEditOpen(o); if (!o) setEditing(null) }}> Editar facultad
) } /* ----------- Subcomponentes ----------- */ function FormFields({ form, setForm, }: { form: { nombre: string; icon: IconName; color: `#${string}` } setForm: React.Dispatch> }) { const PreviewIcon = (Icons as any)[form.icon] || Icons.Building2 const bg = useMemo(() => ({ background: gradientFrom(form.color) }), [form.color]) return (
{/* Preview */}
Vista previa
{/* Nombre */}
setForm((s) => ({ ...s, nombre: e.target.value }))} placeholder="Facultad de Ingeniería" />
{/* Icono */}
{/* Color (paleta curada) */}
setForm((s) => ({ ...s, color: hex }))} />
) } function ColorGrid({ value, onChange }: { value: `#${string}`; onChange: (hex: `#${string}`) => void }) { return (
{PALETTE.map((c) => ( ))}
) }