Overview
The School Information module (DatosPlantel.jsx) provides a centralized interface for managing institutional details including school name, RIF, DEA code, address, director name, and institutional logo.
Desarrollador Access Only: Editing school information is restricted to users with the Desarrollador role (id_rol=4). Other users can view the information but cannot make changes.
Database Schema
School information is stored in the institucion table with a single row (id=1):
CREATE TABLE IF NOT EXISTS institucion (
id SERIAL PRIMARY KEY,
nombre TEXT,
rif TEXT,
codigo_dea TEXT,
direccion TEXT,
director_actual TEXT,
logo_url TEXT,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Loading School Data
The component loads data on mount:
const loadInstitucion = async () => {
try {
const { data, error } = await supabase
.from('institucion')
.select('*')
.eq('id', 1)
.single()
if (error) throw error
setInstitucion(data)
} catch (error) {
console.error('Error cargando datos del plantel:', error)
} finally {
setLoading(false)
}
}
Editable Fields
Institution Name
{editing ? (
<input
type="text"
name="nombre"
value={formData.nombre}
onChange={handleChange}
placeholder="Nombre de la Institución"
className="text-2xl font-heading font-bold text-center w-full bg-white border border-gray-300 rounded-lg px-4 py-2"
/>
) : (
<h1 className="text-2xl font-heading font-bold text-slate-800">
{institucion?.nombre || 'Sin nombre'}
</h1>
)}
The institution name is required. The save function validates that it’s not empty before proceeding.
RIF (Tax ID)
<div className="flex items-start gap-3">
<Hash className="w-5 h-5 text-pae-peach-dark mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs text-slate-500 uppercase tracking-wide font-medium">RIF</p>
{editing ? (
<input
type="text"
name="rif"
value={formData.rif}
onChange={handleChange}
placeholder="J-XXXXXXXX-X"
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm mt-1"
/>
) : (
<p className="text-slate-800 font-medium">{institucion?.rif || '—'}</p>
)}
</div>
</div>
DEA Code
<div className="flex items-start gap-3">
<Hash className="w-5 h-5 text-pae-peach-dark mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs text-slate-500 uppercase tracking-wide font-medium">Código DEA</p>
{editing ? (
<input
type="text"
name="codigo_dea"
value={formData.codigo_dea}
onChange={handleChange}
placeholder="Código DEA"
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm mt-1"
/>
) : (
<p className="text-slate-800 font-medium">{institucion?.codigo_dea || '—'}</p>
)}
</div>
</div>
Address
<div className="flex items-start gap-3 sm:col-span-2">
<MapPin className="w-5 h-5 text-pae-peach-dark mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs text-slate-500 uppercase tracking-wide font-medium">Dirección</p>
{editing ? (
<textarea
name="direccion"
value={formData.direccion}
onChange={handleChange}
placeholder="Dirección completa del plantel"
rows={2}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm mt-1 resize-none"
/>
) : (
<p className="text-slate-800 font-medium">{institucion?.direccion || '—'}</p>
)}
</div>
</div>
Current Director
<div className="flex items-start gap-3 sm:col-span-2">
<User className="w-5 h-5 text-pae-peach-dark mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs text-slate-500 uppercase tracking-wide font-medium">Director(a) Actual</p>
{editing ? (
<input
type="text"
name="director_actual"
value={formData.director_actual}
onChange={handleChange}
placeholder="Nombre del Director(a)"
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm mt-1"
/>
) : (
<p className="text-slate-800 font-medium">{institucion?.director_actual || '—'}</p>
)}
</div>
</div>
Logo Management
Logo Upload
The logo is uploaded to Supabase Storage in the logos bucket:
const handleLogoChange = (e) => {
const file = e.target.files[0]
if (!file) return
if (!file.type.startsWith('image/')) {
notifyError('Archivo inválido', 'Solo se permiten imágenes (PNG, JPG, etc.)')
return
}
setLogoFile(file)
setLogoPreview(URL.createObjectURL(file))
}
Logo Storage
When saving, the logo is uploaded first, then the URL is stored in the database:
let logo_url = institucion?.logo_url || ''
if (logoFile) {
const fileExt = logoFile.name.split('.').pop() || 'png'
const filePath = `logo-${Date.now()}.${fileExt}`
const { error: uploadError } = await supabase.storage
.from('logos')
.upload(filePath, logoFile, { cacheControl: '3600' })
if (uploadError) throw uploadError
const { data: urlData } = supabase.storage
.from('logos')
.getPublicUrl(filePath)
logo_url = urlData.publicUrl
}
Logo Display
In edit mode, clicking the logo allows uploading a new one:
{editing ? (
<div className="mb-4">
<label className="cursor-pointer group block mx-auto w-32 h-32 relative">
{logoPreview || institucion?.logo_url ? (
<img
src={logoPreview || institucion?.logo_url}
alt="Logo"
className="w-32 h-32 rounded-2xl object-cover mx-auto border-2 border-dashed border-orange-300 group-hover:opacity-75 transition-opacity"
/>
) : (
<div className="w-32 h-32 rounded-2xl bg-white border-2 border-dashed border-orange-300 flex items-center justify-center mx-auto group-hover:border-orange-400 transition-colors">
<Building2 className="w-12 h-12 text-slate-300" />
</div>
)}
<div className="absolute inset-0 flex items-center justify-center rounded-2xl bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity">
<Upload className="w-6 h-6 text-white" />
</div>
<input
type="file"
accept="image/*"
onChange={handleLogoChange}
className="hidden"
/>
</label>
<p className="text-xs text-slate-500 mt-2">Click para cambiar el logo</p>
</div>
) : (
<div className="mb-4">
{institucion?.logo_url ? (
<img
src={institucion.logo_url}
alt="Logo institucional"
className="w-32 h-32 rounded-2xl object-cover mx-auto shadow-sm"
/>
) : (
<div className="w-32 h-32 rounded-2xl bg-white border border-gray-200 flex items-center justify-center mx-auto shadow-sm">
<Building2 className="w-12 h-12 text-slate-300" />
</div>
)}
</div>
)}
Logo files are validated to ensure they are images. Non-image files will be rejected with an error notification.
Saving Changes
Validation
if (!formData.nombre.trim()) {
notifyError('Campo requerido', 'El nombre de la institución es obligatorio')
return
}
Update Operation
const { error } = await supabase
.from('institucion')
.update({
...formData,
logo_url,
updated_at: new Date().toISOString()
})
.eq('id', 1)
if (error) throw error
notifySuccess('Datos actualizados', 'Los datos del plantel se guardaron correctamente')
setEditing(false)
setLogoFile(null)
setLogoPreview(null)
loadInstitucion()
Permission Control
Only Desarrollador role can edit:
{editing ? (
<div className="flex gap-3 justify-end">
<button onClick={cancelEdit} disabled={saving} className="btn btn-secondary flex items-center gap-2">
<X className="w-4 h-4" /> Cancelar
</button>
<button onClick={handleSave} disabled={saving} className="btn btn-primary flex items-center gap-2">
<Save className="w-4 h-4" /> {saving ? 'Guardando...' : 'Guardar Cambios'}
</button>
</div>
) : (
userRole === 4 && (
<div className="flex justify-end">
<button onClick={startEdit} className="btn btn-primary flex items-center gap-2">
<Pencil className="w-4 h-4" /> Editar Datos
</button>
</div>
)
)}
Only users with id_rol === 4 (Desarrollador) will see the “Editar Datos” button. Other roles can view but not modify school information.
Last Updated Timestamp
The footer displays when the information was last updated:
{institucion?.updated_at && (
<div className="px-6 py-3 bg-slate-50 border-t border-gray-100 text-center">
<p className="text-xs text-slate-400">
Última actualización: {new Date(institucion.updated_at).toLocaleDateString('es-VE', {
year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'
})}
</p>
</div>
)}
UI Design
The component uses a centered card layout with peach-colored header:
<div className="max-w-2xl mx-auto py-8 px-4">
<div className="bg-white rounded-2xl shadow-md border border-gray-100 overflow-hidden">
{/* Header with logo and name */}
<div className="bg-pae-peach-light px-6 py-8 text-center border-b border-orange-100">
{/* Logo and name here */}
</div>
{/* Body with fields */}
<div className="px-6 py-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Fields here */}
</div>
{/* Buttons */}
</div>
{/* Footer with timestamp */}
</div>
</div>
Starting Edit Mode
const startEdit = () => {
setFormData({
nombre: institucion?.nombre || '',
rif: institucion?.rif || '',
codigo_dea: institucion?.codigo_dea || '',
direccion: institucion?.direccion || '',
director_actual: institucion?.director_actual || ''
})
setLogoFile(null)
setLogoPreview(null)
setEditing(true)
}
Canceling Edit Mode
const cancelEdit = () => {
setLogoFile(null)
setLogoPreview(null)
setEditing(false)
}
const handleChange = (e) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
Storage Requirements
Logos Bucket
The application requires a Supabase Storage bucket named logos with public access:
-- Create bucket (run in Supabase SQL Editor)
INSERT INTO storage.buckets (id, name, public)
VALUES ('logos', 'logos', true);
Storage Policy
Allow authenticated users to upload:
CREATE POLICY "Allow authenticated uploads"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'logos');