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:
1 parent
31144148d1
commit
5a066eb3c4
5 files changed
+111
-193
No files matched your search
@@ -8,7 +8,6 @@ import {
|
||||
import {
|
||||
createCustomerWithPortalAccess,
|
||||
deleteCustomerWithPortalAccess,
|
||||
listCustomersForAdmin,
|
||||
updateCustomerWithPortalAccess,
|
||||
} from '../lib/customerAdminApi'
|
||||
|
||||
@@ -19,18 +18,15 @@ export function useCustomers() {
|
||||
|
||||
const fetchCustomers = useCallback(async () => {
|
||||
try {
|
||||
try {
|
||||
const response = await listCustomersForAdmin()
|
||||
setCustomers(response.customers || [])
|
||||
} catch {
|
||||
const response = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.CUSTOMERS,
|
||||
// Ohne Limit liefert Appwrite nur 25 Dokumente.
|
||||
[Query.orderAsc('name'), Query.limit(5000)]
|
||||
)
|
||||
setCustomers(response.documents || [])
|
||||
}
|
||||
// Direkt aus Appwrite: die Admin-API des Portals liefert gekuerzte Daten
|
||||
// (ohne Stern und InvoiceNinja-Verknuepfung)
|
||||
const response = await databases.listDocuments(
|
||||
DATABASE_ID,
|
||||
COLLECTIONS.CUSTOMERS,
|
||||
// Ohne Limit liefert Appwrite nur 25 Dokumente.
|
||||
[Query.orderAsc('name'), Query.limit(5000)]
|
||||
)
|
||||
setCustomers(response.documents || [])
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
console.error('Error fetching customers:', err)
|
||||
|
||||
@@ -24,10 +24,6 @@ export async function createCustomerWithPortalAccess(payload) {
|
||||
return adminFetch('/api/admin/customers', { method: 'POST', body: payload })
|
||||
}
|
||||
|
||||
export async function listCustomersForAdmin() {
|
||||
return adminFetch('/api/admin/customers', { method: 'GET' })
|
||||
}
|
||||
|
||||
export async function updateCustomerWithPortalAccess(customerId, payload) {
|
||||
return adminFetch(`/api/admin/customers/${customerId}`, {
|
||||
method: 'PATCH',
|
||||
|
||||
+4
-160
@@ -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 && (
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { FaSpinner, FaStar, FaArrowLeft, FaFloppyDisk, FaArrowUp, FaKey, FaArrowUpRightFromSquare, FaBoxArchive, FaBoxOpen } from 'react-icons/fa6'
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom'
|
||||
import { FaSpinner, FaStar, FaArrowLeft, FaFloppyDisk, FaArrowUp, FaKey, FaArrowUpRightFromSquare, FaBoxArchive, FaBoxOpen, FaTrash } from 'react-icons/fa6'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||
import { PageHeader, Card, Button, Badge, Tabs, EmptyState, Field } from '../components/ui'
|
||||
import { StatusPill, UrgentPill } from '../components/ui/StatusPill'
|
||||
import InvoicePanel from '../components/InvoicePanel'
|
||||
import AssignedProjectCard from '../components/AssignedProjectCard'
|
||||
import CopyableCredential, { getCustomerPortalPassword } from '../components/CopyableCredential'
|
||||
import { updateCustomerWithPortalAccess } from '../lib/customerAdminApi'
|
||||
import { updateCustomerWithPortalAccess, deleteCustomerWithPortalAccess } from '../lib/customerAdminApi'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||
import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer, syncCustomerToNinja } from '../lib/leads'
|
||||
|
||||
@@ -26,6 +27,8 @@ export default function CustomerDetailPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [tab, setTab] = useState('overview')
|
||||
const { isAdmin } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -42,7 +45,7 @@ export default function CustomerDetailPage() {
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
if (loading) return <div className="page"><div className="empty"><FaSpinner className="spinner" /></div></div>
|
||||
if (error || !customer) return <div className="page"><EmptyState title="Kunde nicht gefunden" hint={error} action={<Button as={Link} to="/customers"><FaArrowLeft /> Zurueck</Button>} /></div>
|
||||
if (error || !customer) return <div className="page"><EmptyState title="Kunde nicht gefunden" hint={error} action={<Button as={Link} to="/customers"><FaArrowLeft /> Zurück</Button>} /></div>
|
||||
|
||||
const stage = customerStage(customer)
|
||||
const archived = isArchived(customer)
|
||||
@@ -53,6 +56,17 @@ export default function CustomerDetailPage() {
|
||||
await load()
|
||||
}
|
||||
|
||||
// Loescht Kunde und Portal-Login ueber die Admin-API des Kundenportals (nur Admins)
|
||||
const removeCustomer = async () => {
|
||||
if (!window.confirm(`${customer.name || customer.email} und den Portal-Zugang wirklich löschen?`)) return
|
||||
try {
|
||||
await deleteCustomerWithPortalAccess(customer.$id)
|
||||
navigate('/customers')
|
||||
} catch (err) {
|
||||
window.alert(`Löschen fehlgeschlagen: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
@@ -72,6 +86,7 @@ export default function CustomerDetailPage() {
|
||||
<Button variant="ghost" onClick={toggleArchived}>
|
||||
{archived ? <><FaBoxOpen /> Wiederherstellen</> : <><FaBoxArchive /> Archivieren</>}
|
||||
</Button>
|
||||
{isAdmin && <Button variant="danger" onClick={removeCustomer}><FaTrash /> Kunde löschen</Button>}
|
||||
<Button variant="ghost" as={Link} to="/customers"><FaArrowLeft /> Alle Kunden</Button>
|
||||
</>
|
||||
}
|
||||
@@ -154,6 +169,7 @@ function OverviewTab({ customer, stage, onSaved }) {
|
||||
<Field label="E-Mail"><input className="form-control" type="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} /></Field>
|
||||
<Field label="Telefon"><input className="form-control" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} /></Field>
|
||||
</div>
|
||||
<Field label="Standort"><input className="form-control" value={form.location} onChange={(e) => setForm({ ...form, location: e.target.value })} /></Field>
|
||||
<Field label="Ansprechpartner"><input className="form-control" value={form.contactName} onChange={(e) => setForm({ ...form, contactName: e.target.value })} /></Field>
|
||||
<Field label="Notizen"><textarea className="form-control" rows={3} value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} /></Field>
|
||||
<label className="flex items-center gap-2" style={{ marginBottom: 12, cursor: 'pointer' }}>
|
||||
|
||||
+78
-12
@@ -7,11 +7,12 @@ import { createLead, customerStage, isArchived } from '../lib/leads'
|
||||
import { PageHeader, Card, Button, Badge, EmptyState, Field } from '../components/ui'
|
||||
|
||||
export default function CustomersPage() {
|
||||
const { customers, loading, refresh } = useCustomers()
|
||||
const { user } = useAuth()
|
||||
const { customers, loading, refresh, createCustomer } = useCustomers()
|
||||
const { user, isAdmin } = useAuth()
|
||||
const [search, setSearch] = useState('')
|
||||
const [stage, setStage] = useState('all') // all | lead | customer | archived
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
// 105 von 110 Eintraegen sind Leads der Lead-Automatik, deshalb startet die Liste bei den festen Kunden
|
||||
const [stage, setStage] = useState('customer') // customer | lead | all | archived
|
||||
const [showCreate, setShowCreate] = useState(null) // null | 'lead' | 'customer'
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return (customers || [])
|
||||
@@ -37,16 +38,23 @@ export default function CustomersPage() {
|
||||
<PageHeader
|
||||
title="Kunden"
|
||||
subtitle="Potenzielle Kunden (Leads) und feste Kunden - eine Pflege, Sync mit InvoiceNinja"
|
||||
actions={<Button variant="primary" onClick={() => setShowCreate((s) => !s)}><FaPlus /> Neuer Lead</Button>}
|
||||
actions={
|
||||
<>
|
||||
{isAdmin && (
|
||||
<Button onClick={() => setShowCreate((s) => (s === 'customer' ? null : 'customer'))}><FaPlus /> Neuer Kunde</Button>
|
||||
)}
|
||||
<Button variant="primary" onClick={() => setShowCreate((s) => (s === 'lead' ? null : 'lead'))}><FaPlus /> Neuer Lead</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{showCreate && (
|
||||
{showCreate === 'lead' && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<CreateLeadForm
|
||||
onCancel={() => setShowCreate(false)}
|
||||
onCancel={() => setShowCreate(null)}
|
||||
onDone={async (data) => {
|
||||
const r = await createLead(data, user)
|
||||
setShowCreate(false)
|
||||
setShowCreate(null)
|
||||
refresh()
|
||||
return r
|
||||
}}
|
||||
@@ -54,21 +62,38 @@ export default function CustomersPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCreate === 'customer' && isAdmin && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<CreateCustomerForm
|
||||
onCancel={() => setShowCreate(null)}
|
||||
onCreate={async (data) => {
|
||||
const r = await createCustomer(data)
|
||||
if (r.success) {
|
||||
setShowCreate(null)
|
||||
setStage('customer')
|
||||
refresh()
|
||||
}
|
||||
return r
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card pad={false}>
|
||||
<div className="ui-card-head" style={{ gap: 12, flexWrap: 'wrap' }}>
|
||||
<div className="seg">
|
||||
<button className={stage === 'all' ? 'active' : ''} onClick={() => setStage('all')}>Alle</button>
|
||||
<button className={stage === 'lead' ? 'active' : ''} onClick={() => setStage('lead')}>Leads</button>
|
||||
<button className={stage === 'customer' ? 'active' : ''} onClick={() => setStage('customer')}>Feste Kunden</button>
|
||||
<button className={stage === 'lead' ? 'active' : ''} onClick={() => setStage('lead')}>Leads</button>
|
||||
<button className={stage === 'all' ? 'active' : ''} onClick={() => setStage('all')}>Alle</button>
|
||||
<button className={stage === 'archived' ? 'active' : ''} onClick={() => setStage('archived')}>Archiviert</button>
|
||||
</div>
|
||||
<input className="form-control search-input" placeholder="Kunde suchen..." value={search} onChange={(e) => setSearch(e.target.value)} style={{ maxWidth: 320 }} />
|
||||
<input className="form-control search-input" placeholder="Kunde suchen …" value={search} onChange={(e) => setSearch(e.target.value)} style={{ maxWidth: 320 }} />
|
||||
</div>
|
||||
<div className="ui-card-body" style={{ padding: 0 }}>
|
||||
{loading ? (
|
||||
<div className="empty"><FaSpinner className="spinner" /></div>
|
||||
) : filtered.length === 0 ? (
|
||||
<EmptyState title="Keine Eintraege" hint="Lege einen neuen Lead an." />
|
||||
<EmptyState title="Keine Einträge" hint="Keine Kunden in dieser Ansicht." />
|
||||
) : (
|
||||
<div className="list">
|
||||
{filtered.map((c) => {
|
||||
@@ -141,3 +166,44 @@ function CreateLeadForm({ onCancel, onDone }) {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Fester Kunde mit Zugang zum Kundenportal (nur Admins; legt den Login im Portal an)
|
||||
function CreateCustomerForm({ onCancel, onCreate }) {
|
||||
const [form, setForm] = useState({ name: '', email: '', code: '', phone: '', location: '', password: '' })
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const set = (field) => (e) => setForm((f) => ({ ...f, [field]: e.target.value }))
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (form.password.length < 8) { setError('Das Portal-Passwort muss mindestens 8 Zeichen haben.'); return }
|
||||
setBusy(true); setError(null)
|
||||
const r = await onCreate({ ...form, name: form.name.trim(), email: form.email.trim() })
|
||||
if (!r.success) {
|
||||
setError(r.error || 'Kunde konnte nicht angelegt werden.')
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Neuer Kunde mit Portal-Zugang">
|
||||
<form onSubmit={submit}>
|
||||
<div className="flex gap-3 wrap">
|
||||
<Field label="Unternehmen / Name"><input className="form-control" name="name" value={form.name} onChange={set('name')} required /></Field>
|
||||
<Field label="E-Mail (Portal-Login)"><input className="form-control" type="email" name="email" value={form.email} onChange={set('email')} required /></Field>
|
||||
<Field label="Kundennummer"><input className="form-control" name="code" value={form.code} onChange={set('code')} /></Field>
|
||||
</div>
|
||||
<div className="flex gap-3 wrap">
|
||||
<Field label="Telefon"><input className="form-control" name="phone" value={form.phone} onChange={set('phone')} /></Field>
|
||||
<Field label="Standort"><input className="form-control" name="location" value={form.location} onChange={set('location')} /></Field>
|
||||
<Field label="Portal-Passwort (mind. 8 Zeichen)"><input className="form-control" type="password" autoComplete="new-password" name="password" value={form.password} onChange={set('password')} required /></Field>
|
||||
</div>
|
||||
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary" type="submit" disabled={busy}>{busy ? <FaSpinner className="spinner" /> : 'Kunde anlegen'}</Button>
|
||||
<Button variant="ghost" type="button" onClick={onCancel}>Abbrechen</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user