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:
1 parent
0a881fce8f
commit
6adeac1642
3 files changed
+194
-481
No files matched your search
@@ -7,8 +7,7 @@ export default function AdminRoute({ children }) {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center p-4" style={{ marginTop: 80 }}>
|
||||
<div className="spinner"></div>
|
||||
<div className="loading">
|
||||
<p>Wird geladen …</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
+18
-362
@@ -1,364 +1,20 @@
|
||||
/* .page (global.css, wird danach geladen) gewinnt bei gleicher Spezifitaet: darum .page.admin-page */
|
||||
.page.admin-page {
|
||||
max-width: 1480px;
|
||||
}
|
||||
.admin-page {
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
/* Admin: Mitarbeiter-Tabelle, Zugangsdaten, Dialog. Karten, Rahmen und Schatten gibt es nicht mehr; Abstände zwischen den */
|
||||
/* Abschnitten liefert .page section + section (global.css). AdminPage.css lädt vor global.css, darum haben Regeln, die eine */
|
||||
/* 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-notice { margin-bottom: 16px; }
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
/* Admin-Haken im Dialog: Titel und Erklärung untereinander, der Haken oben links */
|
||||
.check.admin-check { align-items: flex-start; }
|
||||
.admin-check small { display: block; margin-top: 2px; font-size: 13px; line-height: 18px; color: var(--text-2); }
|
||||
.admin-check.is-disabled { cursor: not-allowed; opacity: .55; }
|
||||
|
||||
.admin-header h1 {
|
||||
margin: 4px 0 6px;
|
||||
font-size: clamp(28px, 3vw, 38px);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.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; }
|
||||
}
|
||||
/* 403 */
|
||||
.admin-access-denied { max-width: 540px; margin: 12vh auto 0; }
|
||||
.admin-access-denied > span { font-size: 12px; font-weight: 500; color: var(--red-fg); }
|
||||
.admin-access-denied h1 { margin: 4px 0 8px; font-size: 28px; line-height: 34px; font-weight: 700; letter-spacing: -.02em; }
|
||||
.admin-access-denied p { color: var(--text-2); }
|
||||
+175
-117
@@ -1,15 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
FaFloppyDisk,
|
||||
FaPlus,
|
||||
FaSpinner,
|
||||
FaTrash,
|
||||
FaXmark,
|
||||
} from 'react-icons/fa6'
|
||||
import { FaEdit } from 'react-icons/fa'
|
||||
import { useRef, useState } from 'react'
|
||||
import CopyableCredential from '../components/CopyableCredential'
|
||||
import { Badge, Button, Card, Dialog, EmptyState, Field, PageHeader, Tabs } from '../components/ui'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useEmployees } from '../hooks/useEmployees'
|
||||
import { useScrollIntoView } from '../hooks/useScrollIntoView'
|
||||
import { validateEmployeeForm } from '../lib/employeeForm'
|
||||
import './AdminPage.css'
|
||||
|
||||
const EMPTY_EMPLOYEE = {
|
||||
@@ -22,7 +17,11 @@ const EMPTY_EMPLOYEE = {
|
||||
|
||||
function Notice({ notice }) {
|
||||
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() {
|
||||
@@ -40,9 +39,14 @@ export default function AdminPage() {
|
||||
const [activeSection, setActiveSection] = useState('employees')
|
||||
const [employeeForm, setEmployeeForm] = useState(EMPTY_EMPLOYEE)
|
||||
const [editingEmployee, setEditingEmployee] = useState(null)
|
||||
const [formOpen, setFormOpen] = useState(false)
|
||||
const [formError, setFormError] = useState(null)
|
||||
const [employeeBusy, setEmployeeBusy] = useState(false)
|
||||
const [createdCredential, setCreatedCredential] = 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) => {
|
||||
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)
|
||||
setEditingEmployee(null)
|
||||
}
|
||||
|
||||
const submitEmployee = async (event) => {
|
||||
event.preventDefault()
|
||||
if (!employeeForm.displayName.trim() || !employeeForm.email.trim()) {
|
||||
showNotice('error', 'Name und E-Mail sind erforderlich.')
|
||||
return
|
||||
}
|
||||
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 createNew = () => {
|
||||
setEmployeeForm(EMPTY_EMPLOYEE)
|
||||
setEditingEmployee(null)
|
||||
setFormError(null)
|
||||
setFormOpen(true)
|
||||
}
|
||||
|
||||
const editEmployee = (employee) => {
|
||||
@@ -109,6 +91,36 @@ export default function AdminPage() {
|
||||
shortcode: employee.shortcode || '',
|
||||
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) => {
|
||||
@@ -120,107 +132,153 @@ export default function AdminPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const setField = (key) => (event) => setEmployeeForm({ ...employeeForm, [key]: event.target.value })
|
||||
|
||||
return (
|
||||
<div className="page admin-page">
|
||||
<header className="admin-header">
|
||||
<div>
|
||||
<span className="admin-eyebrow">Systemverwaltung</span>
|
||||
<h1>Admin</h1>
|
||||
<p>Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“.</p>
|
||||
</div>
|
||||
<div className="admin-current-user">
|
||||
<span>Angemeldet als</span>
|
||||
<strong>{user?.name || user?.email}</strong>
|
||||
</div>
|
||||
</header>
|
||||
<PageHeader
|
||||
eyebrow="Systemverwaltung"
|
||||
title="Admin"
|
||||
subtitle="Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“."
|
||||
actions={
|
||||
<>
|
||||
<span className="muted small">Angemeldet als <strong>{user?.name || user?.email}</strong></span>
|
||||
<Button variant="primary" type="button" onClick={createNew}>Mitarbeiter anlegen</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Notice notice={notice} />
|
||||
|
||||
<nav className="admin-tabs" aria-label="Admin-Bereiche">
|
||||
{[
|
||||
['employees', `Mitarbeiter (${employees.length})`],
|
||||
].map(([id, label]) => (
|
||||
<button
|
||||
type="button"
|
||||
key={id}
|
||||
className={activeSection === id ? 'active' : ''}
|
||||
onClick={() => setActiveSection(id)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<Tabs
|
||||
tabs={[{ id: 'employees', label: `Mitarbeiter (${employees.length})` }]}
|
||||
active={activeSection}
|
||||
onChange={setActiveSection}
|
||||
/>
|
||||
|
||||
{activeSection === 'employees' && (
|
||||
<section>
|
||||
<>
|
||||
{createdCredential && (
|
||||
<div className="admin-credential-result">
|
||||
<div><strong>Login wurde angelegt</strong><span>Diese Zugangsdaten jetzt sicher weitergeben.</span></div>
|
||||
<Card
|
||||
title="Login wurde angelegt"
|
||||
actions={
|
||||
<button type="button" className="btn btn-quiet" onClick={() => setCreatedCredential(null)} aria-label="Hinweis schließen">×</button>
|
||||
}
|
||||
>
|
||||
<p className="muted small">Diese Zugangsdaten jetzt sicher weitergeben.</p>
|
||||
<div className="credential-list">
|
||||
<CopyableCredential value={createdCredential.email} label="E-Mail" />
|
||||
<CopyableCredential value={createdCredential.password} secret label="Passwort" />
|
||||
<button type="button" className="admin-icon-button" onClick={() => setCreatedCredential(null)} aria-label="Hinweis schließen"><FaXmark /></button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
<div className="admin-split-layout">
|
||||
<form className="admin-form-card" onSubmit={submitEmployee}>
|
||||
<div className="admin-section-head">
|
||||
<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>
|
||||
<label className="admin-field"><span>Name</span><input className="form-control" required value={employeeForm.displayName} onChange={(event) => setEmployeeForm({ ...employeeForm, displayName: event.target.value })} /></label>
|
||||
<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">
|
||||
<div className="admin-section-head"><div><h2>Mitarbeiterkonten</h2><span>Aktive Logins für das Ticketsystem</span></div></div>
|
||||
{employeesError && <Notice notice={{ type: 'error', text: employeesError }} />}
|
||||
<Card title="Mitarbeiterkonten">
|
||||
<p className="muted small admin-lead">Aktive Logins für das Ticketsystem</p>
|
||||
{employeesError && <div className="msg err" role="alert">{employeesError}</div>}
|
||||
{employeesLoading ? (
|
||||
<div className="admin-loading"><FaSpinner className="spinner" /> Mitarbeiter werden geladen …</div>
|
||||
<div className="loading">Mitarbeiter werden geladen …</div>
|
||||
) : 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="admin-table">
|
||||
<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>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>E-Mail</th>
|
||||
<th className="hide-sm">Kürzel</th>
|
||||
<th className="hide-sm">Rolle</th>
|
||||
<th className="hide-sm">User-ID</th>
|
||||
<th className="right"><span className="sr-only">Aktionen</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{employees.map((employee) => (
|
||||
<tr key={employee.$id}>
|
||||
<td><strong>{employee.displayName || '–'}</strong></td>
|
||||
<td><CopyableCredential value={employee.email} label="E-Mail" /></td>
|
||||
<td><span className="admin-shortcode">{employee.shortcode || '–'}</span></td>
|
||||
<td><span className={`admin-role ${employee.isAdmin ? 'admin' : ''}`}>{employee.isAdmin ? 'Admin' : 'Mitarbeiter'}</span></td>
|
||||
<td><CopyableCredential value={employee.userId} label="User-ID" /></td>
|
||||
<td className="admin-row-actions">
|
||||
<button type="button" className="admin-icon-button" onClick={() => editEmployee(employee)} aria-label={`${employee.displayName} bearbeiten`}><FaEdit /></button>
|
||||
{employee.userId !== user?.$id && <button type="button" className="admin-icon-button danger" onClick={() => removeEmployee(employee)} aria-label={`${employee.displayName} löschen`}><FaTrash /></button>}
|
||||
<td className="hide-sm"><span className="admin-shortcode">{employee.shortcode || '–'}</span></td>
|
||||
<td className="hide-sm">
|
||||
{employee.isAdmin ? <Badge tone="ok">Admin</Badge> : <span className="muted">Mitarbeiter</span>}
|
||||
</td>
|
||||
<td className="hide-sm"><CopyableCredential value={employee.userId} label="User-ID" /></td>
|
||||
<td className="right">
|
||||
<div className="admin-actions">
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user