added users management and permissions
parent
a993f8944d
commit
574de2c32d
Binary file not shown.
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-02-22 19:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("api", "0003_documentcounter"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="can_edit_documents",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="can_manage_entities",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="can_manually_measure",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="can_measure",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue