311 lines
11 KiB
JavaScript
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>
|
|
);
|
|
}
|