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