Kunden-Tabs: ExternalLink-Icon neben jedem Reiter
Tabs-Komponente bekommt optionalen tabHrefBuilder(tabId)-Prop. Wenn gesetzt, erscheint neben jedem Tab-Label ein kleines ExternalLink-Icon, das den Tab via ?tab=<id> in einem neuen Browser-Tab öffnet. CustomerDetail übergibt den Builder. URL-Param wird eh schon für den Tab-Sync genutzt – Anhängen reicht. Click-stopPropagation verhindert, dass der Klick auf das Icon gleichzeitig den Tab-Wechsel auslöst.
This commit is contained in:
@@ -97,6 +97,14 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🆕 Kunden-Detail-Tabs: Pro-Tab-Link „in neuem Tab öffnen"**
|
||||||
|
- `Tabs`-Komponente um optionalen Prop `tabHrefBuilder(tabId)` erweitert.
|
||||||
|
Wenn gesetzt, erscheint neben jedem Tab-Label ein kleines
|
||||||
|
`ExternalLink`-Icon. CustomerDetail übergibt den Builder mit
|
||||||
|
`?tab=<id>`-Query-Param, der eh schon vom URL-Sync genutzt wird.
|
||||||
|
- `target="_blank"` + `rel="noopener noreferrer"` + Klick-stopPropagation,
|
||||||
|
damit der Tab-Wechsel nicht parallel zur Tab-Aktivierung passiert.
|
||||||
|
|
||||||
- [x] **🆕 Vertrag: Kunden-/Vertragsnummer bei Vertriebsplattform**
|
- [x] **🆕 Vertrag: Kunden-/Vertragsnummer bei Vertriebsplattform**
|
||||||
- Zwei neue optionale Felder
|
- Zwei neue optionale Felder
|
||||||
`Contract.customerNumberAtSalesPlatform` +
|
`Contract.customerNumberAtSalesPlatform` +
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReactNode, useState, useEffect } from 'react';
|
import { ReactNode, useState, useEffect } from 'react';
|
||||||
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -11,9 +12,21 @@ interface TabsProps {
|
|||||||
defaultTab?: string;
|
defaultTab?: string;
|
||||||
activeTab?: string;
|
activeTab?: string;
|
||||||
onTabChange?: (tabId: string) => void;
|
onTabChange?: (tabId: string) => void;
|
||||||
|
/**
|
||||||
|
* Optional: liefert die URL, unter der ein einzelner Tab in einem
|
||||||
|
* neuen Tab geöffnet werden kann. Wenn gesetzt, erscheint neben jedem
|
||||||
|
* Tab-Label ein kleines „im neuen Tab öffnen"-Icon.
|
||||||
|
*/
|
||||||
|
tabHrefBuilder?: (tabId: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tabs({ tabs, defaultTab, activeTab: controlledTab, onTabChange }: TabsProps) {
|
export default function Tabs({
|
||||||
|
tabs,
|
||||||
|
defaultTab,
|
||||||
|
activeTab: controlledTab,
|
||||||
|
onTabChange,
|
||||||
|
tabHrefBuilder,
|
||||||
|
}: TabsProps) {
|
||||||
const [internalTab, setInternalTab] = useState(defaultTab || tabs[0]?.id);
|
const [internalTab, setInternalTab] = useState(defaultTab || tabs[0]?.id);
|
||||||
const activeTab = controlledTab ?? internalTab;
|
const activeTab = controlledTab ?? internalTab;
|
||||||
|
|
||||||
@@ -31,19 +44,33 @@ export default function Tabs({ tabs, defaultTab, activeTab: controlledTab, onTab
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="border-b border-gray-200">
|
<div className="border-b border-gray-200">
|
||||||
<nav className="flex -mb-px space-x-8">
|
<nav className="flex -mb-px space-x-6">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<div key={tab.id} className="flex items-center gap-1">
|
||||||
key={tab.id}
|
<button
|
||||||
onClick={() => handleTabChange(tab.id)}
|
onClick={() => handleTabChange(tab.id)}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm whitespace-nowrap ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm whitespace-nowrap ${
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'border-blue-500 text-blue-600'
|
? 'border-blue-500 text-blue-600'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
|
{tabHrefBuilder && (
|
||||||
|
<a
|
||||||
|
href={tabHrefBuilder(tab.id)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="text-gray-400 hover:text-blue-600 p-0.5"
|
||||||
|
title={`${tab.label} in neuem Tab öffnen`}
|
||||||
|
aria-label={`${tab.label} in neuem Tab öffnen`}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -411,7 +411,13 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Tabs tabs={tabs} defaultTab={defaultTab} activeTab={activeTab} onTabChange={handleTabChange} />
|
<Tabs
|
||||||
|
tabs={tabs}
|
||||||
|
defaultTab={defaultTab}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={handleTabChange}
|
||||||
|
tabHrefBuilder={(tabId) => `${location.pathname}?tab=${tabId}`}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<AddressModal
|
<AddressModal
|
||||||
|
|||||||
Reference in New Issue
Block a user