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 (
);
};
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
{title}
{children}
Cargando Directorio...
;
}
return (
{/* HEADER COMPACTO */}
updateUserName(e.target.value)}
placeholder="Tu Nombre..."
/>
{/* BUSCADOR Y FILTROS */}
setSearchTerm(e.target.value)}
/>
{/* CONTENIDO PRINCIPAL */}
{loading ? (
) : (
// LISTA DE TARJETAS
{/* MODAL EDICIÓN/CREACIÓN */}
setIsModalOpen(false)}
title={editingSchool ? "Gestionar Colegio" : "Nuevo Colegio"}
>
);
}
Directorio Escolar
) : 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.
{filteredSchools.map((school) => (
)}
{/* Etiqueta de región */}
{school.notes && (
))}
{school.region}
{school.name}
Director
{school.director || "--"}
Alumnos (Est.)
{school.students}
"{school.notes}"
)}