diff --git a/app/package.json b/app/package.json index b524e72..909a8d7 100644 --- a/app/package.json +++ b/app/package.json @@ -24,7 +24,8 @@ "jose": "^5.6.3", "next": "^14.2.5", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", diff --git a/app/src/app/admin/import/page.tsx b/app/src/app/admin/import/page.tsx new file mode 100644 index 0000000..a9f3d3e --- /dev/null +++ b/app/src/app/admin/import/page.tsx @@ -0,0 +1,189 @@ +'use client' + +import { useState, useRef } from 'react' +import { useRouter } from 'next/navigation' + +interface UserRow { name: string; email: string } +interface ImportResult { name: string; email: string; status: 'created' | 'exists' | 'error'; password?: string } + +export default function ImportPage() { + const [preview, setPreview] = useState([]) + const [results, setResults] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const fileRef = useRef(null) + const router = useRouter() + + const handleFile = async (file: File) => { + setError('') + setPreview([]) + setResults([]) + try { + const XLSX = await import('xlsx') + const buf = await file.arrayBuffer() + const wb = XLSX.read(buf, { type: 'array' }) + const ws = wb.Sheets[wb.SheetNames[0]] + const rows = XLSX.utils.sheet_to_json>(ws, { defval: '' }) + + const parsed: UserRow[] = rows.map(row => { + const keys = Object.keys(row).map(k => k.toLowerCase()) + const nameKey = Object.keys(row).find(k => ['nombre', 'name', 'nombre completo', 'full name'].includes(k.toLowerCase())) + const emailKey = Object.keys(row).find(k => k.toLowerCase() === 'email' || k.toLowerCase() === 'correo') + return { + name: nameKey ? String(row[nameKey]).trim() : '', + email: emailKey ? String(row[emailKey]).trim() : '', + } + }).filter(r => r.name && r.email) + + if (parsed.length === 0) { + setError('No se encontraron filas válidas. El archivo debe tener columnas "nombre" y "email".') + return + } + setPreview(parsed) + } catch { + setError('No se pudo leer el archivo. Asegurate de que sea .xlsx o .xls') + } + } + + const handleImport = async () => { + setLoading(true) + setError('') + try { + const res = await fetch('/api/admin/import-users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ users: preview }), + }) + const data = await res.json() + if (!res.ok) { setError(data.error); return } + setResults(data.results) + setPreview([]) + } catch { + setError('Error al importar') + } finally { + setLoading(false) + } + } + + const copyAll = () => { + const created = results.filter(r => r.status === 'created') + const text = created.map(r => `${r.name}\t${r.email}\t${r.password}`).join('\n') + navigator.clipboard.writeText(text) + } + + return ( +
+
+ +

Importar usuarios

+
+ +
+ + {/* Upload */} + {preview.length === 0 && results.length === 0 && ( +
fileRef.current?.click()} + className="border-2 border-dashed border-gray-300 rounded-2xl p-10 text-center cursor-pointer hover:border-gray-400 transition-colors" + > +
📂
+

Seleccioná el archivo Excel

+

Columnas requeridas: nombre y email

+ e.target.files?.[0] && handleFile(e.target.files[0])} + /> +
+ )} + + {error && ( +
{error}
+ )} + + {/* Preview */} + {preview.length > 0 && ( +
+
+

{preview.length} usuarios para importar

+ +
+
+ + + + + + + + + {preview.map((u, i) => ( + + + + + ))} + +
NombreEmail
{u.name}{u.email}
+
+ +
+ )} + + {/* Results */} + {results.length > 0 && ( +
+
+

+ {results.filter(r => r.status === 'created').length} creados ·{' '} + {results.filter(r => r.status === 'exists').length} ya existían +

+ +
+
+ + + + + + + + + + {results.map((r, i) => ( + + + + + + ))} + +
NombreEmailContraseña
{r.name}{r.email} + {r.status === 'created' && ( + {r.password} + )} + {r.status === 'exists' && ya existe} + {r.status === 'error' && error} +
+
+ +
+ )} + +
+
+ ) +} diff --git a/app/src/app/admin/page.tsx b/app/src/app/admin/page.tsx index 2b8517f..cffb23d 100644 --- a/app/src/app/admin/page.tsx +++ b/app/src/app/admin/page.tsx @@ -52,7 +52,12 @@ export default function AdminPage() {
-

⚙️ Admin

+
+

⚙️ Admin

+ + 📥 Importar + +