Kunden an einer Stelle pflegen (Paket E)

- Kundenliste startet bei "Feste Kunden" (105 von 110 Einträgen sind
  Leads der Lead-Automatik); Reihenfolge Feste Kunden, Leads, Alle, Archiv.
- Der zweite Kunden-Editor im Admin ist weg. Admins legen feste Kunden
  mit Portal-Zugang jetzt unter Kunden an ("Neuer Kunde") und löschen sie
  auf der Kundenseite ("Kunde löschen", mit Rückfrage); beides wie bisher
  über die Admin-API des Kundenportals. Die Kundenseite hat das Feld
  Standort, das es bisher nur im Admin gab.
- Die Kundenliste liest direkt aus Appwrite: die Admin-API lieferte
  Admins gekürzte Daten, Stern und InvoiceNinja-Abzeichen fehlten dort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-27 16:19:10 +00:00
1 parent 31144148d1
commit 5a066eb3c4
5 files changed
+111 -193

No files matched your search

+4 -160
View File
@@ -7,23 +7,11 @@ import {
FaXmark,
} from 'react-icons/fa6'
import { FaEdit } from 'react-icons/fa'
import CopyableCredential, {
getCustomerPortalPassword,
} from '../components/CopyableCredential'
import CopyableCredential from '../components/CopyableCredential'
import { useAuth } from '../context/AuthContext'
import { useCustomers } from '../hooks/useCustomers'
import { useEmployees } from '../hooks/useEmployees'
import './AdminPage.css'
const EMPTY_CUSTOMER = {
code: '',
name: '',
location: '',
email: '',
phone: '',
password: '',
}
const EMPTY_EMPLOYEE = {
displayName: '',
email: '',
@@ -39,13 +27,6 @@ function Notice({ notice }) {
export default function AdminPage() {
const { user, isAdmin } = useAuth()
const {
customers,
loading: customersLoading,
createCustomer,
updateCustomer,
deleteCustomer,
} = useCustomers()
const {
employees,
loading: employeesLoading,
@@ -55,10 +36,8 @@ export default function AdminPage() {
deleteEmployee,
} = useEmployees()
const [activeSection, setActiveSection] = useState('customers')
const [customerForm, setCustomerForm] = useState(EMPTY_CUSTOMER)
const [editingCustomer, setEditingCustomer] = useState(null)
const [customerBusy, setCustomerBusy] = useState(false)
// 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 [employeeBusy, setEmployeeBusy] = useState(false)
@@ -82,60 +61,6 @@ export default function AdminPage() {
)
}
const resetCustomerForm = () => {
setCustomerForm(EMPTY_CUSTOMER)
setEditingCustomer(null)
}
const submitCustomer = async (event) => {
event.preventDefault()
if (!customerForm.name.trim() || !customerForm.email.trim()) {
showNotice('error', 'Name und E-Mail sind erforderlich.')
return
}
if (!editingCustomer && customerForm.password.length < 8) {
showNotice('error', 'Das Portal-Passwort muss mindestens 8 Zeichen haben.')
return
}
if (editingCustomer && customerForm.password && customerForm.password.length < 8) {
showNotice('error', 'Ein neues Passwort muss mindestens 8 Zeichen haben.')
return
}
setCustomerBusy(true)
const result = editingCustomer
? await updateCustomer(editingCustomer, customerForm)
: await createCustomer(customerForm)
setCustomerBusy(false)
if (!result.success) {
showNotice('error', result.error || 'Kunde konnte nicht gespeichert werden.')
return
}
showNotice('success', editingCustomer ? 'Kunde aktualisiert.' : 'Kunde angelegt.')
resetCustomerForm()
}
const editCustomer = (customer) => {
setEditingCustomer(customer.$id)
setCustomerForm({
code: customer.code || '',
name: customer.name || '',
location: customer.location || '',
email: customer.email || '',
phone: customer.phone || '',
password: '',
})
}
const removeCustomer = async (customer) => {
if (!window.confirm(`${customer.name || customer.email} wirklich löschen?`)) return
const result = await deleteCustomer(customer.$id)
showNotice(
result.success ? 'success' : 'error',
result.success ? 'Kunde und Portalzugang gelöscht.' : result.error
)
}
const resetEmployeeForm = () => {
setEmployeeForm(EMPTY_EMPLOYEE)
setEditingEmployee(null)
@@ -201,7 +126,7 @@ export default function AdminPage() {
<div>
<span className="admin-eyebrow">Systemverwaltung</span>
<h1>Admin</h1>
<p>Kundenkonten und Mitarbeiterzugänge verwalten.</p>
<p>Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“.</p>
</div>
<div className="admin-current-user">
<span>Angemeldet als</span>
@@ -213,7 +138,6 @@ export default function AdminPage() {
<nav className="admin-tabs" aria-label="Admin-Bereiche">
{[
['customers', `Kunden (${customers.length})`],
['employees', `Mitarbeiter (${employees.length})`],
].map(([id, label]) => (
<button
@@ -227,86 +151,6 @@ export default function AdminPage() {
))}
</nav>
{activeSection === 'customers' && (
<section className="admin-split-layout">
<form className="admin-form-card" onSubmit={submitCustomer}>
<div className="admin-section-head">
<div>
<h2>{editingCustomer ? 'Kunde bearbeiten' : 'Kunde anlegen'}</h2>
<span>Portalzugang für project.webklar.com</span>
</div>
{editingCustomer && (
<button type="button" className="admin-icon-button" onClick={resetCustomerForm} aria-label="Bearbeitung schließen">
<FaXmark />
</button>
)}
</div>
<label className="admin-field">
<span>Kundencode <em>manuell</em></span>
<input className="form-control" value={customerForm.code} onChange={(event) => setCustomerForm({ ...customerForm, code: event.target.value })} placeholder="z. B. C001" />
</label>
<label className="admin-field">
<span>Name</span>
<input className="form-control" required value={customerForm.name} onChange={(event) => setCustomerForm({ ...customerForm, name: event.target.value })} />
</label>
<label className="admin-field">
<span>Standort</span>
<input className="form-control" value={customerForm.location} onChange={(event) => setCustomerForm({ ...customerForm, location: event.target.value })} />
</label>
<label className="admin-field">
<span>E-Mail</span>
<input type="email" className="form-control" required value={customerForm.email} onChange={(event) => setCustomerForm({ ...customerForm, email: event.target.value })} />
</label>
<label className="admin-field">
<span>Telefon</span>
<input className="form-control" value={customerForm.phone} onChange={(event) => setCustomerForm({ ...customerForm, phone: event.target.value })} />
</label>
<label className="admin-field">
<span>{editingCustomer ? 'Neues Passwort (optional)' : 'Portal-Passwort'}</span>
<input type="password" className="form-control" autoComplete="new-password" required={!editingCustomer} value={customerForm.password} onChange={(event) => setCustomerForm({ ...customerForm, password: event.target.value })} placeholder="Mindestens 8 Zeichen" />
</label>
<button className="admin-primary-button" disabled={customerBusy}>
{customerBusy ? <FaSpinner className="spin" /> : editingCustomer ? <FaFloppyDisk /> : <FaPlus />}
{editingCustomer ? 'Änderungen speichern' : 'Kunde anlegen'}
</button>
</form>
<section className="admin-list-card">
<div className="admin-section-head">
<div><h2>Kundenkonten</h2><span>E-Mail sichtbar, Passwort geschützt</span></div>
</div>
{customersLoading ? (
<div className="admin-loading"><FaSpinner className="spinner" /> Kunden werden geladen …</div>
) : customers.length === 0 ? (
<div className="admin-empty-state"><h3>Noch keine Kunden</h3><p>Lege links den ersten Portalzugang an.</p></div>
) : (
<div className="admin-table-wrap">
<table className="admin-table">
<thead><tr><th>Code / Kunde</th><th>Zugang</th><th>Standort / Telefon</th><th>Status</th><th><span className="sr-only">Aktionen</span></th></tr></thead>
<tbody>
{customers.map((customer) => (
<tr key={customer.$id}>
<td><strong>{customer.code || 'Ohne Code'}</strong><span>{customer.name || '–'}</span></td>
<td>
<CopyableCredential value={customer.email} label="E-Mail" />
<CopyableCredential value={getCustomerPortalPassword(customer)} secret label="Passwort" />
</td>
<td><strong>{customer.location || '–'}</strong><span>{customer.phone || '–'}</span></td>
<td><span className={`admin-status ${customer.portalAccessEnabled ? 'active' : ''}`}>{customer.portalAccessEnabled ? 'Aktiv' : 'Inaktiv'}</span></td>
<td className="admin-row-actions">
<button type="button" className="admin-icon-button" onClick={() => editCustomer(customer)} aria-label={`${customer.name} bearbeiten`}><FaEdit /></button>
<button type="button" className="admin-icon-button danger" onClick={() => removeCustomer(customer)} aria-label={`${customer.name} löschen`}><FaTrash /></button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</section>
)}
{activeSection === 'employees' && (
<section>
{createdCredential && (