Finanzen: Reiter Rechnungen am Kunden nur fuer Admins, Badge Adresse unvollstaendig
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2aacdb2d56
commit
edf118a044
4 files changed
+63
-15
No files matched your search
@@ -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'
|
||||||
|
}
|
||||||
@@ -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,
|
* 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) {
|
export function customerInvoicesGate(customer) {
|
||||||
const check = canInvoice(customer)
|
const check = canInvoice(customer)
|
||||||
@@ -776,7 +776,7 @@ export function customerInvoicesGate(customer) {
|
|||||||
|
|
||||||
// --- Buchungen (Block 7) -----------------------------------------------------------------------------------
|
// --- 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)
|
const dayOf = (tx) => text(tx.bookingDate) || text(tx.valueDate)
|
||||||
|
|
||||||
/** Buchungen neueste zuerst (Buchungsdatum, sonst Wertstellung); gleiche Tage behalten die Reihenfolge des Servers. */
|
/** Buchungen neueste zuerst (Buchungsdatum, sonst Wertstellung); gleiche Tage behalten die Reihenfolge des Servers. */
|
||||||
|
|||||||
@@ -10,17 +10,11 @@ import CopyableCredential, { getCustomerPortalPassword } from '../components/Cop
|
|||||||
import { updateCustomerWithPortalAccess, deleteCustomerWithPortalAccess } from '../lib/customerAdminApi'
|
import { updateCustomerWithPortalAccess, deleteCustomerWithPortalAccess } from '../lib/customerAdminApi'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||||
|
import { customerTabs, resolveCustomerTab } from '../lib/customerTabs'
|
||||||
import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
|
import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
|
||||||
|
|
||||||
const PORTAL_URL = 'https://project.webklar.com'
|
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() {
|
export default function CustomerDetailPage() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const [customer, setCustomer] = useState(null)
|
const [customer, setCustomer] = useState(null)
|
||||||
@@ -48,6 +42,8 @@ export default function CustomerDetailPage() {
|
|||||||
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>
|
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 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 archived = isArchived(customer)
|
||||||
|
|
||||||
const toggleArchived = async () => {
|
const toggleArchived = async () => {
|
||||||
@@ -91,12 +87,12 @@ export default function CustomerDetailPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs tabs={TABS} active={tab} onChange={setTab} />
|
<Tabs tabs={customerTabs(isAdmin)} active={activeTab} onChange={setTab} />
|
||||||
|
|
||||||
{tab === 'overview' && <OverviewTab customer={customer} stage={stage} onSaved={load} />}
|
{activeTab === 'overview' && <OverviewTab customer={customer} stage={stage} onSaved={load} />}
|
||||||
{tab === 'tickets' && <CustomerTickets customerId={customer.$id} />}
|
{activeTab === 'tickets' && <CustomerTickets customerId={customer.$id} />}
|
||||||
{tab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}
|
{activeTab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}
|
||||||
{tab === 'projects' && <CustomerProjects customerId={customer.$id} />}
|
{activeTab === 'projects' && <CustomerProjects customerId={customer.$id} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -172,7 +168,7 @@ function OverviewTab({ customer, stage, onSaved }) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="Rechnungsdaten">
|
<Card title="Rechnungsdaten">
|
||||||
{!ready && <div className="badge badge-warn" style={{ marginBottom: 12 }}>Unvollständig – für Rechnungen erforderlich</div>}
|
{!ready && <div className="badge badge-warn" style={{ marginBottom: 12 }}>Adresse unvollständig</div>}
|
||||||
<Field label="Strasse + Nr."><input className="form-control" value={form.street} onChange={(e) => setForm({ ...form, street: e.target.value })} /></Field>
|
<Field label="Strasse + Nr."><input className="form-control" value={form.street} onChange={(e) => setForm({ ...form, street: e.target.value })} /></Field>
|
||||||
<div className="flex gap-3 wrap">
|
<div className="flex gap-3 wrap">
|
||||||
<Field label="PLZ"><input className="form-control" value={form.postalCode} onChange={(e) => setForm({ ...form, postalCode: e.target.value })} /></Field>
|
<Field label="PLZ"><input className="form-control" value={form.postalCode} onChange={(e) => setForm({ ...form, postalCode: e.target.value })} /></Field>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
})
|
||||||
Reference in new issue
Block a user