Design: Admin mit Tabelle, Mitarbeiter-Formular als Dialog, AdminPage.css neu

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 14:42:34 +00:00
1 parent 0a881fce8f
commit 6adeac1642
3 files changed
+211 -498

No files matched your search

+1 -2
View File
@@ -7,8 +7,7 @@ export default function AdminRoute({ children }) {
if (loading) { if (loading) {
return ( return (
<div className="text-center p-4" style={{ marginTop: 80 }}> <div className="loading">
<div className="spinner"></div>
<p>Wird geladen …</p> <p>Wird geladen …</p>
</div> </div>
) )
+18 -362
View File
@@ -1,364 +1,20 @@
/* .page (global.css, wird danach geladen) gewinnt bei gleicher Spezifitaet: darum .page.admin-page */ /* Admin: Mitarbeiter-Tabelle, Zugangsdaten, Dialog. Karten, Rahmen und Schatten gibt es nicht mehr; Abstände zwischen den */
.page.admin-page { /* Abschnitten liefert .page section + section (global.css). AdminPage.css lädt vor global.css, darum haben Regeln, die eine */
max-width: 1480px; /* Global-Klasse überstimmen, die Spezifität 0,2,0. Zugangsdaten (.credential*) und .sr-only stehen in global.css. */
} .page.admin-page { max-width: 1480px; }
.admin-page { .admin-notice { margin-bottom: 16px; }
padding-bottom: 48px; .admin-lead { margin: 0 0 8px; }
} .admin-shortcode { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; letter-spacing: .04em; }
.admin-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px 16px; }
.credential-list { display: flex; flex-wrap: wrap; gap: 8px 32px; margin-top: 8px; }
.admin-header { /* Admin-Haken im Dialog: Titel und Erklärung untereinander, der Haken oben links */
display: flex; .check.admin-check { align-items: flex-start; }
align-items: flex-end; .admin-check small { display: block; margin-top: 2px; font-size: 13px; line-height: 18px; color: var(--text-2); }
justify-content: space-between; .admin-check.is-disabled { cursor: not-allowed; opacity: .55; }
gap: 24px;
margin-bottom: 24px;
}
.admin-header h1 { /* 403 */
margin: 4px 0 6px; .admin-access-denied { max-width: 540px; margin: 12vh auto 0; }
font-size: clamp(28px, 3vw, 38px); .admin-access-denied > span { font-size: 12px; font-weight: 500; color: var(--red-fg); }
line-height: 1.1; .admin-access-denied h1 { margin: 4px 0 8px; font-size: 28px; line-height: 34px; font-weight: 700; letter-spacing: -.02em; }
letter-spacing: -0.03em; .admin-access-denied p { color: var(--text-2); }
}
.admin-header p,
.admin-current-user span,
.admin-section-head span {
color: var(--text-muted);
font-size: 13px;
}
.admin-eyebrow {
color: var(--accent);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.11em;
text-transform: uppercase;
}
.admin-current-user {
min-width: 220px;
padding-left: 16px;
border-left: 2px solid var(--accent);
}
.admin-current-user span,
.admin-current-user strong {
display: block;
}
.admin-current-user strong {
margin-top: 3px;
font-size: 14px;
}
.admin-tabs {
display: flex;
gap: 0;
margin-bottom: 22px;
border-bottom: 1px solid var(--admin-line);
}
.admin-tabs button {
position: relative;
padding: 12px 18px;
border: 0;
background: transparent;
color: var(--text-muted);
font: inherit;
font-size: 14px;
font-weight: 650;
cursor: pointer;
}
.admin-tabs button::after {
content: '';
position: absolute;
right: 18px;
bottom: -1px;
left: 18px;
height: 2px;
background: transparent;
}
.admin-tabs button:hover,
.admin-tabs button.active {
color: var(--text);
}
.admin-tabs button.active::after {
background: var(--accent);
}
.admin-notice {
margin-bottom: 16px;
padding: 11px 14px;
border: 1px solid var(--admin-line);
border-left-width: 3px;
background: var(--surface-1);
font-size: 14px;
}
.admin-notice.success { border-left-color: var(--ok); }
.admin-notice.error { border-left-color: var(--danger); }
.admin-form-card,
.admin-list-card {
border: 1px solid var(--admin-line);
background: var(--surface-1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}
.admin-section-head {
display: flex;
min-height: 62px;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 14px 16px;
border-bottom: 1px solid var(--admin-line);
}
.admin-section-head h2,
.admin-section-head h3 {
margin: 0 0 3px;
font-size: 15px;
line-height: 1.3;
}
.admin-icon-button {
display: inline-flex;
min-width: 34px;
min-height: 34px;
align-items: center;
justify-content: center;
gap: 7px;
padding: 7px 9px;
border: 1px solid var(--admin-line-strong);
border-radius: 6px;
background: transparent;
color: var(--text-muted);
font: inherit;
font-size: 12px;
font-weight: 650;
cursor: pointer;
}
.admin-icon-button:hover {
border-color: var(--accent-ring);
color: var(--text);
background: var(--accent-soft);
}
.admin-icon-button.danger:hover {
border-color: var(--danger);
color: var(--red-fg);
background: var(--red-bg);
}
.admin-primary-button {
display: inline-flex;
min-height: 40px;
align-items: center;
justify-content: center;
gap: 8px;
padding: 9px 15px;
border: 1px solid var(--accent);
border-radius: 6px;
background: var(--accent);
color: #fff;
font: inherit;
font-size: 13px;
font-weight: 750;
cursor: pointer;
}
.admin-primary-button:hover { background: var(--accent-hover); }
.admin-primary-button:disabled { opacity: 0.55; cursor: wait; }
.admin-split-layout {
display: grid;
grid-template-columns: minmax(290px, 360px) minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.admin-form-card {
position: sticky;
top: 20px;
padding-bottom: 16px;
}
.admin-form-card > .admin-field,
.admin-form-card > .admin-checkbox,
.admin-form-card > .admin-primary-button {
margin-right: 16px;
margin-left: 16px;
}
.admin-field {
display: block;
margin-top: 13px;
}
.admin-field > span {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 6px;
color: var(--text-muted);
font-size: 12px;
font-weight: 650;
}
.admin-field em {
color: var(--text-faint);
font-style: normal;
font-weight: 500;
}
.admin-form-card > .admin-primary-button {
width: calc(100% - 32px);
margin-top: 16px;
}
.admin-checkbox {
display: flex;
align-items: flex-start;
gap: 10px;
margin-top: 16px;
padding: 11px;
border: 1px solid var(--admin-line);
cursor: pointer;
}
.admin-checkbox input { margin-top: 2px; accent-color: var(--accent); }
.admin-checkbox span,
.admin-checkbox strong,
.admin-checkbox small { display: block; }
.admin-checkbox strong { font-size: 13px; }
.admin-checkbox small { margin-top: 3px; color: var(--text-muted); line-height: 1.35; }
.admin-checkbox.disabled { cursor: not-allowed; opacity: 0.65; }
.admin-list-card { min-width: 0; }
.admin-table-wrap { width: 100%; overflow-x: auto; }
.admin-table {
width: 100%;
border-collapse: collapse;
white-space: nowrap;
}
.admin-table th {
padding: 10px 13px;
border-bottom: 1px solid var(--admin-line);
color: var(--text-faint);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-align: left;
text-transform: uppercase;
}
.admin-table td {
padding: 12px 13px;
border-bottom: 1px solid var(--line);
color: var(--text);
font-size: 13px;
vertical-align: middle;
}
.admin-table tbody tr:last-child td { border-bottom: 0; }
.admin-table tbody tr:hover td { background: var(--bg-subtle); }
.admin-table td > strong,
.admin-table td > span { display: block; }
.admin-table td > span { margin-top: 3px; color: var(--text-muted); font-size: 12px; }
.admin-row-actions {
display: flex;
justify-content: flex-end;
gap: 6px;
}
/* Tabellenzellen: .admin-table td > span (0,1,2) setzt display/Farbe/Schrift; die Selektoren hier sind spezifischer (0,2,1) */
.admin-table td > .admin-status,
.admin-table td > .admin-role {
display: inline-flex;
width: fit-content;
align-items: center;
padding: 3px 7px;
border: 1px solid var(--admin-line-strong);
border-radius: 4px;
color: var(--text-muted);
font-size: 11px;
font-weight: 700;
}
.admin-table td > .admin-status.active,
.admin-table td > .admin-role.admin {
border-color: var(--green-fg);
color: var(--green-fg);
}
.admin-table td > .admin-shortcode {
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
letter-spacing: 0.04em;
}
.admin-credential-result {
display: grid;
grid-template-columns: minmax(190px, 1fr) minmax(180px, auto) minmax(180px, auto) auto;
gap: 16px;
align-items: center;
margin-bottom: 16px;
padding: 13px 15px;
border: 1px solid var(--line-strong);
background: var(--bg-subtle);
}
/* Nur der Hinweistext im ersten Feld: ein Nachfahrenselektor `span` (0,1,1) schlug .credential (display: flex, gap) und .credential-value, die Textlinks klebten ohne Abstand aneinander */
.admin-credential-result strong,
.admin-credential-result > div > span { display: block; }
.admin-credential-result > div > span { margin-top: 3px; color: var(--text-muted); font-size: 12px; }
.admin-loading,
.admin-empty-state,
.admin-empty-line {
padding: 28px 18px;
color: var(--text-muted);
text-align: center;
}
.admin-loading .spinner { width: 22px; height: 22px; margin-bottom: 9px; }
.admin-empty-state h3 { margin-bottom: 5px; color: var(--text); font-size: 15px; }
.admin-empty-state p { font-size: 13px; }
.admin-empty-line { padding: 16px 8px; font-size: 13px; }
.admin-access-denied {
max-width: 540px;
margin: 12vh auto 0;
padding: 32px;
border-left: 3px solid var(--danger);
background: var(--surface-1);
}
.admin-access-denied > span { color: var(--danger); font-size: 12px; font-weight: 800; letter-spacing: 0.1em; }
.admin-access-denied h1 { margin: 8px 0; }
.admin-access-denied p { color: var(--text-muted); }
@media (max-width: 1060px) {
.admin-split-layout { grid-template-columns: 1fr; }
.admin-form-card { position: static; }
.admin-credential-result { grid-template-columns: 1fr auto; }
}
@media (max-width: 760px) {
.admin-page { padding: 16px; }
.admin-header { align-items: flex-start; flex-direction: column; }
.admin-current-user { width: 100%; }
.admin-tabs { overflow-x: auto; }
.admin-tabs button { flex: 0 0 auto; padding: 11px 12px; }
.admin-credential-result { grid-template-columns: 1fr; }
}
+192 -134
View File
@@ -1,15 +1,10 @@
import { useState } from 'react' import { useRef, useState } from 'react'
import {
FaFloppyDisk,
FaPlus,
FaSpinner,
FaTrash,
FaXmark,
} from 'react-icons/fa6'
import { FaEdit } from 'react-icons/fa'
import CopyableCredential from '../components/CopyableCredential' import CopyableCredential from '../components/CopyableCredential'
import { Badge, Button, Card, Dialog, EmptyState, Field, PageHeader, Tabs } from '../components/ui'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import { useEmployees } from '../hooks/useEmployees' import { useEmployees } from '../hooks/useEmployees'
import { useScrollIntoView } from '../hooks/useScrollIntoView'
import { validateEmployeeForm } from '../lib/employeeForm'
import './AdminPage.css' import './AdminPage.css'
const EMPTY_EMPLOYEE = { const EMPTY_EMPLOYEE = {
@@ -22,7 +17,11 @@ const EMPTY_EMPLOYEE = {
function Notice({ notice }) { function Notice({ notice }) {
if (!notice?.text) return null if (!notice?.text) return null
return <div className={`admin-notice ${notice.type}`}>{notice.text}</div> return (
<div className={`msg ${notice.type === 'error' ? 'err' : 'ok'} admin-notice`} role={notice.type === 'error' ? 'alert' : 'status'}>
{notice.text}
</div>
)
} }
export default function AdminPage() { export default function AdminPage() {
@@ -40,9 +39,14 @@ export default function AdminPage() {
const [activeSection, setActiveSection] = useState('employees') const [activeSection, setActiveSection] = useState('employees')
const [employeeForm, setEmployeeForm] = useState(EMPTY_EMPLOYEE) const [employeeForm, setEmployeeForm] = useState(EMPTY_EMPLOYEE)
const [editingEmployee, setEditingEmployee] = useState(null) const [editingEmployee, setEditingEmployee] = useState(null)
const [formOpen, setFormOpen] = useState(false)
const [formError, setFormError] = useState(null)
const [employeeBusy, setEmployeeBusy] = useState(false) const [employeeBusy, setEmployeeBusy] = useState(false)
const [createdCredential, setCreatedCredential] = useState(null) const [createdCredential, setCreatedCredential] = useState(null)
const [notice, setNotice] = useState(null) const [notice, setNotice] = useState(null)
const errorRef = useRef(null)
// Hooks stehen vor dem frühen return bei !isAdmin
useScrollIntoView(errorRef, formError)
const showNotice = (type, text) => { const showNotice = (type, text) => {
setNotice({ type, text }) setNotice({ type, text })
@@ -61,43 +65,21 @@ export default function AdminPage() {
) )
} }
const resetEmployeeForm = () => { // Der Admin-Haken des eigenen Kontos ist gesperrt: man nimmt sich nicht selbst die Rechte
const editingSelf = Boolean(editingEmployee) && employees.find((item) => item.$id === editingEmployee)?.userId === user?.$id
const closeForm = () => {
setFormOpen(false)
setFormError(null)
setEmployeeForm(EMPTY_EMPLOYEE) setEmployeeForm(EMPTY_EMPLOYEE)
setEditingEmployee(null) setEditingEmployee(null)
} }
const submitEmployee = async (event) => { const createNew = () => {
event.preventDefault() setEmployeeForm(EMPTY_EMPLOYEE)
if (!employeeForm.displayName.trim() || !employeeForm.email.trim()) { setEditingEmployee(null)
showNotice('error', 'Name und E-Mail sind erforderlich.') setFormError(null)
return setFormOpen(true)
}
if (!editingEmployee && employeeForm.password.length < 8) {
showNotice('error', 'Das Passwort muss mindestens 8 Zeichen haben.')
return
}
if (editingEmployee && employeeForm.password && employeeForm.password.length < 8) {
showNotice('error', 'Ein neues Passwort muss mindestens 8 Zeichen haben.')
return
}
setEmployeeBusy(true)
const result = editingEmployee
? await updateEmployee(editingEmployee, employeeForm)
: await createEmployee(employeeForm)
setEmployeeBusy(false)
if (!result.success) {
showNotice('error', result.error || 'Mitarbeiter konnte nicht gespeichert werden.')
return
}
if (!editingEmployee) {
setCreatedCredential({
email: employeeForm.email.trim(),
password: employeeForm.password,
})
}
showNotice('success', editingEmployee ? 'Mitarbeiter aktualisiert.' : 'Mitarbeiter angelegt. Der Login ist sofort aktiv.')
resetEmployeeForm()
} }
const editEmployee = (employee) => { const editEmployee = (employee) => {
@@ -109,6 +91,36 @@ export default function AdminPage() {
shortcode: employee.shortcode || '', shortcode: employee.shortcode || '',
isAdmin: Boolean(employee.isAdmin), isAdmin: Boolean(employee.isAdmin),
}) })
setFormError(null)
setFormOpen(true)
}
const submitEmployee = async (event) => {
event.preventDefault()
const message = validateEmployeeForm(employeeForm, Boolean(editingEmployee))
if (message) {
setFormError(message)
return
}
setFormError(null)
setEmployeeBusy(true)
const result = editingEmployee
? await updateEmployee(editingEmployee, employeeForm)
: await createEmployee(employeeForm)
setEmployeeBusy(false)
if (!result.success) {
setFormError(result.error || 'Mitarbeiter konnte nicht gespeichert werden.')
return
}
if (!editingEmployee) {
setCreatedCredential({
email: employeeForm.email.trim(),
password: employeeForm.password,
})
}
showNotice('success', editingEmployee ? 'Mitarbeiter aktualisiert.' : 'Mitarbeiter angelegt. Der Login ist sofort aktiv.')
closeForm()
} }
const removeEmployee = async (employee) => { const removeEmployee = async (employee) => {
@@ -120,107 +132,153 @@ export default function AdminPage() {
) )
} }
const setField = (key) => (event) => setEmployeeForm({ ...employeeForm, [key]: event.target.value })
return ( return (
<div className="page admin-page"> <div className="page admin-page">
<header className="admin-header"> <PageHeader
<div> eyebrow="Systemverwaltung"
<span className="admin-eyebrow">Systemverwaltung</span> title="Admin"
<h1>Admin</h1> subtitle="Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“."
<p>Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“.</p> actions={
</div> <>
<div className="admin-current-user"> <span className="muted small">Angemeldet als <strong>{user?.name || user?.email}</strong></span>
<span>Angemeldet als</span> <Button variant="primary" type="button" onClick={createNew}>Mitarbeiter anlegen</Button>
<strong>{user?.name || user?.email}</strong> </>
</div> }
</header> />
<Notice notice={notice} /> <Notice notice={notice} />
<nav className="admin-tabs" aria-label="Admin-Bereiche"> <Tabs
{[ tabs={[{ id: 'employees', label: `Mitarbeiter (${employees.length})` }]}
['employees', `Mitarbeiter (${employees.length})`], active={activeSection}
].map(([id, label]) => ( onChange={setActiveSection}
<button />
type="button"
key={id}
className={activeSection === id ? 'active' : ''}
onClick={() => setActiveSection(id)}
>
{label}
</button>
))}
</nav>
{activeSection === 'employees' && ( {activeSection === 'employees' && (
<section> <>
{createdCredential && ( {createdCredential && (
<div className="admin-credential-result"> <Card
<div><strong>Login wurde angelegt</strong><span>Diese Zugangsdaten jetzt sicher weitergeben.</span></div> title="Login wurde angelegt"
<CopyableCredential value={createdCredential.email} label="E-Mail" /> actions={
<CopyableCredential value={createdCredential.password} secret label="Passwort" /> <button type="button" className="btn btn-quiet" onClick={() => setCreatedCredential(null)} aria-label="Hinweis schließen">×</button>
<button type="button" className="admin-icon-button" onClick={() => setCreatedCredential(null)} aria-label="Hinweis schließen"><FaXmark /></button> }
</div> >
)} <p className="muted small">Diese Zugangsdaten jetzt sicher weitergeben.</p>
<div className="admin-split-layout"> <div className="credential-list">
<form className="admin-form-card" onSubmit={submitEmployee}> <CopyableCredential value={createdCredential.email} label="E-Mail" />
<div className="admin-section-head"> <CopyableCredential value={createdCredential.password} secret label="Passwort" />
<div>
<h2>{editingEmployee ? 'Mitarbeiter bearbeiten' : 'Mitarbeiter anlegen'}</h2>
<span>Die User-ID wird automatisch vergeben.</span>
</div>
{editingEmployee && <button type="button" className="admin-icon-button" onClick={resetEmployeeForm} aria-label="Bearbeitung schließen"><FaXmark /></button>}
</div> </div>
<label className="admin-field"><span>Name</span><input className="form-control" required value={employeeForm.displayName} onChange={(event) => setEmployeeForm({ ...employeeForm, displayName: event.target.value })} /></label> </Card>
<label className="admin-field"><span>E-Mail</span><input type="email" className="form-control" required value={employeeForm.email} onChange={(event) => setEmployeeForm({ ...employeeForm, email: event.target.value })} /></label> )}
<label className="admin-field"><span>{editingEmployee ? 'Neues Passwort (optional)' : 'Passwort'}</span><input type="password" className="form-control" autoComplete="new-password" required={!editingEmployee} value={employeeForm.password} onChange={(event) => setEmployeeForm({ ...employeeForm, password: event.target.value })} placeholder="Mindestens 8 Zeichen" /></label>
<label className="admin-field"><span>Kürzel</span><input className="form-control" maxLength={10} value={employeeForm.shortcode} onChange={(event) => setEmployeeForm({ ...employeeForm, shortcode: event.target.value.toUpperCase() })} placeholder="z. B. KNSO" /></label>
<label className={`admin-checkbox ${editingEmployee && employees.find((item) => item.$id === editingEmployee)?.userId === user?.$id ? 'disabled' : ''}`}>
<input
type="checkbox"
checked={employeeForm.isAdmin}
disabled={editingEmployee && employees.find((item) => item.$id === editingEmployee)?.userId === user?.$id}
onChange={(event) => setEmployeeForm({ ...employeeForm, isAdmin: event.target.checked })}
/>
<span><strong>Admin-Account</strong><small>Darf Systemkonfiguration und Benutzer verwalten.</small></span>
</label>
<button className="admin-primary-button" disabled={employeeBusy}>
{employeeBusy ? <FaSpinner className="spin" /> : editingEmployee ? <FaFloppyDisk /> : <FaPlus />}
{editingEmployee ? 'Änderungen speichern' : 'Mitarbeiter anlegen'}
</button>
</form>
<section className="admin-list-card"> <Card title="Mitarbeiterkonten">
<div className="admin-section-head"><div><h2>Mitarbeiterkonten</h2><span>Aktive Logins für das Ticketsystem</span></div></div> <p className="muted small admin-lead">Aktive Logins für das Ticketsystem</p>
{employeesError && <Notice notice={{ type: 'error', text: employeesError }} />} {employeesError && <div className="msg err" role="alert">{employeesError}</div>}
{employeesLoading ? ( {employeesLoading ? (
<div className="admin-loading"><FaSpinner className="spinner" /> Mitarbeiter werden geladen …</div> <div className="loading">Mitarbeiter werden geladen …</div>
) : employees.length === 0 ? ( ) : employees.length === 0 ? (
<div className="admin-empty-state"><h3>Noch keine Mitarbeiter</h3><p>Lege links den ersten Login an.</p></div> <EmptyState title="Noch keine Mitarbeiter" hint="Lege mit „Mitarbeiter anlegen“ den ersten Login an." />
) : ( ) : (
<div className="admin-table-wrap"> <table className="table">
<table className="admin-table"> <thead>
<thead><tr><th>Mitarbeiter</th><th>E-Mail</th><th>Kürzel</th><th>Rolle</th><th>User-ID</th><th><span className="sr-only">Aktionen</span></th></tr></thead> <tr>
<tbody> <th>Mitarbeiter</th>
{employees.map((employee) => ( <th>E-Mail</th>
<tr key={employee.$id}> <th className="hide-sm">Kürzel</th>
<td><strong>{employee.displayName || '–'}</strong></td> <th className="hide-sm">Rolle</th>
<td><CopyableCredential value={employee.email} label="E-Mail" /></td> <th className="hide-sm">User-ID</th>
<td><span className="admin-shortcode">{employee.shortcode || '–'}</span></td> <th className="right"><span className="sr-only">Aktionen</span></th>
<td><span className={`admin-role ${employee.isAdmin ? 'admin' : ''}`}>{employee.isAdmin ? 'Admin' : 'Mitarbeiter'}</span></td> </tr>
<td><CopyableCredential value={employee.userId} label="User-ID" /></td> </thead>
<td className="admin-row-actions"> <tbody>
<button type="button" className="admin-icon-button" onClick={() => editEmployee(employee)} aria-label={`${employee.displayName} bearbeiten`}><FaEdit /></button> {employees.map((employee) => (
{employee.userId !== user?.$id && <button type="button" className="admin-icon-button danger" onClick={() => removeEmployee(employee)} aria-label={`${employee.displayName} löschen`}><FaTrash /></button>} <tr key={employee.$id}>
</td> <td><strong>{employee.displayName || '–'}</strong></td>
</tr> <td><CopyableCredential value={employee.email} label="E-Mail" /></td>
))} <td className="hide-sm"><span className="admin-shortcode">{employee.shortcode || '–'}</span></td>
</tbody> <td className="hide-sm">
</table> {employee.isAdmin ? <Badge tone="ok">Admin</Badge> : <span className="muted">Mitarbeiter</span>}
</div> </td>
)} <td className="hide-sm"><CopyableCredential value={employee.userId} label="User-ID" /></td>
</section> <td className="right">
</div> <div className="admin-actions">
</section> <button type="button" className="link" onClick={() => editEmployee(employee)} aria-label={`${employee.displayName} bearbeiten`}>Bearbeiten</button>
{employee.userId !== user?.$id && (
<Button variant="danger" type="button" onClick={() => removeEmployee(employee)} aria-label={`${employee.displayName} löschen`}>Löschen</Button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</Card>
</>
)}
{formOpen && (
<Dialog
as="form"
title={editingEmployee ? 'Mitarbeiter bearbeiten' : 'Mitarbeiter anlegen'}
busy={employeeBusy}
onClose={closeForm}
onSubmit={submitEmployee}
footer={
<>
<span className="grow" />
<Button type="button" disabled={employeeBusy} onClick={closeForm}>Abbrechen</Button>
<Button variant="primary" type="submit" disabled={employeeBusy}>
{employeeBusy ? 'Speichert …' : editingEmployee ? 'Änderungen speichern' : 'Mitarbeiter anlegen'}
</Button>
</>
}
>
{formError && <div className="msg err" role="alert" ref={errorRef}>{formError}</div>}
<p className="muted small">Die User-ID wird automatisch vergeben.</p>
<Field label="Name" htmlFor="employee-displayName">
<input className="input" id="employee-displayName" required value={employeeForm.displayName} onChange={setField('displayName')} />
</Field>
<Field label="E-Mail" htmlFor="employee-email">
<input className="input" id="employee-email" type="email" required value={employeeForm.email} onChange={setField('email')} />
</Field>
<Field label={editingEmployee ? 'Neues Passwort (optional)' : 'Passwort'} htmlFor="employee-password">
<input
className="input"
id="employee-password"
type="password"
autoComplete="new-password"
required={!editingEmployee}
value={employeeForm.password}
onChange={setField('password')}
placeholder="Mindestens 8 Zeichen"
/>
</Field>
<Field label="Kürzel" htmlFor="employee-shortcode">
<input
className="input"
id="employee-shortcode"
maxLength={10}
value={employeeForm.shortcode}
onChange={(event) => setEmployeeForm({ ...employeeForm, shortcode: event.target.value.toUpperCase() })}
placeholder="z. B. KNSO"
/>
</Field>
<label className={`check admin-check${editingSelf ? ' is-disabled' : ''}`}>
<input
type="checkbox"
checked={employeeForm.isAdmin}
disabled={editingSelf}
onChange={(event) => setEmployeeForm({ ...employeeForm, isAdmin: event.target.checked })}
/>
<span>
<strong>Admin-Account</strong>
<small>Darf Systemkonfiguration und Benutzer verwalten.</small>
</span>
</label>
</Dialog>
)} )}
</div> </div>
) )