import React, { useState, useEffect, useMemo, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged, User } from 'firebase/auth'; import { getFirestore, collection, addDoc, updateDoc, doc, onSnapshot, query, orderBy, serverTimestamp, deleteDoc, writeBatch } from 'firebase/firestore'; import { MapPin, Phone, Users, Search, Plus, FileSpreadsheet, User as UserIcon, Briefcase, ExternalLink, BookOpen, Filter, Save, Download, XCircle, Edit, PhoneCall, Upload } from 'lucide-react'; // --- CONFIGURACIÓN DE FIREBASE --- const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // --- TIPOS DE DATOS --- type SchoolStatus = 'Pendiente' | 'Contactado' | 'Cita Agendada' | 'Muestra Entregada' | 'Cerrado' | 'No Interesado'; type Region = 'Torreón' | 'Saltillo' | 'Gómez Palacio' | 'Lerdo' | 'Otro'; type Level = 'Kinder' | 'Primaria' | 'Secundaria' | 'Múltiples Niveles'; interface School { id: string; name: string; region: Region; level: Level; director: string; address: string; phone: string; students: number; status: SchoolStatus; assignedTo: string; lastUpdate: any; notes: string; mapsLink: string; } // --- DATOS INICIALES --- const INITIAL_DATA: Omit[] = [ // --- TORREÓN --- { name: "Colegio Cervantes (Campus Vigatá)", region: "Torreón", level: "Múltiples Niveles", director: "Dirección General", address: "Calz. José Vasconcelos, Residencial Tecnológico", phone: "8717298800", students: 1200, status: "Pendiente", assignedTo: "Oficina", notes: "Uno de los colegios más grandes. Prioridad alta.", mapsLink: "https://goo.gl/maps/example1" }, { name: "Colegio Americano de Torreón", region: "Torreón", level: "Múltiples Niveles", director: "Dirección de Primaria", address: "Paseo del Algodón 500, Los Viñedos", phone: "8717505150", students: 1400, status: "Pendiente", assignedTo: "Oficina", notes: "Calendario americano. Verificar fechas.", mapsLink: "" }, { name: "Centro Educativo María Montessori", region: "Torreón", level: "Múltiples Niveles", director: "Dirección Académica", address: "Av. Río Nilo 650, Col. La Estrella", phone: "8717189797", students: 450, status: "Pendiente", assignedTo: "Oficina", notes: "", mapsLink: "" }, { name: "Colegio León Felipe", region: "Torreón", level: "Múltiples Niveles", director: "Admisiones", address: "Calz. Las Palmas 1750, Santa María", phone: "8717189492", students: 600, status: "Pendiente", assignedTo: "Oficina", notes: "", mapsLink: "" }, { name: "Colegio Valladolid", region: "Torreón", level: "Primaria", director: "Dirección Técnica", address: "Blvd. de la Libertad 396, Residencial Nazas", phone: "8717315348", students: 550, status: "Pendiente", assignedTo: "Oficina", notes: "", mapsLink: "" }, { name: "Instituto Británico", region: "Torreón", level: "Múltiples Niveles", director: "Dirección General", address: "Nuevo México 92, Residencial Linda Vista", phone: "8717173505", students: 900, status: "Pendiente", assignedTo: "Oficina", notes: "", mapsLink: "" }, // --- SALTILLO --- { name: "Colegio Ignacio Zaragoza (CIZ)", region: "Saltillo", level: "Múltiples Niveles", director: "Hno. Director", address: "La Salle, 25240 Saltillo", phone: "8444154700", students: 1500, status: "Pendiente", assignedTo: "Oficina", notes: "Volumen alto de alumnos.", mapsLink: "" }, { name: "Colegio Americano de Saltillo", region: "Saltillo", level: "Múltiples Niveles", director: "Admissions Office", address: "Carr. Saltillo - Monterrey Km 6.5", phone: "8444161100", students: 1000, status: "Pendiente", assignedTo: "Oficina", notes: "Nivel socioeconómico alto.", mapsLink: "" }, { name: "Colegio Roberts", region: "Saltillo", level: "Múltiples Niveles", director: "Dirección", address: "Abasolo Nte 850, Zona Centro", phone: "8444120612", students: 600, status: "Pendiente", assignedTo: "Oficina", notes: "", mapsLink: "" }, { name: "Colegio México de Saltillo", region: "Saltillo", level: "Primaria", director: "Dirección Primaria", address: "Presidente Cárdenas 251 Ote, Centro", phone: "8444123289", students: 450, status: "Pendiente", assignedTo: "Oficina", notes: "", mapsLink: "" } ]; // --- COMPONENTES UI --- const StatusBadge = ({ status }: { status: SchoolStatus }) => { const styles = { 'Pendiente': 'bg-gray-100 text-gray-700 border-gray-300', 'Contactado': 'bg-blue-100 text-blue-800 border-blue-300', 'Cita Agendada': 'bg-yellow-100 text-yellow-800 border-yellow-300 animate-pulse', 'Muestra Entregada': 'bg-purple-100 text-purple-800 border-purple-300', 'Cerrado': 'bg-green-100 text-green-800 border-green-300 font-bold', 'No Interesado': 'bg-red-100 text-red-800 border-red-300', }; return ( {status} ); }; const Modal = ({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) => { if (!isOpen) return null; return (

