Initial commit: IT Nexus Web-App
This commit is contained in:
643
frontend/src/pages/UsersPage.jsx
Normal file
643
frontend/src/pages/UsersPage.jsx
Normal file
@@ -0,0 +1,643 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import userService from '../services/userService';
|
||||
import LoadingSpinner from '../components/common/LoadingSpinner';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const UsersPage = () => {
|
||||
const { isSuperAdmin } = useAuth();
|
||||
const [users, setUsers] = useState([]);
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const [azureGroups, setAzureGroups] = useState([]);
|
||||
const [azureGroupsLoading, setAzureGroupsLoading] = useState(false);
|
||||
const [importGroupId, setImportGroupId] = useState('');
|
||||
const [importRoleId, setImportRoleId] = useState('');
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [groupSearch, setGroupSearch] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role_id: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [usersData, rolesData] = await Promise.all([
|
||||
userService.getAll(),
|
||||
userService.getRoles(),
|
||||
]);
|
||||
setUsers(usersData);
|
||||
setRoles(rolesData);
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Laden der Daten');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingUser(null);
|
||||
setFormData({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role_id: roles[0]?.id || '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
is_active: true,
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleEdit = (user) => {
|
||||
setEditingUser(user);
|
||||
setFormData({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: '',
|
||||
role_id: user.role_id,
|
||||
first_name: user.first_name || '',
|
||||
last_name: user.last_name || '',
|
||||
is_active: user.is_active,
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const submitData = { ...formData };
|
||||
|
||||
// Remove password if empty (for edit)
|
||||
if (editingUser && !submitData.password) {
|
||||
delete submitData.password;
|
||||
}
|
||||
|
||||
if (editingUser) {
|
||||
await userService.update(editingUser.id, submitData);
|
||||
toast.success('Benutzer erfolgreich aktualisiert');
|
||||
} else {
|
||||
await userService.create(submitData);
|
||||
toast.success('Benutzer erfolgreich erstellt');
|
||||
}
|
||||
|
||||
setShowModal(false);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Fehler beim Speichern');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!window.confirm('Möchten Sie diesen Benutzer wirklich löschen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userService.delete(id);
|
||||
toast.success('Benutzer erfolgreich gelöscht');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (user) => {
|
||||
try {
|
||||
await userService.toggleStatus(user.id, !user.is_active);
|
||||
toast.success('Status erfolgreich geändert');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Fehler beim Ändern des Status');
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenImportModal = async () => {
|
||||
setImportGroupId('');
|
||||
setImportRoleId(roles[0]?.id || '');
|
||||
setGroupSearch('');
|
||||
setShowImportModal(true);
|
||||
setAzureGroupsLoading(true);
|
||||
try {
|
||||
const groups = await userService.getAzureGroups();
|
||||
setAzureGroups(groups);
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Fehler beim Laden der Azure-Gruppen');
|
||||
setShowImportModal(false);
|
||||
} finally {
|
||||
setAzureGroupsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importGroupId || !importRoleId) return;
|
||||
setImporting(true);
|
||||
try {
|
||||
const result = await userService.importFromAzure(importGroupId, importRoleId);
|
||||
toast.success(
|
||||
`Import abgeschlossen: ${result.imported} importiert, ${result.skipped} übersprungen` +
|
||||
(result.errors.length > 0 ? `, ${result.errors.length} Fehler` : '')
|
||||
);
|
||||
setShowImportModal(false);
|
||||
if (result.imported > 0) loadData();
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Fehler beim Importieren');
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleChange = async (userId, roleId) => {
|
||||
try {
|
||||
await userService.assignRole(userId, roleId);
|
||||
toast.success('Rolle erfolgreich zugewiesen');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Fehler beim Zuweisen der Rolle');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="main-content">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<div className="container">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
{isSuperAdmin() && (
|
||||
<div className="flex" style={{ gap: '0.5rem' }}>
|
||||
<button onClick={handleOpenImportModal} className="btn btn-secondary">
|
||||
☁️ Aus Azure importieren
|
||||
</button>
|
||||
<button onClick={handleCreate} className="btn btn-primary">
|
||||
+ Neuer Benutzer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<input
|
||||
className="form-input"
|
||||
type="text"
|
||||
placeholder="🔍 Suche nach Name, Benutzername, E-Mail oder Rolle…"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
style={{ maxWidth: 420 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="card">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzername</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Name</th>
|
||||
<th>Rolle</th>
|
||||
<th>Status</th>
|
||||
<th>Letzter Login</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(() => {
|
||||
const q = search.toLowerCase();
|
||||
const filtered = q
|
||||
? users.filter(u =>
|
||||
u.username?.toLowerCase().includes(q) ||
|
||||
u.email?.toLowerCase().includes(q) ||
|
||||
u.first_name?.toLowerCase().includes(q) ||
|
||||
u.last_name?.toLowerCase().includes(q) ||
|
||||
u.role_name?.toLowerCase().includes(q)
|
||||
)
|
||||
: users;
|
||||
if (filtered.length === 0) return (
|
||||
<tr><td colSpan="7" className="text-center">Keine Benutzer gefunden</td></tr>
|
||||
);
|
||||
return filtered.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
{user.first_name || user.last_name
|
||||
? `${user.first_name || ''} ${
|
||||
user.last_name || ''
|
||||
}`.trim()
|
||||
: '-'}
|
||||
</td>
|
||||
<td>
|
||||
<span className="role-badge">{user.role_name}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`status-badge ${
|
||||
user.is_active
|
||||
? 'status-aktiv'
|
||||
: 'status-inaktiv'
|
||||
}`}
|
||||
>
|
||||
{user.is_active ? 'Aktiv' : 'Inaktiv'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{user.last_login
|
||||
? new Date(user.last_login).toLocaleString('de-DE')
|
||||
: 'Nie'}
|
||||
</td>
|
||||
<td>
|
||||
{isSuperAdmin() && (
|
||||
<div className="table-actions">
|
||||
<button
|
||||
onClick={() => handleEdit(user)}
|
||||
className="btn btn-primary btn-small"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleToggleStatus(user)}
|
||||
className="btn btn-secondary btn-small"
|
||||
>
|
||||
{user.is_active
|
||||
? 'Deaktivieren'
|
||||
: 'Aktivieren'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(user.id)}
|
||||
className="btn btn-danger btn-small"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Azure Import Modal */}
|
||||
{showImportModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowImportModal(false)}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">Benutzer aus Azure AD importieren</h2>
|
||||
<button className="modal-close" onClick={() => setShowImportModal(false)}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{azureGroupsLoading ? (
|
||||
<p style={{ textAlign: 'center', padding: '1rem' }}>Gruppen werden geladen...</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Azure AD Gruppe*</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="Gruppe suchen..."
|
||||
value={groupSearch}
|
||||
onChange={(e) => setGroupSearch(e.target.value)}
|
||||
style={{ marginBottom: '0.4rem' }}
|
||||
/>
|
||||
<div style={{
|
||||
border: '1px solid var(--border-color)',
|
||||
borderRadius: '6px',
|
||||
maxHeight: '220px',
|
||||
overflowY: 'auto',
|
||||
background: 'var(--bg-secondary)',
|
||||
}}>
|
||||
{azureGroups
|
||||
.filter(g => g.displayName.toLowerCase().includes(groupSearch.toLowerCase()))
|
||||
.map((g) => (
|
||||
<div
|
||||
key={g.id}
|
||||
onClick={() => setImportGroupId(g.id)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid var(--border-color)',
|
||||
background: importGroupId === g.id ? 'var(--primary)' : 'transparent',
|
||||
color: importGroupId === g.id ? '#fff' : 'var(--text-primary)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 500, fontSize: '14px' }}>{g.displayName}</div>
|
||||
{g.description && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
opacity: 0.7,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '100%',
|
||||
}}>
|
||||
{g.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{azureGroups.filter(g => g.displayName.toLowerCase().includes(groupSearch.toLowerCase())).length === 0 && (
|
||||
<div style={{ padding: '8px 12px', color: 'var(--text-muted)', fontSize: '13px' }}>
|
||||
Keine Gruppen gefunden
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{importGroupId && (
|
||||
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' }}>
|
||||
✓ Ausgewählt: <strong>{azureGroups.find(g => g.id === importGroupId)?.displayName}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Rolle für importierte Benutzer *</label>
|
||||
{(() => {
|
||||
const ROLE_META = {
|
||||
super_admin: { icon: '👑', color: '#ef4444', bg: '#fef2f2', label: 'Super Admin' },
|
||||
admin: { icon: '🛡️', color: '#f97316', bg: '#fff7ed', label: 'Admin' },
|
||||
support: { icon: '🎧', color: '#3b82f6', bg: '#eff6ff', label: 'IT-Support' },
|
||||
bearbeiter: { icon: '🔧', color: '#8b5cf6', bg: '#f5f3ff', label: 'Bearbeiter' },
|
||||
benutzer: { icon: '👤', color: '#6b7280', bg: '#f9fafb', label: 'Benutzer' },
|
||||
hr_personal: { icon: '🧑💼', color: '#10b981', bg: '#f0fdf4', label: 'HR / Personal' },
|
||||
buchhaltung: { icon: '💶', color: '#a78bfa', bg: '#faf5ff', label: 'Buchhaltung' },
|
||||
};
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
{roles.map(role => {
|
||||
const meta = ROLE_META[role.name] || { icon: '🔵', color: '#6b7280', bg: '#f9fafb', label: role.name };
|
||||
const selected = String(importRoleId) === String(role.id);
|
||||
return (
|
||||
<button
|
||||
key={role.id}
|
||||
type="button"
|
||||
onClick={() => setImportRoleId(role.id)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '10px 12px', borderRadius: 8, cursor: 'pointer',
|
||||
border: `2px solid ${selected ? meta.color : 'var(--border-color)'}`,
|
||||
background: selected ? meta.bg : 'var(--bg-secondary)',
|
||||
textAlign: 'left', transition: 'all 0.15s',
|
||||
boxShadow: selected ? `0 0 0 3px ${meta.color}22` : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: '50%', display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center', fontSize: 16,
|
||||
background: selected ? meta.color + '22' : 'var(--bg-tertiary)',
|
||||
flexShrink: 0,
|
||||
}}>{meta.icon}</div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: selected ? meta.color : 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{meta.label}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.3, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
|
||||
{role.description}
|
||||
</div>
|
||||
</div>
|
||||
{selected && (
|
||||
<div style={{ marginLeft: 'auto', color: meta.color, fontSize: 16, flexShrink: 0 }}>✓</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '0.5rem' }}>
|
||||
Bereits vorhandene Benutzer (gleiche E-Mail oder Azure ID) werden übersprungen.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={() => setShowImportModal(false)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleImport}
|
||||
disabled={importing || azureGroupsLoading || !importGroupId || !importRoleId}
|
||||
>
|
||||
{importing ? 'Importiere...' : 'Importieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{showModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowModal(false)}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">
|
||||
{editingUser ? 'Benutzer bearbeiten' : 'Neuer Benutzer'}
|
||||
</h2>
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Benutzername*</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={formData.username}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, username: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">E-Mail*</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-input"
|
||||
value={formData.email}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, email: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
Passwort{editingUser ? '' : '*'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-input"
|
||||
value={formData.password}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, password: e.target.value })
|
||||
}
|
||||
required={!editingUser}
|
||||
placeholder={
|
||||
editingUser
|
||||
? 'Leer lassen, um nicht zu ändern'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Vorname</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={formData.first_name}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
first_name: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Nachname</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={formData.last_name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, last_name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Rolle *</label>
|
||||
{(() => {
|
||||
const ROLE_META = {
|
||||
super_admin: { icon: '👑', color: '#ef4444', bg: '#fef2f2', label: 'Super Admin' },
|
||||
admin: { icon: '🛡️', color: '#f97316', bg: '#fff7ed', label: 'Admin' },
|
||||
support: { icon: '🎧', color: '#3b82f6', bg: '#eff6ff', label: 'IT-Support' },
|
||||
bearbeiter: { icon: '🔧', color: '#8b5cf6', bg: '#f5f3ff', label: 'Bearbeiter' },
|
||||
benutzer: { icon: '👤', color: '#6b7280', bg: '#f9fafb', label: 'Benutzer' },
|
||||
hr_personal: { icon: '🧑💼', color: '#10b981', bg: '#f0fdf4', label: 'HR / Personal' },
|
||||
buchhaltung: { icon: '💶', color: '#a78bfa', bg: '#faf5ff', label: 'Buchhaltung' },
|
||||
};
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
{roles.map(role => {
|
||||
const meta = ROLE_META[role.name] || { icon: '🔵', color: '#6b7280', bg: '#f9fafb', label: role.name };
|
||||
const selected = formData.role_id === role.id;
|
||||
return (
|
||||
<button
|
||||
key={role.id}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, role_id: role.id })}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '10px 12px', borderRadius: 8, cursor: 'pointer',
|
||||
border: `2px solid ${selected ? meta.color : 'var(--border-color)'}`,
|
||||
background: selected ? meta.bg : 'var(--bg-secondary)',
|
||||
textAlign: 'left', transition: 'all 0.15s',
|
||||
boxShadow: selected ? `0 0 0 3px ${meta.color}22` : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: '50%', display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center', fontSize: 16,
|
||||
background: selected ? meta.color + '22' : 'var(--bg-tertiary)',
|
||||
flexShrink: 0,
|
||||
}}>{meta.icon}</div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: selected ? meta.color : 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{meta.label}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.3, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
|
||||
{role.description}
|
||||
</div>
|
||||
</div>
|
||||
{selected && (
|
||||
<div style={{ marginLeft: 'auto', color: meta.color, fontSize: 16, flexShrink: 0 }}>✓</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{!formData.role_id && (
|
||||
<div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>Bitte eine Rolle auswählen</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_active}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
is_active: e.target.checked,
|
||||
})
|
||||
}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
Benutzer ist aktiv
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="card-footer">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowModal(false)}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersPage;
|
||||
Reference in New Issue
Block a user