Files
backendDonMarco/frontend/src/pages/AdminPage.jsx
2026-06-13 12:04:04 -04:00

311 lines
11 KiB
JavaScript

import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { usersApi, settingsApi } from '../api/client';
const SHEET_LABELS = {
ventas: 'Ventas',
servicios: 'Servicios',
alquileres: 'Alquileres',
gastos_diarios: 'Gastos diarios',
gastos_fijos: 'Gastos fijos',
personal: 'Personal',
reportes: 'Reportes',
};
const SHEET_KEYS = Object.keys(SHEET_LABELS);
function emptyPerms() {
return Object.fromEntries(SHEET_KEYS.map((k) => [k, { view: false, edit: false }]));
}
export default function AdminPage() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [newUser, setNewUser] = useState({ username: '', password: '', permissions: emptyPerms() });
const [gestion, setGestion] = useState(null);
const [fechaCierre, setFechaCierre] = useState('');
const [gestionMsg, setGestionMsg] = useState('');
const [gestionError, setGestionError] = useState('');
const location = useLocation();
const load = async () => {
setError('');
setGestionError('');
try {
setUsers(await usersApi.list());
} catch (e) {
setError(e.message);
}
try {
const gestionData = await settingsApi.getGestion();
setGestion(gestionData);
setFechaCierre(gestionData.sugerirCierre || gestionData.fechaCierreGestion || '');
} catch (e) {
setGestionError(
e.message.includes('404') || e.message.includes('Error en la solicitud')
? 'No se pudo cargar el cierre de gestión. Reconstruya Docker: docker compose build --no-cache && docker compose up -d'
: e.message
);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
useEffect(() => {
if (location.hash === '#cierre-gestion') {
document.getElementById('cierre-gestion')?.scrollIntoView({ behavior: 'smooth' });
}
}, [location.hash, loading]);
const handleCreate = async (e) => {
e.preventDefault();
setError('');
try {
await usersApi.create(newUser);
setNewUser({ username: '', password: '', permissions: emptyPerms() });
await load();
} catch (err) {
setError(err.message);
}
};
const updateUserPerms = async (user, sheet, field, value) => {
const permissions = { ...user.permissions, [sheet]: { ...user.permissions[sheet], [field]: value } };
try {
await usersApi.update(user.id, { permissions });
await load();
} catch (err) {
setError(err.message);
}
};
const toggleActive = async (user) => {
if (user.isAdmin) return;
try {
await usersApi.update(user.id, { active: !user.active });
await load();
} catch (err) {
setError(err.message);
}
};
const cerrarGestion = async (e) => {
e.preventDefault();
if (!fechaCierre) {
setError('Indique el último día de la gestión a cerrar.');
return;
}
const etiqueta = gestion?.gestionActual?.fechaInicio
? `del ${gestion.gestionActual.fechaInicio} al ${fechaCierre}`
: `hasta el ${fechaCierre}`;
if (
!window.confirm(
`¿Cerrar la gestión ${etiqueta}?\n\nSe bloqueará el período para todos (excepto JEFE) y comenzará una gestión nueva. El historial quedará en la barra lateral de cada planilla.`
)
) {
return;
}
setGestionMsg('');
setError('');
try {
const data = await settingsApi.cerrarGestion(fechaCierre);
setGestion(data);
setFechaCierre(data.sugerirCierre || '');
const nombre = data.cierre?.cerrada?.etiqueta || 'gestión cerrada';
setGestionMsg(`"${nombre}" cerrada. Nueva gestión desde ${data.cierre?.nueva?.fechaInicio || 'mañana'}.`);
} catch (err) {
setError(err.message);
}
};
if (loading) return <p className="loading">Cargando administración</p>;
return (
<div>
<h1 className="page-title">Administración</h1>
<p style={{ color: 'var(--muted)' }}>
Solo visible para el administrador <strong>JEFE</strong>. Gestione el cierre del período y los usuarios.
</p>
<nav className="admin-subnav">
<a href="#cierre-gestion" className="admin-subnav__link admin-subnav__link--active">
Cierre de gestión
</a>
<a href="#usuarios" className="admin-subnav__link">
Usuarios
</a>
</nav>
{error && <div className="alert alert-error">{error}</div>}
{gestionMsg && <div className="alert alert-success">{gestionMsg}</div>}
<div id="cierre-gestion" className="card card-highlight admin-grid" style={{ marginBottom: '1.5rem' }}>
<h2 style={{ marginTop: 0 }}>Cierre de gestión</h2>
{gestionError && <div className="alert alert-error">{gestionError}</div>}
<div className="alert alert-info" style={{ fontSize: '0.88rem', lineHeight: 1.6 }}>
<strong>¿Cómo funciona?</strong>
<ul style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
<li>Al <strong>cerrar la gestión</strong> (típicamente fin de mes), el período queda bloqueado de inmediato.</li>
<li>Cada planilla muestra solo la <strong>gestión actual</strong>; el historial está en la <strong>barra lateral</strong> (solo lectura).</li>
<li>La nueva gestión comienza el día siguiente al cierre. Use el botón <strong>+</strong> para cargar datos nuevos.</li>
<li>Filas con fecha del período cerrado no se pueden editar (excepto JEFE).</li>
</ul>
</div>
{gestion?.gestionActual && (
<p style={{ fontSize: '0.85rem', marginBottom: '1rem' }}>
Gestión actual: desde <strong>{gestion.gestionActual.fechaInicio}</strong> Hoy:{' '}
<strong>{gestion.hoy}</strong>
</p>
)}
{gestion?.historial?.length > 0 && (
<div style={{ marginBottom: '1rem' }}>
<p style={{ fontSize: '0.85rem', marginBottom: '0.5rem' }}>
<strong>Gestiones cerradas:</strong>
</p>
<ul style={{ margin: 0, paddingLeft: '1.2rem', fontSize: '0.85rem' }}>
{gestion.historial.map((g) => (
<li key={g.id}>
{g.etiqueta} ({g.fechaInicio} {g.fechaFin})
</li>
))}
</ul>
</div>
)}
<form onSubmit={cerrarGestion}>
<div className="form-group">
<label>Último día de la gestión a cerrar</label>
<input type="date" value={fechaCierre} onChange={(e) => setFechaCierre(e.target.value)} required />
{gestion?.sugerirCierre && (
<p style={{ fontSize: '0.8rem', color: 'var(--muted)', marginTop: '0.35rem' }}>
Sugerido (fin de mes): {gestion.sugerirCierre}
</p>
)}
</div>
<div className="toolbar">
<button type="submit" className="btn btn-primary">
Cerrar gestión
</button>
</div>
</form>
</div>
<div id="usuarios" className="card admin-grid" style={{ marginBottom: '1.5rem' }}>
<h2 style={{ marginTop: 0 }}>Usuarios Nuevo usuario</h2>
<form onSubmit={handleCreate}>
<div className="form-group">
<label>Usuario</label>
<input
value={newUser.username}
onChange={(e) => setNewUser((p) => ({ ...p, username: e.target.value }))}
required
/>
</div>
<div className="form-group">
<label>Contraseña</label>
<input
type="password"
value={newUser.password}
onChange={(e) => setNewUser((p) => ({ ...p, password: e.target.value }))}
required
/>
</div>
<p style={{ fontSize: '0.85rem' }}>Permisos por planilla:</p>
<div className="perm-grid">
{SHEET_KEYS.filter((k) => k !== 'reportes').map((key) => (
<div key={key} className="perm-item">
<strong>{SHEET_LABELS[key]}</strong>
<label className="checkbox-row">
<input
type="checkbox"
checked={newUser.permissions[key]?.view}
onChange={(e) =>
setNewUser((p) => ({
...p,
permissions: {
...p.permissions,
[key]: { ...p.permissions[key], view: e.target.checked },
},
}))
}
/>
Ver
</label>
<label className="checkbox-row">
<input
type="checkbox"
checked={newUser.permissions[key]?.edit}
onChange={(e) =>
setNewUser((p) => ({
...p,
permissions: {
...p.permissions,
[key]: { ...p.permissions[key], edit: e.target.checked },
},
}))
}
/>
Editar
</label>
</div>
))}
</div>
<button type="submit" className="btn btn-primary" style={{ marginTop: '1rem' }}>
Crear usuario
</button>
</form>
</div>
<div className="card">
<h2 style={{ marginTop: 0 }}>Usuarios del sistema</h2>
{users.map((u) => (
<div key={u.id} style={{ borderBottom: '1px solid var(--border)', padding: '1rem 0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<strong>{u.username}</strong>
{u.isAdmin && <span style={{ color: 'var(--accent)' }}>Administrador</span>}
{!u.isAdmin && (
<button type="button" className="btn btn-secondary" onClick={() => toggleActive(u)}>
{u.active ? 'Deshabilitar' : 'Habilitar'}
</button>
)}
</div>
{!u.isAdmin && (
<div className="perm-grid" style={{ marginTop: '0.75rem' }}>
{SHEET_KEYS.map((key) => (
<div key={key} className="perm-item">
<span>{SHEET_LABELS[key]}</span>
<label className="checkbox-row">
<input
type="checkbox"
checked={u.permissions?.[key]?.view}
onChange={(e) => updateUserPerms(u, key, 'view', e.target.checked)}
/>
Ver
</label>
<label className="checkbox-row">
<input
type="checkbox"
checked={u.permissions?.[key]?.edit}
onChange={(e) => updateUserPerms(u, key, 'edit', e.target.checked)}
/>
Editar
</label>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
);
}