From 7d4676121eebde14a2c854e89706be312abf7740 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:52:36 +0000 Subject: [PATCH] Kundenliste als Tabelle mit Reitern und Zaehlern, Neuer Kunde und Neuer Lead als Dialoge Zeilenklick nur ueber isRowActivation, Link.row-link in der ersten Zelle. CreateCustomerForm bleibt exportiert (Zuordnen-Assistent), dort entfaellt der Umschlag um den Dialog. KOMPAT-Regel .side-logo gestrichen. Co-Authored-By: Claude Sonnet 5.5 --- src/pages/AssignProjectPage.jsx | 24 ++- src/pages/CustomersPage.jsx | 291 ++++++++++++++++++-------------- src/styles/global.css | 6 +- 3 files changed, 174 insertions(+), 147 deletions(-) diff --git a/src/pages/AssignProjectPage.jsx b/src/pages/AssignProjectPage.jsx index ab40790..bea7f9a 100644 --- a/src/pages/AssignProjectPage.jsx +++ b/src/pages/AssignProjectPage.jsx @@ -274,19 +274,17 @@ export default function AssignProjectPage() { )} {showNewCustomer && isAdmin && ( -
- setShowNewCustomer(false)} - onCreate={async (data) => { - const r = await createCustomer(data) - if (r.success) { - setShowNewCustomer(false) - patch({ customerId: r.data.$id, ticketId: '', newTicket: null }) - } - return r - }} - /> -
+ setShowNewCustomer(false)} + onCreate={async (data) => { + const r = await createCustomer(data) + if (r.success) { + setShowNewCustomer(false) + patch({ customerId: r.data.$id, ticketId: '', newTicket: null }) + } + return r + }} + /> )}
{filteredCustomers.slice(0, 100).map((c) => ( diff --git a/src/pages/CustomersPage.jsx b/src/pages/CustomersPage.jsx index d7f65f6..f69a768 100644 --- a/src/pages/CustomersPage.jsx +++ b/src/pages/CustomersPage.jsx @@ -1,37 +1,74 @@ import { useState, useMemo } from 'react' -import { Link } from 'react-router-dom' -import { FaSpinner, FaPlus, FaStar } from 'react-icons/fa6' +import { Link, useNavigate } from 'react-router-dom' import { useCustomers } from '../hooks/useCustomers' import { useAuth } from '../context/AuthContext' -import { createLead, customerStage, isArchived } from '../lib/leads' -import { PageHeader, Card, Button, Badge, EmptyState, Field } from '../components/ui' +import { createLead } from '../lib/leads' +import { CUSTOMER_TABS, customerBadges, customerTitle, filterCustomers, stageCounts } from '../lib/customerList' +import { isRowActivation } from '../lib/rowClick' +import { PageHeader, Tabs, Button, Badge, EmptyState, Field, Dialog } from '../components/ui' export default function CustomersPage() { const { customers, loading, refresh, createCustomer } = useCustomers() const { user, isAdmin } = useAuth() + const navigate = useNavigate() const [search, setSearch] = useState('') // 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 [stage, setStage] = useState('customer') // customer | lead | archived | all const [showCreate, setShowCreate] = useState(null) // null | 'lead' | 'customer' - const filtered = useMemo(() => { - return (customers || []) - .filter((c) => { - const archived = isArchived(c) - // Archivierte nur im Archiviert-Tab, sonst nie - if (stage === 'archived') return archived - if (archived) return false - const st = customerStage(c) - if (st === 'lost') return stage === 'all' - if (stage === 'all') return true - return st === stage - }) - .filter((c) => { - if (!search) return true - const s = search.toLowerCase() - return `${c.name} ${c.code || ''} ${c.location || ''} ${c.email || ''}`.toLowerCase().includes(s) - }) - }, [customers, search, stage]) + const filtered = useMemo(() => filterCustomers(customers, { stage, search }), [customers, stage, search]) + // Zähler mit derselben Funktion wie die Liste (beachten den Suchtext); bis zum Laden ohne Zahl + const counts = useMemo(() => stageCounts(customers, search), [customers, search]) + const tabs = CUSTOMER_TABS.map((tab) => ({ id: tab.id, label: tab.label, count: loading ? undefined : counts[tab.id] })) + + let content + if (loading) { + content =
Wird geladen …
+ } else if (filtered.length === 0) { + content = + } else { + content = ( + + + + + + + + + + + + {filtered.map((c) => { + const path = `/customers/${c.$id}` + return ( + { + if (isRowActivation(e)) navigate(path) + }} + > + + + + + + + ) + })} + +
UnternehmenKundennummerStandortE-MailTelefon
+
+ {customerTitle(c)} + {customerBadges(c).map((b) => ( + {b.label} + ))} +
+ {c.contactName &&
{c.contactName}
} +
{c.code || –}{c.location || '–'}{c.email || '–'}{c.phone || '–'}
+ ) + } return (
@@ -41,89 +78,54 @@ export default function CustomersPage() { actions={ <> {isAdmin && ( - + )} - + } /> + + +
+ setSearch(e.target.value)} + /> +
+ + {content} + {showCreate === 'lead' && ( -
- setShowCreate(null)} - onDone={async (data) => { - const r = await createLead(data, user) - setShowCreate(null) - refresh() - return r - }} - /> -
+ setShowCreate(null)} + onDone={async (data) => { + const r = await createLead(data, user) + setShowCreate(null) + refresh() + return r + }} + /> )} {showCreate === 'customer' && isAdmin && ( -
- setShowCreate(null)} - onCreate={async (data) => { - const r = await createCustomer(data) - if (r.success) { - setShowCreate(null) - setStage('customer') - refresh() - } - return r - }} - /> -
+ setShowCreate(null)} + onCreate={async (data) => { + const r = await createCustomer(data) + if (r.success) { + setShowCreate(null) + setStage('customer') + refresh() + } + return r + }} + /> )} - - -
-
- - - - -
- setSearch(e.target.value)} style={{ maxWidth: 320 }} /> -
-
- {loading ? ( -
- ) : filtered.length === 0 ? ( - - ) : ( -
- {filtered.map((c) => { - const st = customerStage(c) - return ( - -
- {(c.name || '?').charAt(0).toUpperCase()} -
-
-
- {c.importantCustomer && } - {c.name} - {c.code && · {c.code}} -
-
{[c.location, c.email].filter(Boolean).join(' · ') || '-'}
-
-
- {isArchived(c) && Archiviert} - {st === 'lead' && Lead} - {st === 'customer' && Fester Kunde} - {st === 'lost' && Abgesagt} -
- - ) - })} -
- )} -
-
) } @@ -147,22 +149,33 @@ function CreateLeadForm({ onCancel, onDone }) { } return ( - -

+

+ + + + + } + > + {error &&
{error}
} +

