added users management and permissions

This commit is contained in:
2026-02-23 15:01:58 +02:00
parent a993f8944d
commit 574de2c32d
11 changed files with 440 additions and 82 deletions
@@ -0,0 +1,231 @@
import { useState, useEffect } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import api from '../../services/api';
import Header from '../Header';
const PERMISSION_FIELDS = [
{ key: 'can_measure', label: 'Can Measure' },
{ key: 'can_manually_measure', label: 'Can Manually Measure' },
{ key: 'can_manage_entities', label: 'Can Manage Entities' },
{ key: 'can_edit_documents', label: 'Can Edit Documents' },
];
const DEFAULT_NEW_USER = {
username: '',
password: '',
can_measure: false,
can_manually_measure: false,
can_manage_entities: false,
can_edit_documents: false,
};
export default function UserManager() {
const { currentUser } = useAuth();
const [users, setUsers] = useState([]);
const [addFormOpen, setAddFormOpen] = useState(false);
const [newUser, setNewUser] = useState({ ...DEFAULT_NEW_USER });
const [error, setError] = useState('');
useEffect(() => {
api.get('/api/users/')
.then(res => setUsers(res.data.results ?? res.data))
.catch(() => setError('Failed to load users'));
}, []);
const handlePermissionChange = async (userId, field, value) => {
setError('');
try {
const res = await api.patch(`/api/users/${userId}/`, { [field]: value });
setUsers(prev => prev.map(u => u.id === userId ? { ...u, ...res.data } : u));
} catch {
setError('Failed to update permission');
}
};
const handleDeactivate = async (userId) => {
setError('');
try {
await api.delete(`/api/users/${userId}/`);
setUsers(prev => prev.map(u => u.id === userId ? { ...u, is_active: false } : u));
} catch {
setError('Failed to deactivate user');
}
};
const handleReactivate = async (userId) => {
setError('');
try {
const res = await api.patch(`/api/users/${userId}/`, { is_active: true });
setUsers(prev => prev.map(u => u.id === userId ? { ...u, ...res.data } : u));
} catch {
setError('Failed to reactivate user');
}
};
const handleAddUser = async (e) => {
e.preventDefault();
setError('');
try {
const res = await api.post('/api/users/', {
username: newUser.username,
password: newUser.password,
can_measure: newUser.can_measure,
can_manually_measure: newUser.can_manually_measure,
can_manage_entities: newUser.can_manage_entities,
can_edit_documents: newUser.can_edit_documents,
});
setUsers(prev => [...prev, res.data]);
setNewUser({ ...DEFAULT_NEW_USER });
setAddFormOpen(false);
} catch (err) {
const data = err.response?.data;
const msg = data?.username?.[0] || data?.detail || (typeof data === 'string' ? data : JSON.stringify(data)) || 'Failed to create user';
setError(msg);
}
};
return (
<div>
<Header />
<div style={{ padding: '24px', maxWidth: '900px' }}>
<h2 className="vehicle-detail-title">User Manager</h2>
{error && <div className="vehicle-error">{error}</div>}
<div style={{ marginBottom: '16px' }}>
<button
className="vehicle-add-btn"
onClick={() => setAddFormOpen(prev => !prev)}
>
{addFormOpen ? 'Cancel' : '+ Add User'}
</button>
</div>
{addFormOpen && (
<form onSubmit={handleAddUser} style={{ marginBottom: '24px', padding: '16px', border: '1px solid #e0e0e0', borderRadius: '8px', background: '#fafafa' }}>
<h3 style={{ margin: '0 0 14px 0', fontSize: '14px', color: '#667eea', fontWeight: 600 }}>New User</h3>
<div className="extra-fields">
<div className="extra-field">
<label>Username</label>
<input
type="text"
value={newUser.username}
onChange={e => setNewUser(prev => ({ ...prev, username: e.target.value }))}
required
autoFocus
/>
</div>
<div className="extra-field">
<label>Password</label>
<input
type="password"
value={newUser.password}
onChange={e => setNewUser(prev => ({ ...prev, password: e.target.value }))}
required
/>
</div>
{PERMISSION_FIELDS.map(({ key, label }) => (
<div key={key} className="extra-field" style={{ flexDirection: 'row', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
id={`new-${key}`}
checked={newUser[key]}
onChange={e => setNewUser(prev => ({ ...prev, [key]: e.target.checked }))}
/>
<label htmlFor={`new-${key}`} style={{ textTransform: 'none', letterSpacing: 'normal', cursor: 'pointer' }}>{label}</label>
</div>
))}
</div>
<div className="vehicle-form-actions" style={{ marginTop: '14px', paddingTop: '12px' }}>
<button
type="button"
className="vehicle-form-cancel"
onClick={() => { setAddFormOpen(false); setNewUser({ ...DEFAULT_NEW_USER }); }}
>
Cancel
</button>
<button
type="submit"
className="extra-save-btn"
disabled={!newUser.username.trim() || !newUser.password.trim()}
>
Create User
</button>
</div>
</form>
)}
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
<thead>
<tr style={{ borderBottom: '2px solid #e0e0e0', textAlign: 'left' }}>
<th style={{ padding: '10px 12px', color: '#555', fontWeight: 600 }}>Username</th>
<th style={{ padding: '10px 12px', color: '#555', fontWeight: 600 }}>Status</th>
{PERMISSION_FIELDS.map(({ key, label }) => (
<th key={key} style={{ padding: '10px 12px', color: '#555', fontWeight: 600 }}>{label}</th>
))}
<th style={{ padding: '10px 12px', color: '#555', fontWeight: 600 }}>Actions</th>
</tr>
</thead>
<tbody>
{users.map(u => (
<tr key={u.id} style={{ borderBottom: '1px solid #f0f0f0', background: u.is_active ? 'white' : '#fafafa' }}>
<td style={{ padding: '10px 12px', fontWeight: 600, color: '#333' }}>{u.username}</td>
<td style={{ padding: '10px 12px' }}>
<span style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: '10px',
fontSize: '12px',
fontWeight: 600,
background: u.is_active ? '#e8f5e9' : '#fce4ec',
color: u.is_active ? '#2e7d32' : '#c62828',
}}>
{u.is_active ? 'Active' : 'Inactive'}
</span>
</td>
{PERMISSION_FIELDS.map(({ key }) => (
<td key={key} style={{ padding: '10px 12px', textAlign: 'center' }}>
<input
type="checkbox"
checked={!!u[key]}
onChange={e => handlePermissionChange(u.id, key, e.target.checked)}
style={{ width: '18px', height: '18px', cursor: 'pointer' }}
/>
</td>
))}
<td style={{ padding: '10px 12px' }}>
{u.is_active ? (
<button
className="vehicle-form-cancel"
onClick={() => handleDeactivate(u.id)}
disabled={u.id === currentUser?.id}
title={u.id === currentUser?.id ? 'Cannot deactivate yourself' : undefined}
style={{ fontSize: '12px', padding: '5px 12px' }}
>
Deactivate
</button>
) : (
<button
className="extra-save-btn"
onClick={() => handleReactivate(u.id)}
style={{ fontSize: '12px', padding: '5px 12px' }}
>
Reactivate
</button>
)}
</td>
</tr>
))}
{users.length === 0 && (
<tr>
<td colSpan={PERMISSION_FIELDS.length + 3} style={{ padding: '20px', textAlign: 'center', color: '#999' }}>
No users found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
}