diff --git a/src/lib/customerTabs.js b/src/lib/customerTabs.js new file mode 100644 index 0000000..e9056b7 --- /dev/null +++ b/src/lib/customerTabs.js @@ -0,0 +1,19 @@ +// Reiter der Kundenansicht (rein, ohne React; unter Node testbar). „Rechnungen“ ruft die Finanzen-API des Portals auf, +// die nur Admins freigibt: für alle anderen gibt es den Reiter nicht. + +const BASE_TABS = [ + { id: 'overview', label: 'Uebersicht' }, + { id: 'tickets', label: 'Tickets' }, + { id: 'invoices', label: 'Rechnungen', adminOnly: true }, + { id: 'projects', label: 'Projekte' }, +] + +/** Reiter für Tabs: [{ id, label }]; „Rechnungen“ nur für Admins. */ +export function customerTabs(isAdmin) { + return BASE_TABS.filter((t) => !t.adminOnly || isAdmin === true).map(({ id, label }) => ({ id, label })) +} + +/** Der Reiter, der tatsächlich gezeigt wird: ein nicht (mehr) erlaubter oder unbekannter Reiter fällt auf „Übersicht“ zurück. */ +export function resolveCustomerTab(tab, isAdmin) { + return customerTabs(isAdmin).some((t) => t.id === tab) ? tab : 'overview' +} diff --git a/src/lib/financeView.js b/src/lib/financeView.js index 5b2f722..4775d23 100644 --- a/src/lib/financeView.js +++ b/src/lib/financeView.js @@ -748,7 +748,7 @@ export function resolveCustomerText(options, input) { /** * Reiter „Rechnungen“ am Kunden: { ok: true } oder { ok: false, title, hint } (Grund nach canInvoice plus ein Satz, - * was zu tun ist). Der Reiter ist für jeden Kunden sichtbar; ohne ok zeigt er statt der Liste diesen Hinweis. + * was zu tun ist). Der Reiter ist für Admins bei jedem Kunden sichtbar; ohne ok zeigt er statt der Liste diesen Hinweis. */ export function customerInvoicesGate(customer) { const check = canInvoice(customer) @@ -776,7 +776,7 @@ export function customerInvoicesGate(customer) { // --- Buchungen (Block 7) ----------------------------------------------------------------------------------- -const RANK ={ hoch: 0, mittel: 1, niedrig: 2 } +const RANK = { hoch: 0, mittel: 1, niedrig: 2 } const dayOf = (tx) => text(tx.bookingDate) || text(tx.valueDate) /** Buchungen neueste zuerst (Buchungsdatum, sonst Wertstellung); gleiche Tage behalten die Reihenfolge des Servers. */ diff --git a/src/pages/CustomerDetailPage.jsx b/src/pages/CustomerDetailPage.jsx index 58be87b..0e7abf0 100644 --- a/src/pages/CustomerDetailPage.jsx +++ b/src/pages/CustomerDetailPage.jsx @@ -10,17 +10,11 @@ import CopyableCredential, { getCustomerPortalPassword } from '../components/Cop import { updateCustomerWithPortalAccess, deleteCustomerWithPortalAccess } from '../lib/customerAdminApi' import { useAuth } from '../context/AuthContext' import { useWebsiteProjects } from '../hooks/useWebsiteProjects' +import { customerTabs, resolveCustomerTab } from '../lib/customerTabs' import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads' const PORTAL_URL = 'https://project.webklar.com' -const TABS = [ - { id: 'overview', label: 'Uebersicht' }, - { id: 'tickets', label: 'Tickets' }, - { id: 'invoices', label: 'Rechnungen' }, - { id: 'projects', label: 'Projekte' }, -] - export default function CustomerDetailPage() { const { id } = useParams() const [customer, setCustomer] = useState(null) @@ -48,6 +42,8 @@ export default function CustomerDetailPage() { if (error || !customer) return
Zurück} />
const stage = customerStage(customer) + // „Rechnungen“ nur für Admins (die Finanzen-API gibt sonst 403); ein nicht erlaubter Reiter fällt auf die Übersicht zurück + const activeTab = resolveCustomerTab(tab, isAdmin) const archived = isArchived(customer) const toggleArchived = async () => { @@ -91,12 +87,12 @@ export default function CustomerDetailPage() { } /> - + - {tab === 'overview' && } - {tab === 'tickets' && } - {tab === 'invoices' && setTab('overview')} />} - {tab === 'projects' && } + {activeTab === 'overview' && } + {activeTab === 'tickets' && } + {activeTab === 'invoices' && setTab('overview')} />} + {activeTab === 'projects' && } ) } @@ -172,7 +168,7 @@ function OverviewTab({ customer, stage, onSaved }) { - {!ready &&
Unvollständig – für Rechnungen erforderlich
} + {!ready &&
Adresse unvollständig
} setForm({ ...form, street: e.target.value })} />
setForm({ ...form, postalCode: e.target.value })} /> diff --git a/tests/unit/customerTabs.test.mjs b/tests/unit/customerTabs.test.mjs new file mode 100644 index 0000000..2fa5325 --- /dev/null +++ b/tests/unit/customerTabs.test.mjs @@ -0,0 +1,33 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { customerTabs, resolveCustomerTab } = await loadModule('src/lib/customerTabs.js') + +test('customerTabs: Rechnungen nur für Admins, Reihenfolge bleibt', () => { + assert.deepEqual(customerTabs(true), [ + { id: 'overview', label: 'Uebersicht' }, + { id: 'tickets', label: 'Tickets' }, + { id: 'invoices', label: 'Rechnungen' }, + { id: 'projects', label: 'Projekte' }, + ]) + const staff = customerTabs(false) + assert.deepEqual(staff.map((t) => t.id), ['overview', 'tickets', 'projects']) + // unbekannt oder nicht eindeutig true gilt als kein Admin + for (const v of [undefined, null, 0, '', 'true']) { + assert.deepEqual(customerTabs(v).map((t) => t.id), ['overview', 'tickets', 'projects'], String(v)) + } + // die Einträge tragen nur id und label (kein internes Feld) + assert.deepEqual(Object.keys(customerTabs(true)[2]), ['id', 'label']) +}) + +test('resolveCustomerTab: erlaubter Reiter bleibt, sonst Übersicht', () => { + assert.equal(resolveCustomerTab('invoices', true), 'invoices') + assert.equal(resolveCustomerTab('invoices', false), 'overview') + assert.equal(resolveCustomerTab('invoices', undefined), 'overview') + assert.equal(resolveCustomerTab('tickets', false), 'tickets') + assert.equal(resolveCustomerTab('projects', false), 'projects') + assert.equal(resolveCustomerTab('overview', false), 'overview') + assert.equal(resolveCustomerTab('gibtsnicht', true), 'overview') + assert.equal(resolveCustomerTab(undefined, true), 'overview') +})