From 5a066eb3c41a08e238277c52151f32d619d67392 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:19:10 +0000 Subject: [PATCH] Kunden an einer Stelle pflegen (Paket E) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/hooks/useCustomers.js | 22 ++--- src/lib/customerAdminApi.js | 4 - src/pages/AdminPage.jsx | 164 +------------------------------ src/pages/CustomerDetailPage.jsx | 24 ++++- src/pages/CustomersPage.jsx | 90 ++++++++++++++--- 5 files changed, 111 insertions(+), 193 deletions(-) diff --git a/src/hooks/useCustomers.js b/src/hooks/useCustomers.js index 147afb1..7298d72 100644 --- a/src/hooks/useCustomers.js +++ b/src/hooks/useCustomers.js @@ -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) diff --git a/src/lib/customerAdminApi.js b/src/lib/customerAdminApi.js index 21f6829..fc76d21 100644 --- a/src/lib/customerAdminApi.js +++ b/src/lib/customerAdminApi.js @@ -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', diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx index 4ec6c16..939df38 100644 --- a/src/pages/AdminPage.jsx +++ b/src/pages/AdminPage.jsx @@ -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() {
Systemverwaltung

Admin

-

Kundenkonten und Mitarbeiterzugänge verwalten.

+

Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“.

Angemeldet als @@ -213,7 +138,6 @@ export default function AdminPage() {
- - - - - - - - - -
-
-

Kundenkonten

E-Mail sichtbar, Passwort geschützt
-
- {customersLoading ? ( -
Kunden werden geladen …
- ) : customers.length === 0 ? ( -

Noch keine Kunden

Lege links den ersten Portalzugang an.

- ) : ( -
- - - - {customers.map((customer) => ( - - - - - - - - ))} - -
Code / KundeZugangStandort / TelefonStatusAktionen
{customer.code || 'Ohne Code'}{customer.name || '–'} - - - {customer.location || '–'}{customer.phone || '–'}{customer.portalAccessEnabled ? 'Aktiv' : 'Inaktiv'} - - -
-
- )} -
- - )} - {activeSection === 'employees' && (
{createdCredential && ( diff --git a/src/pages/CustomerDetailPage.jsx b/src/pages/CustomerDetailPage.jsx index 4ca7743..e857a67 100644 --- a/src/pages/CustomerDetailPage.jsx +++ b/src/pages/CustomerDetailPage.jsx @@ -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
- if (error || !customer) return
Zurueck} />
+ if (error || !customer) return
Zurück} />
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 (
{archived ? <> Wiederherstellen : <> Archivieren} + {isAdmin && } } @@ -154,6 +169,7 @@ function OverviewTab({ customer, stage, onSaved }) { setForm({ ...form, email: e.target.value })} /> setForm({ ...form, phone: e.target.value })} />
+ setForm({ ...form, location: e.target.value })} /> setForm({ ...form, contactName: e.target.value })} />