Files
tickte-system/src/pages/AdminPage.jsx
T

286 lines
11 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = {
displayName: '',
email: '',
password: '',
shortcode: '',
isAdmin: false,
}
function Notice({ notice }) {
if (!notice?.text) return null
return (
<div className={`msg ${notice.type === 'error' ? 'err' : 'ok'} admin-notice`} role={notice.type === 'error' ? 'alert' : 'status'}>
{notice.text}
</div>
)
}
export default function AdminPage() {
const { user, isAdmin } = useAuth()
const {
employees,
loading: employeesLoading,
error: employeesError,
createEmployee,
updateEmployee,
deleteEmployee,
} = useEmployees()
// Kunden werden unter "Kunden" gepflegt (Anlegen und Loeschen fuer Admins dort)
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 })
window.setTimeout(() => setNotice(null), 4500)
}
if (!isAdmin) {
return (
<div className="page admin-page">
<section className="admin-access-denied">
<span>403</span>
<h1>Zugriff verweigert</h1>
<p>Für diesen Bereich ist ein Admin-Account erforderlich.</p>
</section>
</div>
)
}
// 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 createNew = () => {
setEmployeeForm(EMPTY_EMPLOYEE)
setEditingEmployee(null)
setFormError(null)
setFormOpen(true)
}
const editEmployee = (employee) => {
setEditingEmployee(employee.$id)
setEmployeeForm({
displayName: employee.displayName || '',
email: employee.email || '',
password: '',
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) => {
if (!window.confirm(`${employee.displayName || employee.email} und dessen Login wirklich löschen?`)) return
const result = await deleteEmployee(employee.$id)
showNotice(
result.success ? 'success' : 'error',
result.success ? 'Mitarbeiter und Login gelöscht.' : result.error
)
}
const setField = (key) => (event) => setEmployeeForm({ ...employeeForm, [key]: event.target.value })
return (
<div className="page admin-page">
<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} />
<Tabs
tabs={[{ id: 'employees', label: `Mitarbeiter (${employees.length})` }]}
active={activeSection}
onChange={setActiveSection}
/>
{activeSection === 'employees' && (
<>
{createdCredential && (
<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" />
</div>
</Card>
)}
<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="loading">Mitarbeiter werden geladen …</div>
) : employees.length === 0 ? (
<EmptyState title="Noch keine Mitarbeiter" hint="Lege mit „Mitarbeiter anlegen“ den ersten Login an." />
) : (
<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 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>
)}
</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>
)
}