mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
feat(users): improve user editing and ticket details
- Add user's full name to the database and UI.\n- Allow admins to edit user details (name, username, role).\n- Allow users to edit their own name and password.\n- Automatically set the 'staff' field on new tickets to the current user's name.\n- Bold the 'Te atendió:' label in the printed ticket.
This commit is contained in:
@@ -234,9 +234,11 @@ function renderUsersTable() {
|
||||
users.forEach(u => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${u.name}</td>
|
||||
<td>${u.username}</td>
|
||||
<td>${u.role === 'admin' ? 'Administrador' : 'Usuario'}</td>
|
||||
<td>
|
||||
<button class="action-btn" data-id="${u.id}" data-action="edit-user">Editar</button>
|
||||
${u.id !== currentUser.id ? `<button class="action-btn" data-id="${u.id}" data-action="delete-user">Eliminar</button>` : ''}
|
||||
</td>
|
||||
`;
|
||||
@@ -276,10 +278,11 @@ async function handleSaveSettings(e) {
|
||||
|
||||
async function handleSaveCredentials(e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('s-name').value;
|
||||
const username = document.getElementById('s-username').value;
|
||||
const password = document.getElementById('s-password').value;
|
||||
|
||||
const body = { username };
|
||||
const body = { username, name };
|
||||
if (password) {
|
||||
body.password = password;
|
||||
}
|
||||
@@ -293,6 +296,8 @@ async function handleSaveCredentials(e) {
|
||||
|
||||
if (response.ok) {
|
||||
alert('Credenciales actualizadas.');
|
||||
currentUser.name = name; // Actualizar el nombre en el estado local
|
||||
currentUser.username = username;
|
||||
document.getElementById('s-password').value = '';
|
||||
} else {
|
||||
const error = await response.json();
|
||||
@@ -303,31 +308,54 @@ async function handleSaveCredentials(e) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddUser(e) {
|
||||
async function handleAddOrUpdateUser(e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('u-id').value;
|
||||
const name = document.getElementById('u-name').value;
|
||||
const username = document.getElementById('u-username').value;
|
||||
const password = document.getElementById('u-password').value;
|
||||
const role = document.getElementById('u-role').value;
|
||||
|
||||
const isUpdate = !!id;
|
||||
const url = isUpdate ? `/api/users/${id}` : '/api/users';
|
||||
const method = isUpdate ? 'PUT' : 'POST';
|
||||
|
||||
const body = { name, username, role };
|
||||
if (password || !isUpdate) {
|
||||
if (!password && !isUpdate) {
|
||||
alert('La contraseña es obligatoria para nuevos usuarios.');
|
||||
return;
|
||||
}
|
||||
body.password = password;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, role })
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const newUser = await response.json();
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert('Usuario creado exitosamente.');
|
||||
users.push(newUser);
|
||||
alert(`Usuario ${isUpdate ? 'actualizado' : 'creado'} exitosamente.`);
|
||||
if (isUpdate) {
|
||||
const index = users.findIndex(u => u.id === parseInt(id));
|
||||
if (index > -1) {
|
||||
users[index] = { ...users[index], name, username, role };
|
||||
}
|
||||
} else {
|
||||
users.push(result);
|
||||
}
|
||||
renderUsersTable();
|
||||
formAddUser.reset();
|
||||
document.getElementById('u-id').value = '';
|
||||
} else {
|
||||
alert(`Error: ${newUser.error}`);
|
||||
alert(`Error: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error de conexión al crear usuario.');
|
||||
alert('Error de conexión al guardar el usuario.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,7 +409,7 @@ async function handleNewMovement(e) {
|
||||
monto: Number(monto.toFixed(2)),
|
||||
metodo: document.getElementById('m-metodo').value,
|
||||
concepto: document.getElementById('m-concepto').value,
|
||||
staff: document.getElementById('m-staff').value,
|
||||
staff: currentUser.name, // Usar el nombre del usuario actual
|
||||
notas: document.getElementById('m-notas').value,
|
||||
fechaCita: document.getElementById('m-fecha-cita').value,
|
||||
horaCita: document.getElementById('m-hora-cita').value,
|
||||
@@ -424,6 +452,16 @@ function handleTableClick(e) {
|
||||
deleteClient(id);
|
||||
}
|
||||
}
|
||||
} else if (action === 'edit-user') {
|
||||
const user = users.find(u => u.id === parseInt(id));
|
||||
if (user) {
|
||||
document.getElementById('u-id').value = user.id;
|
||||
document.getElementById('u-name').value = user.name;
|
||||
document.getElementById('u-username').value = user.username;
|
||||
document.getElementById('u-role').value = user.role;
|
||||
document.getElementById('u-password').value = ''; // Limpiar campo de contraseña
|
||||
document.getElementById('u-password').placeholder = 'Dejar en blanco para no cambiar';
|
||||
}
|
||||
} else if (action === 'delete-user') {
|
||||
deleteUser(parseInt(id, 10));
|
||||
}
|
||||
@@ -508,6 +546,7 @@ function setupUIForRole(role) {
|
||||
const dashboardTab = document.querySelector('[data-tab="tab-dashboard"]');
|
||||
const settingsTab = document.querySelector('[data-tab="tab-settings"]');
|
||||
const userManagementSection = document.getElementById('user-management-section');
|
||||
const staffInput = document.getElementById('m-staff');
|
||||
|
||||
if (role === 'admin') {
|
||||
// El admin puede ver todo
|
||||
@@ -526,6 +565,11 @@ function setupUIForRole(role) {
|
||||
settingsTab.style.display = 'none';
|
||||
userManagementSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Deshabilitar el campo "Atendió" para todos, ya que se autocompleta
|
||||
if (staffInput) {
|
||||
staffInput.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -563,6 +607,7 @@ async function initializeApp() {
|
||||
// 3. Añadir manejadores de eventos.
|
||||
const tabs = document.querySelector('.tabs');
|
||||
const btnLogout = document.getElementById('btnLogout');
|
||||
const btnCancelEditUser = document.getElementById('btnCancelEditUser');
|
||||
|
||||
formSettings?.addEventListener('submit', handleSaveSettings);
|
||||
formCredentials?.addEventListener('submit', handleSaveCredentials);
|
||||
@@ -575,7 +620,7 @@ async function initializeApp() {
|
||||
tabs?.addEventListener('click', handleTabChange);
|
||||
|
||||
if (currentUser.role === 'admin') {
|
||||
formAddUser?.addEventListener('submit', handleAddUser);
|
||||
formAddUser?.addEventListener('submit', handleAddOrUpdateUser);
|
||||
tblUsersBody?.addEventListener('click', handleTableClick);
|
||||
}
|
||||
|
||||
@@ -589,6 +634,13 @@ async function initializeApp() {
|
||||
document.getElementById('c-id').value = '';
|
||||
});
|
||||
|
||||
btnCancelEditUser?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
formAddUser.reset();
|
||||
document.getElementById('u-id').value = '';
|
||||
document.getElementById('u-password').placeholder = 'Contraseña';
|
||||
});
|
||||
|
||||
// 4. Cargar el resto de los datos de la aplicación.
|
||||
Promise.all([
|
||||
load(KEY_SETTINGS, DEFAULT_SETTINGS),
|
||||
@@ -603,7 +655,9 @@ async function initializeApp() {
|
||||
updateClientDatalist();
|
||||
|
||||
if (currentUser) {
|
||||
document.getElementById('s-name').value = currentUser.name || '';
|
||||
document.getElementById('s-username').value = currentUser.username;
|
||||
document.getElementById('m-staff').value = currentUser.name || '';
|
||||
}
|
||||
|
||||
// 5. Configurar la UI y activar la pestaña inicial correcta.
|
||||
|
||||
@@ -183,6 +183,8 @@
|
||||
<h2>Mis Credenciales</h2>
|
||||
<form id="formCredentials">
|
||||
<div class="form-grid">
|
||||
<label>Mi Nombre:</label>
|
||||
<input type="text" id="s-name" required />
|
||||
<label>Mi Usuario:</label>
|
||||
<input type="text" id="s-username" required />
|
||||
<label>Nueva Contraseña:</label>
|
||||
@@ -197,13 +199,16 @@
|
||||
<div class="section" id="user-management-section" style="display: none;">
|
||||
<h2>Gestión de Usuarios</h2>
|
||||
<div class="sub-section">
|
||||
<h3>Añadir Nuevo Usuario</h3>
|
||||
<h3>Añadir/Editar Usuario</h3>
|
||||
<form id="formAddUser">
|
||||
<input type="hidden" id="u-id" />
|
||||
<div class="form-grid">
|
||||
<label>Nuevo Usuario:</label>
|
||||
<label>Nombre:</label>
|
||||
<input type="text" id="u-name" required />
|
||||
<label>Usuario:</label>
|
||||
<input type="text" id="u-username" required />
|
||||
<label>Contraseña:</label>
|
||||
<input type="password" id="u-password" required />
|
||||
<input type="password" id="u-password" placeholder="Dejar en blanco para no cambiar" />
|
||||
<label>Rol:</label>
|
||||
<select id="u-role">
|
||||
<option value="user">Usuario (Ventas)</option>
|
||||
@@ -211,7 +216,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Crear Usuario</button>
|
||||
<button type="submit">Guardar Usuario</button>
|
||||
<button type="reset" id="btnCancelEditUser" class="btn-danger">Cancelar Edición</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -221,6 +227,7 @@
|
||||
<table id="tblUsers">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Usuario</th>
|
||||
<th>Rol</th>
|
||||
<th>Acciones</th>
|
||||
|
||||
@@ -42,7 +42,7 @@ function templateTicket(mov, settings) {
|
||||
lines.push(`<div><span class="t-bold">${esc(mov.tipo)}</span></div>`);
|
||||
if (mov.cliente) lines.push(`<div class="t-small">Cliente: ${esc(mov.cliente)}</div>`);
|
||||
if (mov.concepto) lines.push(`<div class="t-small">Concepto: ${esc(mov.concepto)}</div>`);
|
||||
if (mov.staff) lines.push(`<div class="t-small">Atendió: ${esc(mov.staff)}</div>`);
|
||||
if (mov.staff) lines.push(`<div class="t-small"><b>Te atendió:</b> ${esc(mov.staff)}</div>`);
|
||||
if (mov.metodo) lines.push(`<div class="t-small">Método: ${esc(mov.metodo)}</div>`);
|
||||
if (mov.notas) lines.push(`<div class="t-small">Notas: ${esc(mov.notas)}</div>`);
|
||||
|
||||
|
||||
@@ -42,26 +42,34 @@ db.serialize(() => {
|
||||
}
|
||||
});
|
||||
|
||||
db.run("ALTER TABLE users ADD COLUMN name TEXT", (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error("Error adding name column to users table:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE,
|
||||
password TEXT,
|
||||
role TEXT DEFAULT 'user'
|
||||
role TEXT DEFAULT 'user',
|
||||
name TEXT
|
||||
)`, (err) => {
|
||||
if (err) return;
|
||||
// Solo intentar insertar si la tabla fue creada o ya existía
|
||||
const adminUsername = 'admin';
|
||||
const defaultPassword = 'password';
|
||||
const defaultName = 'Admin'; // Nombre por defecto para el admin
|
||||
|
||||
db.get('SELECT * FROM users WHERE username = ?', [adminUsername], (err, row) => {
|
||||
if (err) return;
|
||||
if (!row) {
|
||||
bcrypt.hash(defaultPassword, SALT_ROUNDS, (err, hash) => {
|
||||
if (err) return;
|
||||
// Insertar admin con su rol
|
||||
db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [adminUsername, hash, 'admin'], (err) => {
|
||||
// Insertar admin con su rol y nombre
|
||||
db.run('INSERT INTO users (username, password, role, name) VALUES (?, ?, ?, ?)', [adminUsername, hash, 'admin', defaultName], (err) => {
|
||||
if (!err) {
|
||||
console.log(`Default user '${adminUsername}' created with password '${defaultPassword}' and role 'admin'. Please change it.`);
|
||||
console.log(`Default user '${adminUsername}' created with name '${defaultName}', password '${defaultPassword}' and role 'admin'. Please change it.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -129,7 +137,8 @@ app.post('/api/login', (req, res) => {
|
||||
}
|
||||
req.session.userId = user.id;
|
||||
req.session.role = user.role; // Guardar rol en la sesión
|
||||
res.json({ message: 'Login successful', role: user.role });
|
||||
req.session.name = user.name; // Guardar nombre en la sesión
|
||||
res.json({ message: 'Login successful', role: user.role, name: user.name });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -147,7 +156,7 @@ app.post('/api/logout', (req, res) => {
|
||||
// Endpoint para verificar el estado de la autenticación en el frontend
|
||||
app.get('/api/check-auth', (req, res) => {
|
||||
if (req.session.userId) {
|
||||
res.json({ isAuthenticated: true, role: req.session.role });
|
||||
res.json({ isAuthenticated: true, role: req.session.role, name: req.session.name });
|
||||
} else {
|
||||
res.json({ isAuthenticated: false });
|
||||
}
|
||||
@@ -267,7 +276,7 @@ app.use('/api', apiRouter);
|
||||
// --- User Management (Admin) ---
|
||||
// Proteger estas rutas para que solo los admins puedan usarlas
|
||||
apiRouter.get('/users', isAdmin, (req, res) => {
|
||||
db.all("SELECT id, username, role FROM users", [], (err, rows) => {
|
||||
db.all("SELECT id, username, role, name FROM users", [], (err, rows) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
@@ -277,9 +286,9 @@ apiRouter.get('/users', isAdmin, (req, res) => {
|
||||
});
|
||||
|
||||
apiRouter.post('/users', isAdmin, (req, res) => {
|
||||
const { username, password, role } = req.body;
|
||||
if (!username || !password || !role) {
|
||||
return res.status(400).json({ error: 'Username, password, and role are required' });
|
||||
const { username, password, role, name } = req.body;
|
||||
if (!username || !password || !role || !name) {
|
||||
return res.status(400).json({ error: 'Username, password, name, and role are required' });
|
||||
}
|
||||
if (role !== 'admin' && role !== 'user') {
|
||||
return res.status(400).json({ error: 'Invalid role' });
|
||||
@@ -289,18 +298,41 @@ apiRouter.post('/users', isAdmin, (req, res) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Error hashing password' });
|
||||
}
|
||||
db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [username, hash, role], function(err) {
|
||||
db.run('INSERT INTO users (username, password, role, name) VALUES (?, ?, ?, ?)', [username, hash, role, name], function(err) {
|
||||
if (err) {
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
return res.status(409).json({ error: 'Username already exists' });
|
||||
}
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
res.status(201).json({ id: this.lastID, username, role });
|
||||
res.status(201).json({ id: this.lastID, username, role, name });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Nueva ruta para actualizar un usuario (solo admin)
|
||||
apiRouter.put('/users/:id', isAdmin, (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { username, role, name } = req.body;
|
||||
|
||||
if (!username || !role || !name) {
|
||||
return res.status(400).json({ error: 'Username, role, and name are required' });
|
||||
}
|
||||
|
||||
db.run('UPDATE users SET username = ?, role = ?, name = ? WHERE id = ?', [username, role, name, id], function(err) {
|
||||
if (err) {
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
return res.status(409).json({ error: 'Username already exists' });
|
||||
}
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
res.json({ message: 'User updated successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.delete('/users/:id', isAdmin, (req, res) => {
|
||||
const { id } = req.params;
|
||||
// Prevenir que el admin se elimine a sí mismo
|
||||
@@ -321,7 +353,7 @@ apiRouter.delete('/users/:id', isAdmin, (req, res) => {
|
||||
|
||||
// --- Current User Settings ---
|
||||
apiRouter.get('/user', (req, res) => {
|
||||
db.get("SELECT id, username, role FROM users WHERE id = ?", [req.session.userId], (err, row) => {
|
||||
db.get("SELECT id, username, role, name FROM users WHERE id = ?", [req.session.userId], (err, row) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
@@ -331,9 +363,9 @@ apiRouter.get('/user', (req, res) => {
|
||||
});
|
||||
|
||||
apiRouter.post('/user', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Username is required' });
|
||||
const { username, password, name } = req.body;
|
||||
if (!username || !name) {
|
||||
return res.status(400).json({ error: 'Username and name are required' });
|
||||
}
|
||||
|
||||
if (password) {
|
||||
@@ -342,7 +374,7 @@ apiRouter.post('/user', (req, res) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Error hashing password' });
|
||||
}
|
||||
db.run('UPDATE users SET username = ?, password = ? WHERE id = ?', [username, hash, req.session.userId], function(err) {
|
||||
db.run('UPDATE users SET username = ?, password = ?, name = ? WHERE id = ?', [username, hash, name, req.session.userId], function(err) {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
@@ -350,12 +382,12 @@ apiRouter.post('/user', (req, res) => {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Si no se proporciona contraseña, solo actualizar el nombre de usuario
|
||||
db.run('UPDATE users SET username = ? WHERE id = ?', [username, req.session.userId], function(err) {
|
||||
// Si no se proporciona contraseña, solo actualizar el nombre de usuario y el nombre
|
||||
db.run('UPDATE users SET username = ?, name = ? WHERE id = ?', [username, name, req.session.userId], function(err) {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
res.json({ message: 'Username updated successfully' });
|
||||
res.json({ message: 'Username and name updated successfully' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user