{title}

{children}
); }; export default function App() { const [user, setUser] = useState(null); const [userName, setUserName] = useState("Vendedor"); const [schools, setSchools] = useState([]); const [loading, setLoading] = useState(true); // Filtros const [filterRegion, setFilterRegion] = useState('Todas'); const [filterStatus, setFilterStatus] = useState('Todos'); const [searchTerm, setSearchTerm] = useState(''); // Modal State const [isModalOpen, setIsModalOpen] = useState(false); const [editingSchool, setEditingSchool] = useState(null); // File Input Ref const fileInputRef = useRef(null); // Form State const [formData, setFormData] = useState>({ name: '', region: 'Torreón', level: 'Primaria', director: '', address: '', phone: '', students: 0, status: 'Pendiente', assignedTo: '', notes: '', mapsLink: '' }); // --- AUTENTICACIÓN --- useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Auth error:", error); } }; initAuth(); // Recuperar nombre guardado o usar default const savedName = localStorage.getItem('sellerName'); if (savedName) setUserName(savedName); return onAuthStateChanged(auth, setUser); }, []); // --- BASE DE DATOS --- useEffect(() => { if (!user) return; const q = query( collection(db, 'artifacts', appId, 'public', 'data', 'schools'), orderBy('name') ); const unsubscribe = onSnapshot(q, (snapshot) => { const schoolsData = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) as School[]; setSchools(schoolsData); setLoading(false); }, (error) => { console.error("Error lectura DB:", error); setLoading(false); } ); return () => unsubscribe(); }, [user]); // --- IMPORTACIÓN CSV --- const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; if (!user) { alert("Error: No estás autenticado."); return; } const reader = new FileReader(); reader.onload = async (e) => { const text = e.target?.result as string; if (!text) return; // Parseo básico de CSV const lines = text.split('\n'); const headers = lines[0].toLowerCase().split(',').map(h => h.trim().replace(/"/g, '')); const newSchools: Omit[] = []; // Mapeo simple de columnas const getIndex = (keys: string[]) => headers.findIndex(h => keys.some(k => h.includes(k))); const idxName = getIndex(['nombre', 'colegio', 'escuela', 'name']); const idxRegion = getIndex(['region', 'ciudad', 'ubicacion', 'city']); const idxPhone = getIndex(['telefono', 'celular', 'phone', 'tel']); const idxDirector = getIndex(['director', 'contacto', 'contact']); const idxStudents = getIndex(['alumnos', 'cantidad', 'students']); const idxAddress = getIndex(['direccion', 'calle', 'address']); for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // Manejo básico de comillas en CSV const row = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(cell => cell.trim().replace(/^"|"$/g, '')); if (row.length < 2) continue; // Ignorar filas vacías o mal formadas newSchools.push({ name: idxName !== -1 ? row[idxName] || "Sin Nombre" : row[0], // Fallback a primera columna region: (idxRegion !== -1 ? row[idxRegion] : "Torreón") as Region, level: "Múltiples Niveles", director: idxDirector !== -1 ? row[idxDirector] : "", address: idxAddress !== -1 ? row[idxAddress] : "", phone: idxPhone !== -1 ? row[idxPhone] : "", students: idxStudents !== -1 ? (parseInt(row[idxStudents]) || 0) : 0, status: 'Pendiente', assignedTo: 'Importado', notes: 'Importado desde CSV', mapsLink: '' }); } if (newSchools.length > 0) { if(confirm(`Se encontraron ${newSchools.length} registros. ¿Deseas importarlos?`)) { setLoading(true); try { // Procesar en lotes de 500 para Firestore const batchSize = 400; for (let i = 0; i < newSchools.length; i += batchSize) { const batch = writeBatch(db); const chunk = newSchools.slice(i, i + batchSize); chunk.forEach(school => { const newRef = doc(collection(db, 'artifacts', appId, 'public', 'data', 'schools')); batch.set(newRef, { ...school, lastUpdate: serverTimestamp() }); }); await batch.commit(); } alert("✅ Importación exitosa."); } catch (err) { console.error(err); alert("Error al importar en la base de datos."); } finally { setLoading(false); } } } else { alert("No se pudieron leer registros válidos. Revisa que el CSV tenga encabezados (Nombre, Telefono, etc)."); } // Reset input if (fileInputRef.current) fileInputRef.current.value = ''; }; reader.readAsText(file); }; // --- FUNCIONES --- const handleSeedData = async () => { // Verificación de seguridad if (!auth.currentUser) { await signInAnonymously(auth); } setLoading(true); try { const batch = writeBatch(db); INITIAL_DATA.forEach(school => { const newRef = doc(collection(db, 'artifacts', appId, 'public', 'data', 'schools')); batch.set(newRef, { ...school, lastUpdate: serverTimestamp() }); }); await batch.commit(); console.log("Datos cargados"); } catch (error) { console.error("Error carga masiva:", error); alert("Error al cargar datos. Verifica tu conexión."); } finally { setLoading(false); } }; const handleSaveSchool = async (e: React.FormEvent) => { e.preventDefault(); if (!user) return; try { const dataToSave = { ...formData, lastUpdate: serverTimestamp() }; if (editingSchool) { await updateDoc(doc(db, 'artifacts', appId, 'public', 'data', 'schools', editingSchool.id), dataToSave); } else { if (!dataToSave.assignedTo) dataToSave.assignedTo = userName; await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'schools'), dataToSave); } setIsModalOpen(false); resetForm(); } catch (error) { alert("Error al guardar."); } }; const handleDelete = async (id: string) => { if(!confirm("¿Eliminar este colegio permanentemente?")) return; try { await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'schools', id)); } catch (e) { console.error(e); } }; const resetForm = () => { setFormData({ name: '', region: 'Torreón', level: 'Primaria', director: '', address: '', phone: '', students: 0, status: 'Pendiente', assignedTo: userName, notes: '', mapsLink: '' }); setEditingSchool(null); }; const openEditModal = (school: School) => { setEditingSchool(school); setFormData(school); setIsModalOpen(true); }; const handleExportCSV = () => { const headers = ['Nombre', 'Región', 'Teléfono', 'Director', 'Alumnos', 'Estatus', 'Vendedor', 'Notas', 'Dirección']; const csvContent = [ headers.join(','), ...filteredSchools.map(s => [ `"${s.name}"`, `"${s.region}"`, `"${s.phone}"`, `"${s.director}"`, s.students, `"${s.status}"`, `"${s.assignedTo}"`, `"${s.notes.replace(/"/g, '""')}"`, `"${s.address}"` ].join(',')) ].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `directorio_colegios_${new Date().toISOString().slice(0,10)}.csv`; link.click(); }; const updateUserName = (name: string) => { setUserName(name); localStorage.setItem('sellerName', name); }; // --- FILTROS --- const filteredSchools = useMemo(() => { return schools.filter(school => { const matchesRegion = filterRegion === 'Todas' || school.region === filterRegion; const matchesStatus = filterStatus === 'Todos' || school.status === filterStatus; const searchLower = searchTerm.toLowerCase(); const matchesSearch = school.name.toLowerCase().includes(searchLower) || school.phone.includes(searchLower); return matchesRegion && matchesStatus && matchesSearch; }); }, [schools, filterRegion, filterStatus, searchTerm]); // --- UI RENDER --- // Si está cargando auth inicial if (loading && schools.length === 0 && !user) { return
Cargando Directorio...
; } return (
{/* HEADER COMPACTO */}

Directorio Escolar

updateUserName(e.target.value)} placeholder="Tu Nombre..." />
{/* BUSCADOR Y FILTROS */}
setSearchTerm(e.target.value)} />
{/* CONTENIDO PRINCIPAL */} {loading ? (
) : schools.length === 0 ? ( // ESTADO VACÍO CON BOTÓN DE ACCIÓN GRANDE

La lista está vacía

Puedes cargar los datos sugeridos por mi, o subir tu propia hoja de cálculo (CSV) de Google.

) : ( // LISTA DE TARJETAS
{filteredSchools.map((school) => (
{/* Etiqueta de región */}
{school.region}

{school.name}

{school.address || "Sin dirección"}

{/* SECCIÓN CLAVE: TELÉFONO Y ESTATUS */}
Director {school.director || "--"}
Alumnos (Est.) {school.students}
{school.notes && (
"{school.notes}"
)}
))}
)}
{/* MODAL EDICIÓN/CREACIÓN */} setIsModalOpen(false)} title={editingSchool ? "Gestionar Colegio" : "Nuevo Colegio"} >
setFormData({...formData, name: e.target.value})} />
setFormData({...formData, phone: e.target.value})} />
{['Pendiente', 'Contactado', 'Cita Agendada', 'Muestra Entregada', 'Cerrado', 'No Interesado'].map(s => ( ))}
setFormData({...formData, director: e.target.value})} />
setFormData({...formData, students: Number(e.target.value)})} />
setFormData({...formData, address: e.target.value})} />
{editingSchool && ( )}
); }