Fuer einen Lead reichen Unternehmen und E-Mail. Es wird automatisch ein Akquise-Ticket erstellt.

-
-
- setCompany(e.target.value)} required /> - setEmail(e.target.value)} /> -
- {error &&
{error}
} -
- - -
-
-
+
+ + setCompany(e.target.value)} required /> + + + setEmail(e.target.value)} /> + +
+ ) } @@ -185,24 +198,44 @@ export function CreateCustomerForm({ onCancel, onCreate }) { } return ( - -
-
- - - -
-
- - - -
- {error &&
{error}
} -
- - -
-
-
+ + + + + + } + > + {error &&
{error}
} +
+ + + + + + + + + +
+
+ + + + + + + + + +
+
) } diff --git a/src/styles/global.css b/src/styles/global.css index da9916d..2e8ad2d 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1124,12 +1124,8 @@ select.form-control { .text-grey { color: var(--text-2); } .text-red { color: var(--red-fg); } -/* Zaehler und Initiale der Altlisten: .side-count bis P6 (Chat-Liste), .side-logo bis P4 (Kundenliste) */ +/* Zaehler der Altliste: .side-count bis P6 (Chat-Liste) */ .side-count { margin-left: auto; font-size: 12px; font-weight: 500; color: var(--text-2); font-variant-numeric: tabular-nums; } -.side-logo { - display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 32px; height: 32px; - border-radius: var(--radius); background: var(--grey-bg); color: var(--grey-fg); font-weight: 600; -} /* Neutral: Karte, Raster */ .card { margin-bottom: 16px; background: none; border: 0; border-radius: 0; color: var(--text); }