added users management and permissions

master
kikimor 1 week ago
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),
),
]

@ -12,6 +12,10 @@ class User(AbstractUser):
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='viewer')
is_admin = models.BooleanField(default=False)
can_measure = models.BooleanField(default=False)
can_manually_measure = models.BooleanField(default=False)
can_manage_entities = models.BooleanField(default=False)
can_edit_documents = models.BooleanField(default=False)
class Meta:
db_table = 'api_user'

@ -11,7 +11,8 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name',
'role', 'is_admin', 'is_active', 'date_joined', 'password']
'role', 'is_admin', 'is_active', 'date_joined', 'password',
'can_measure', 'can_manually_measure', 'can_manage_entities', 'can_edit_documents']
read_only_fields = ['id', 'date_joined']
extra_kwargs = {
'password': {'write_only': True}
@ -40,7 +41,8 @@ class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name',
'role', 'is_admin', 'is_active', 'date_joined']
'role', 'is_admin', 'is_active', 'date_joined',
'can_measure', 'can_manually_measure', 'can_manage_entities', 'can_edit_documents']
read_only_fields = ['id', 'date_joined']

@ -14,6 +14,11 @@ from nomenclatures.models import Nomenclature, NomenclatureEntry
from scalesapp.sse import sse_broadcast_update
class IsAdminUser(IsAuthenticated):
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated and request.user.is_admin)
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint for user management.
@ -22,7 +27,7 @@ class UserViewSet(viewsets.ModelViewSet):
create: Create a new user
retrieve: Get a specific user
update: Update a user
destroy: Delete a user
destroy: Soft-delete a user (sets is_active=False)
me: Get current authenticated user
change_password: Change password for current user
"""
@ -31,13 +36,24 @@ class UserViewSet(viewsets.ModelViewSet):
filterset_fields = ['role', 'is_admin', 'is_active']
ordering = ['username']
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def get_permissions(self):
if self.action in ('me', 'change_password'):
return [IsAuthenticated()]
return [IsAuthenticated(), IsAdminUser()]
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
instance.is_active = False
instance.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=False, methods=['get'])
def me(self, request):
"""Get current authenticated user details"""
serializer = UserDetailSerializer(request.user)
return Response(serializer.data)
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated],
@action(detail=False, methods=['post'],
url_path='change-password', url_name='change_password')
def change_password(self, request):
"""Change password for current user"""

@ -5,6 +5,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
import Login from './components/Users/Login';
import Main from './components/Main';
import ReportEditor from './components/ReportEditor/ReportEditor';
import UserManager from './components/Users/UserManager';
import './App.css';
import { NomenclatureProvider } from './contexts/NomenclatureContext';
import { NomenclatureDataProvider } from './contexts/NomenclatureDataContext';
@ -40,7 +41,7 @@ import { NomenclatureDataProvider } from './contexts/NomenclatureDataContext';
// }
function AppContent() {
const { user, login, logout, loading, isAuthenticated } = useAuth();
const { user, currentUser, login, logout, loading, isAuthenticated } = useAuth();
if (loading) {
return (
@ -59,7 +60,8 @@ function AppContent() {
<NomenclatureDataProvider>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/report-editor" element={<ReportEditor />} />
<Route path="/report-editor" element={currentUser?.can_edit_documents ? <ReportEditor /> : <Navigate to="/" />} />
<Route path="/users" element={currentUser?.is_admin ? <UserManager /> : <Navigate to="/" />} />
</Routes>
</NomenclatureDataProvider>
</NomenclatureProvider>

@ -27,20 +27,33 @@ function Header() {
<header className="app-header">
<div className="header-content">
<div className="header-left">
<button
className="nav-button"
onClick={() => navigate('/report-editor')}
title="Report Editor"
>
📝
</button>
<button
className="nav-button"
onClick={() => setShowNomenclatureManager(true)}
title="Nomenclatures"
>
📋
</button>
{currentUser?.can_edit_documents && (
<button
className="nav-button"
onClick={() => navigate('/report-editor')}
title="Report Editor"
>
📝
</button>
)}
{currentUser?.can_manage_entities && (
<button
className="nav-button"
onClick={() => setShowNomenclatureManager(true)}
title="Nomenclatures"
>
📋
</button>
)}
{currentUser?.is_admin && (
<button
className="nav-button"
onClick={() => navigate('/users')}
title="User Manager"
>
👥
</button>
)}
<h1>ScalesApp - Real-time Data Monitor</h1>
</div>
<div className="header-right">

@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import api from '../../services/api';
import { useAuth } from '../../contexts/AuthContext';
import './NomenclatureManager.css';
const EMPTY_FORM = {
@ -13,6 +14,8 @@ const EMPTY_FORM = {
};
function NomenclatureManager({ onClose }) {
const { currentUser } = useAuth();
const canEdit = !!currentUser?.can_manage_entities;
const [nomenclatures, setNomenclatures] = useState([]);
const [selectedId, setSelectedId] = useState(null);
const [form, setForm] = useState(null); // null = nothing selected
@ -179,7 +182,7 @@ function NomenclatureManager({ onClose }) {
<div className="nm-body">
{/* Left panel - List */}
<div className="nm-list-panel">
<button className="nm-add-btn" onClick={handleAdd}>+ Add New</button>
{canEdit && <button className="nm-add-btn" onClick={handleAdd}>+ Add New</button>}
{isLoading && <div className="nm-loading">Loading...</div>}
@ -196,13 +199,15 @@ function NomenclatureManager({ onClose }) {
{nom.kind === 'lookup' ? 'Lookup' : 'Field'} / {nom.applies_to}
</span>
</div>
<button
className="nm-edit-btn"
onClick={(e) => { e.stopPropagation(); handleSelect(nom); }}
title="Edit"
>
&#9998;
</button>
{canEdit && (
<button
className="nm-edit-btn"
onClick={(e) => { e.stopPropagation(); handleSelect(nom); }}
title="Edit"
>
&#9998;
</button>
)}
</div>
))}
@ -216,14 +221,14 @@ function NomenclatureManager({ onClose }) {
<div className="nm-form-panel">
{!form ? (
<div className="nm-placeholder">
Select a nomenclature to edit or click "+ Add New"
{canEdit ? 'Select a nomenclature to edit or click "+ Add New"' : 'Select a nomenclature to view'}
</div>
) : (
<div className="nm-form">
{error && <div className="nm-error">{error}</div>}
<div className="nm-form-title">
{selectedId ? 'Edit Nomenclature' : 'New Nomenclature'}
{!canEdit ? 'View Nomenclature' : selectedId ? 'Edit Nomenclature' : 'New Nomenclature'}
</div>
{/* Basic fields */}
@ -235,7 +240,7 @@ function NomenclatureManager({ onClose }) {
value={form.code}
onChange={e => setForm(prev => ({ ...prev, code: e.target.value }))}
placeholder="e.g. cargo_type"
disabled={isSaving}
disabled={isSaving || !canEdit}
/>
</div>
@ -246,7 +251,7 @@ function NomenclatureManager({ onClose }) {
value={form.name}
onChange={e => setForm(prev => ({ ...prev, name: e.target.value }))}
placeholder="e.g. Cargo Type"
disabled={isSaving}
disabled={isSaving || !canEdit}
/>
</div>
@ -255,7 +260,7 @@ function NomenclatureManager({ onClose }) {
<select
value={form.applies_to}
onChange={e => setForm(prev => ({ ...prev, applies_to: e.target.value }))}
disabled={isSaving}
disabled={isSaving || !canEdit}
>
<option value="vehicle">Vehicle</option>
<option value="container">Container</option>
@ -267,7 +272,7 @@ function NomenclatureManager({ onClose }) {
<select
value={form.kind}
onChange={e => setForm(prev => ({ ...prev, kind: e.target.value }))}
disabled={isSaving}
disabled={isSaving || !canEdit}
>
<option value="lookup">Lookup Table</option>
<option value="field">Custom Field</option>
@ -280,7 +285,7 @@ function NomenclatureManager({ onClose }) {
<select
value={form.display_field}
onChange={e => setForm(prev => ({ ...prev, display_field: e.target.value }))}
disabled={isSaving}
disabled={isSaving || !canEdit}
>
{form.fields.filter(f => f.key).map(f => (
<option key={f.key} value={f.key}>{f.key}</option>
@ -294,9 +299,11 @@ function NomenclatureManager({ onClose }) {
<div className="nm-section">
<div className="nm-section-header">
<h3>Fields</h3>
<button className="nm-section-add" onClick={addField} disabled={isSaving}>
+ Add Field
</button>
{canEdit && (
<button className="nm-section-add" onClick={addField} disabled={isSaving || !canEdit}>
+ Add Field
</button>
)}
</div>
<div className="nm-fields-table">
@ -314,19 +321,19 @@ function NomenclatureManager({ onClose }) {
value={field.key}
onChange={e => updateField(idx, 'key', e.target.value)}
placeholder="key"
disabled={isSaving}
disabled={isSaving || !canEdit}
/>
<input
type="text"
value={field.label}
onChange={e => updateField(idx, 'label', e.target.value)}
placeholder="Label"
disabled={isSaving}
disabled={isSaving || !canEdit}
/>
<select
value={field.field_type}
onChange={e => updateField(idx, 'field_type', e.target.value)}
disabled={isSaving}
disabled={isSaving || !canEdit}
>
<option value="text">Text</option>
<option value="number">Number</option>
@ -337,7 +344,7 @@ function NomenclatureManager({ onClose }) {
type="checkbox"
checked={field.required}
onChange={e => updateField(idx, 'required', e.target.checked)}
disabled={isSaving}
disabled={isSaving || !canEdit}
/>
<button
className="nm-remove-btn"
@ -354,7 +361,7 @@ function NomenclatureManager({ onClose }) {
{/* Actions */}
<div className="nm-form-actions">
{selectedId && (
{canEdit && selectedId && (
<button
className="nm-delete-btn"
onClick={handleDelete}
@ -369,15 +376,17 @@ function NomenclatureManager({ onClose }) {
onClick={handleCancel}
disabled={isSaving}
>
Cancel
</button>
<button
className="nm-save-btn"
onClick={handleSave}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save'}
{canEdit ? 'Cancel' : 'Close'}
</button>
{canEdit && (
<button
className="nm-save-btn"
onClick={handleSave}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save'}
</button>
)}
</div>
</div>
</div>

@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useNomenclatureData } from '../../contexts/NomenclatureDataContext';
import { useAuth } from '../../contexts/AuthContext';
import FormOverlay from './FormOverlay';
import NomenclatureEntryForm from './NomenclatureEntryForm';
import './NomenclatureManagementOverlay.css';
@ -13,6 +14,9 @@ export default function NomenclatureManagementOverlay({
initialSelection = null, // entry ID to pre-select
onSelect, // called in 'select' mode when user picks entry
}) {
const { currentUser } = useAuth();
const canEdit = !!currentUser?.can_manage_entities;
const {
definitions, entries,
createEntry, updateEntry, deleteEntry, toggleEntryActive,
@ -238,9 +242,11 @@ export default function NomenclatureManagementOverlay({
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button className="nui-mgmt-add-btn" onClick={handleAddClick} type="button">
+ Add New
</button>
{canEdit && (
<button className="nui-mgmt-add-btn" onClick={handleAddClick} type="button">
+ Add New
</button>
)}
</div>
{error && <div className="nui-mgmt-error">{error}</div>}
@ -255,7 +261,7 @@ export default function NomenclatureManagementOverlay({
<th key={f.key}>{f.label || f.key}</th>
))}
<th className="nui-mgmt-th-status">Active</th>
<th className="nui-mgmt-th-actions"></th>
{canEdit && <th className="nui-mgmt-th-actions"></th>}
</tr>
</thead>
<tbody>
@ -286,22 +292,25 @@ export default function NomenclatureManagementOverlay({
<td className="nui-mgmt-td-status">
<span
className={`nui-mgmt-active-badge ${entry.is_active ? 'nui-mgmt-active-badge--active' : 'nui-mgmt-active-badge--inactive'}`}
onClick={(e) => { e.stopPropagation(); setSelectedId(entry.id); toggleEntryActive(entry.id, entry.is_active); }}
title={entry.is_active ? 'Click to deactivate' : 'Click to activate'}
onClick={canEdit ? (e) => { e.stopPropagation(); setSelectedId(entry.id); toggleEntryActive(entry.id, entry.is_active); } : undefined}
title={canEdit ? (entry.is_active ? 'Click to deactivate' : 'Click to activate') : undefined}
style={canEdit ? { cursor: 'pointer' } : undefined}
>
{entry.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="nui-mgmt-td-actions">
<button
className="nui-mgmt-edit-btn"
onClick={(e) => { e.stopPropagation(); setSelectedId(entry.id); setTimeout(() => { setFormMode('edit'); setEditData(entry.data); setShowForm(true); }, 0); }}
title="Edit"
type="button"
>
&#9998;
</button>
</td>
{canEdit && (
<td className="nui-mgmt-td-actions">
<button
className="nui-mgmt-edit-btn"
onClick={(e) => { e.stopPropagation(); setSelectedId(entry.id); setTimeout(() => { setFormMode('edit'); setEditData(entry.data); setShowForm(true); }, 0); }}
title="Edit"
type="button"
>
&#9998;
</button>
</td>
)}
</tr>
))
)}
@ -316,7 +325,7 @@ export default function NomenclatureManagementOverlay({
{searchTerm && ` (filtered)`}
</div>
<div className="nui-mgmt-footer-actions">
{selectedEntry && (
{canEdit && selectedEntry && (
<>
<button
className="nui-mgmt-delete-btn"

@ -4,9 +4,9 @@ import { DataProvider, useReportData } from './DataContext';
import { BandProvider } from './BandContext';
import Toolbar from './Toolbar';
import EditorCanvas from './EditorCanvas';
import ConfigPanel from './ConfigPanel';
import CharacterPalette from './CharacterPalette';
import ObjectInspector from './ObjectInspector';
import { useNomenclatureData } from '../../contexts/NomenclatureDataContext';
import api from '../../services/api';
import './ReportEditor.css';
@ -23,7 +23,6 @@ function ReportEditorContent() {
const [toolMode, setToolMode] = useState('select');
const [borderStyle, setBorderStyle] = useState('single');
const [previewMode, setPreviewMode] = useState(false);
const [showConfigPanel, setShowConfigPanel] = useState(false);
const [selectedChar, setSelectedChar] = useState('─');
const [showCharacterPalette, setShowCharacterPalette] = useState(false);
@ -37,6 +36,7 @@ function ReportEditorContent() {
// Auto-load report and vehicle data passed via router state
const location = useLocation();
const { setData } = useReportData();
const { definitions, entries, isLoaded } = useNomenclatureData();
useEffect(() => {
const { reportName, vehicleData } = location.state || {};
@ -66,6 +66,55 @@ function ReportEditorContent() {
});
}, []);
// When no vehicle data was passed (direct navigation), auto-set sample data
// once nomenclatures have loaded so field bindings can be designed
useEffect(() => {
if (!isLoaded || location.state?.vehicleData) return;
const extra = {};
for (const def of Object.values(definitions)) {
if (def.kind === 'lookup') {
const activeEntries = (entries[def.code] || []).filter(e => e.is_active);
const firstEntry = activeEntries[0];
if (firstEntry?.data && Object.keys(firstEntry.data).length > 0) {
extra[def.name] = { ...firstEntry.data };
} else if (def.fields?.length > 0) {
const sample = {};
for (const field of def.fields) {
if (field.field_type === 'number') sample[field.key] = 0;
else if (field.field_type === 'bool') sample[field.key] = false;
else sample[field.key] = 'Sample';
}
extra[def.name] = sample;
} else {
extra[def.name] = {};
}
} else {
extra[def.name] = 'Sample Text';
}
}
setData({
vehicle: {
vehicle_number: 'АА1234ВВ',
trailer1_number: В5678ВВ',
trailer2_number: null,
driver_pid: '123456789',
tare: 8500,
tare_date: '2026-02-22T08:00:00Z',
tare_user_name: 'operator',
gross: 28500,
gross_date: '2026-02-22T10:30:00Z',
gross_user_name: 'operator',
net: 20000,
net_date: '2026-02-22T10:30:00Z',
net_user_name: 'operator',
doc_number: '2026-001',
extra,
}
});
}, [isLoaded]);
const handleAddElement = (element) => {
setReport(prev => {
// Check if the new element is positioned inside a band
@ -332,9 +381,6 @@ function ReportEditorContent() {
setSelectedElementIds([]);
setToolMode('select');
setShowCharacterPalette(false);
if (showConfigPanel) {
setShowConfigPanel(false);
}
} else if (e.key.toLowerCase() === 'd' && !e.ctrlKey && !e.metaKey && e.target.tagName !== 'INPUT') {
handleToolChange('drawTable');
} else if (e.key === 'Delete' && selectedElementIds.length > 0) {
@ -347,7 +393,7 @@ function ReportEditorContent() {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [showConfigPanel, selectedElementIds, toolMode, report.elements]);
}, [selectedElementIds, toolMode, report.elements]);
// Get the selected element for the Object Inspector
const selectedElement = selectedElementIds.length === 1
@ -363,7 +409,6 @@ function ReportEditorContent() {
onBorderStyleChange={setBorderStyle}
previewMode={previewMode}
onTogglePreview={handleTogglePreview}
onConfigureAPI={() => setShowConfigPanel(true)}
onSave={handleSave}
onLoad={handleLoadClick}
reportName={report.name}
@ -393,12 +438,6 @@ function ReportEditorContent() {
onUpdate={(updates) => handleElementUpdate(selectedElementIds[0], updates)}
/>
)}
<ConfigPanel
apiEndpoint={report.apiEndpoint}
onApiEndpointChange={handleApiEndpointChange}
isOpen={showConfigPanel}
onClose={() => setShowConfigPanel(false)}
/>
{showCharacterPalette && (
<CharacterPalette
selectedChar={selectedChar}

@ -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…
Cancel
Save