✨ first version
This commit is contained in:
+146
-49
@@ -1,8 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router'
|
||||
|
||||
import { ConfirmDialog } from '#/components/confirm-dialog'
|
||||
import { StaffMemberDetailDialog } from '#/components/staff-member-detail-dialog'
|
||||
import { StaffTabs } from '#/components/staff-tabs'
|
||||
import { getCurrentStaffUser } from '#/lib/auth'
|
||||
import { authClient } from '#/lib/auth-client'
|
||||
import { phoneSchema } from '#/lib/phone'
|
||||
import { listStaffMembers } from '#/lib/staff'
|
||||
|
||||
export const Route = createFileRoute('/staff/team')({
|
||||
@@ -28,12 +32,13 @@ type StaffMember = Awaited<ReturnType<typeof listStaffMembers>>[number]
|
||||
type FormState = {
|
||||
name: string
|
||||
email: string
|
||||
phone: string
|
||||
password: string
|
||||
role: 'admin' | 'user'
|
||||
}
|
||||
|
||||
function emptyForm(): FormState {
|
||||
return { name: '', email: '', password: '', role: 'user' }
|
||||
return { name: '', email: '', phone: '', password: '', role: 'user' }
|
||||
}
|
||||
|
||||
function StaffTeam() {
|
||||
@@ -45,11 +50,20 @@ function StaffTeam() {
|
||||
const [success, setSuccess] = useState<string | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [busyId, setBusyId] = useState<string | null>(null)
|
||||
const [viewingMember, setViewingMember] = useState<StaffMember | null>(null)
|
||||
const [pendingRemove, setPendingRemove] = useState<StaffMember | null>(null)
|
||||
|
||||
async function handleCreate(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
|
||||
const phoneResult = phoneSchema.safeParse(form.phone)
|
||||
if (!phoneResult.success) {
|
||||
setError('Bitte geben Sie eine gültige Telefonnummer an')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
const { error: createError } = await authClient.admin.createUser({
|
||||
@@ -57,6 +71,7 @@ function StaffTeam() {
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
role: form.role,
|
||||
data: { phone: phoneResult.data },
|
||||
})
|
||||
|
||||
if (createError) {
|
||||
@@ -71,11 +86,11 @@ function StaffTeam() {
|
||||
await router.invalidate()
|
||||
}
|
||||
|
||||
async function handleRemove(member: StaffMember) {
|
||||
if (!window.confirm(`Zugang für ${member.name} wirklich entfernen?`)) {
|
||||
return
|
||||
}
|
||||
async function confirmRemove() {
|
||||
const member = pendingRemove
|
||||
if (!member) return
|
||||
|
||||
setPendingRemove(null)
|
||||
setBusyId(member.id)
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
@@ -129,6 +144,44 @@ function StaffTeam() {
|
||||
setBusyId(null)
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authClient.signOut()
|
||||
await router.navigate({ to: '/staff/login' })
|
||||
await router.invalidate()
|
||||
}
|
||||
|
||||
async function handleSetPhone(member: StaffMember) {
|
||||
const newPhone = window.prompt(`Telefonnummer für ${member.name}:`, member.phone ?? '')
|
||||
|
||||
if (newPhone === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const phoneResult = phoneSchema.safeParse(newPhone)
|
||||
if (!phoneResult.success) {
|
||||
setError('Bitte geben Sie eine gültige Telefonnummer an')
|
||||
return
|
||||
}
|
||||
|
||||
setBusyId(member.id)
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
|
||||
const { error: phoneError } = await authClient.admin.updateUser({
|
||||
userId: member.id,
|
||||
data: { phone: phoneResult.data },
|
||||
})
|
||||
|
||||
if (phoneError) {
|
||||
setError(phoneError.message ?? 'Etwas ist schiefgelaufen')
|
||||
} else {
|
||||
setSuccess(`Telefonnummer für ${member.name} wurde aktualisiert.`)
|
||||
await router.invalidate()
|
||||
}
|
||||
|
||||
setBusyId(null)
|
||||
}
|
||||
|
||||
async function handleSetPassword(member: StaffMember) {
|
||||
const newPassword = window.prompt(`Neues Passwort für ${member.name} (mind. 8 Zeichen):`)
|
||||
|
||||
@@ -156,17 +209,23 @@ function StaffTeam() {
|
||||
|
||||
return (
|
||||
<div className="staff-page">
|
||||
<header className="staff-topbar">
|
||||
<Link className="wordmark" to="/">
|
||||
Steakhaus <span>bei Mirko</span>
|
||||
</Link>
|
||||
<div className="staff-topbar-right">
|
||||
<Link className="btn btn-secondary" to="/staff">
|
||||
Zu den Reservierungen
|
||||
<div className="staff-nav">
|
||||
<header className="staff-topbar">
|
||||
<Link className="wordmark" to="/">
|
||||
Steakhaus <span>bei Mirko</span>
|
||||
</Link>
|
||||
<span>Angemeldet als {staffUser.name}</span>
|
||||
<div className="staff-topbar-right">
|
||||
<span>Angemeldet als {staffUser.name}</span>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleLogout}>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="staff-tabs-bar">
|
||||
<StaffTabs role={staffUser.role} />
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<main className="page staff-main">
|
||||
<h1 className="section-heading">Mitarbeiter</h1>
|
||||
@@ -197,6 +256,19 @@ function StaffTeam() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Telefon
|
||||
<input
|
||||
type="tel"
|
||||
inputMode="tel"
|
||||
pattern="[+]?[0-9()\-.\s]{6,30}"
|
||||
title="Nur Ziffern, Leerzeichen und + ( ) - ."
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="form-row">
|
||||
<label>
|
||||
Passwort
|
||||
@@ -232,11 +304,12 @@ function StaffTeam() {
|
||||
|
||||
<section className="staff-table-section">
|
||||
<div className="table-scroll">
|
||||
<table className="reservations-table">
|
||||
<table className="reservations-table reservations-table-clickable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Telefon</th>
|
||||
<th>Rolle</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
@@ -244,44 +317,55 @@ function StaffTeam() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{members.map((member) => (
|
||||
<tr key={member.id}>
|
||||
<tr key={member.id} onClick={() => setViewingMember(member)}>
|
||||
<td>{member.name}</td>
|
||||
<td>{member.email}</td>
|
||||
<td>{member.phone || <span className="muted">—</span>}</td>
|
||||
<td>{member.role === 'admin' ? 'Admin' : 'Mitarbeiter'}</td>
|
||||
<td>{member.banned ? 'Gesperrt' : 'Aktiv'}</td>
|
||||
<td className="table-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busyId === member.id}
|
||||
onClick={() => handleToggleRole(member)}
|
||||
>
|
||||
{member.role === 'admin' ? 'Zu Mitarbeiter machen' : 'Zu Admin machen'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busyId === member.id}
|
||||
onClick={() => handleSetPassword(member)}
|
||||
>
|
||||
Passwort setzen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busyId === member.id || member.id === staffUser.id}
|
||||
onClick={() => handleToggleBan(member)}
|
||||
>
|
||||
{member.banned ? 'Entsperren' : 'Sperren'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
disabled={busyId === member.id || member.id === staffUser.id}
|
||||
onClick={() => handleRemove(member)}
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
<td onClick={(event) => event.stopPropagation()}>
|
||||
<div className="table-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busyId === member.id}
|
||||
onClick={() => handleToggleRole(member)}
|
||||
>
|
||||
{member.role === 'admin' ? 'Zu Mitarbeiter machen' : 'Zu Admin machen'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busyId === member.id}
|
||||
onClick={() => handleSetPhone(member)}
|
||||
>
|
||||
Telefon bearbeiten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busyId === member.id}
|
||||
onClick={() => handleSetPassword(member)}
|
||||
>
|
||||
Passwort setzen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={busyId === member.id || member.id === staffUser.id}
|
||||
onClick={() => handleToggleBan(member)}
|
||||
>
|
||||
{member.banned ? 'Entsperren' : 'Sperren'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
disabled={busyId === member.id || member.id === staffUser.id}
|
||||
onClick={() => setPendingRemove(member)}
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -290,6 +374,19 @@ function StaffTeam() {
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<StaffMemberDetailDialog member={viewingMember} onClose={() => setViewingMember(null)} />
|
||||
|
||||
<ConfirmDialog
|
||||
open={pendingRemove !== null}
|
||||
title="Zugang entfernen"
|
||||
message={pendingRemove && <>Zugang für <strong>{pendingRemove.name}</strong> wirklich entfernen?</>}
|
||||
warning="Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
confirmLabel="Entfernen"
|
||||
danger
|
||||
onConfirm={confirmRemove}
|
||||
onCancel={() => setPendingRemove(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user