mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +02:00
feat(frontend): redesign CompanyDetailPage with tabbed layout
Replace 3-column grid with 4-tab layout: - Tab 1 (Unternehmen): 2-column grid with master data + Lexware status (left), contacts + relationships (right) - Tab 2 (Aktivitäten): ActivityFeed component at full width - Tab 3 (Vorgänge): Combined CRM deals + Lexware vouchers table with source badges, filters, and color-coded Lexware rows - Tab 4 (Verträge): ContractsCard placeholder at full width Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dbbde6c4fa
commit
923e6bc127
2 changed files with 957 additions and 303 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/* ============================================================
|
||||
CompanyDetailPage – 3-Spalten Layout & Komponenten
|
||||
CompanyDetailPage – Tabbed Layout
|
||||
============================================================ */
|
||||
|
||||
.backLink {
|
||||
|
|
@ -49,29 +49,63 @@
|
|||
}
|
||||
|
||||
/* ============================================================
|
||||
3-Spalten Grid
|
||||
Tab Bar
|
||||
============================================================ */
|
||||
|
||||
.layout {
|
||||
.tabBar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tabActive {
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Tab 1: Unternehmensdaten – 2-Spalten Grid
|
||||
============================================================ */
|
||||
|
||||
.overviewGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr 360px;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr 360px;
|
||||
}
|
||||
.layout > :first-child {
|
||||
grid-column: 1 / -1;
|
||||
@media (max-width: 900px) {
|
||||
.overviewGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.rightStack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
|
@ -105,12 +139,12 @@
|
|||
}
|
||||
|
||||
/* ============================================================
|
||||
Info Grid (linke Spalte)
|
||||
Info Grid (Stammdaten)
|
||||
============================================================ */
|
||||
|
||||
.infoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
|
@ -158,7 +192,300 @@
|
|||
}
|
||||
|
||||
/* ============================================================
|
||||
Activity Feed (mittlere Spalte)
|
||||
Lexware Verknuepfungs-Info (in Stammdaten-Card)
|
||||
============================================================ */
|
||||
|
||||
.lexwareInfo {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.lexwareStatusRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.lexwareBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lexwareBadgeLinked {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.lexwareBadgeNotLinked {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .lexwareBadgeLinked {
|
||||
background: #064e3b;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.lexwareActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.lexwareActionBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.lexwareActionBtn:hover {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.lexwareActionBtn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.lexwareActionBtnPrimary {
|
||||
composes: lexwareActionBtn;
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.lexwareActionBtnPrimary:hover {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.lexwareActionBtnDanger {
|
||||
composes: lexwareActionBtn;
|
||||
border-color: #fecaca;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.lexwareActionBtnDanger:hover {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .lexwareActionBtnDanger:hover {
|
||||
background: #450a0a;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Compact Table (Kontakte in Stammdaten-Tab)
|
||||
============================================================ */
|
||||
|
||||
.compactTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.compactTable th {
|
||||
text-align: left;
|
||||
padding: 0.375rem 0;
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.compactTable td {
|
||||
padding: 0.375rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.compactTable tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.compactTable tr[data-clickable='true'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.compactTable tr[data-clickable='true']:hover td {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Tab 3: Vorgaenge — Kombinierte Deals + Vouchers Tabelle
|
||||
============================================================ */
|
||||
|
||||
.vorgaengeHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filterRow {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filterSelect {
|
||||
padding: 0.375rem 0.625rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8125rem;
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filterSelect:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.refreshBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.refreshBtn:hover {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.refreshBtn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.vorgaengeTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.vorgaengeTable th {
|
||||
text-align: left;
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-weight: 500;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.vorgaengeTable td {
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.vorgaengeTable tbody tr {
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.vorgaengeTable tbody tr:hover {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.vorgaengeTableCard {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Lexware-Zeilen farblich hervorheben */
|
||||
.lexwareRow {
|
||||
background: rgba(99, 102, 241, 0.03);
|
||||
border-left: 3px solid rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.lexwareRow:hover {
|
||||
background: rgba(99, 102, 241, 0.07) !important;
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .lexwareRow {
|
||||
background: rgba(99, 102, 241, 0.06);
|
||||
border-left-color: rgba(129, 140, 248, 0.4);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .lexwareRow:hover {
|
||||
background: rgba(99, 102, 241, 0.12) !important;
|
||||
}
|
||||
|
||||
/* Quell-Badges */
|
||||
.sourceBadge {
|
||||
display: inline-block;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.sourceBadgeCrm {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.sourceBadgeLexware {
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .sourceBadgeLexware {
|
||||
background: #312e81;
|
||||
color: #a5b4fc;
|
||||
}
|
||||
|
||||
/* Leerer Zustand in Tabelle */
|
||||
.emptyRow {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Activity Feed — von ActivityFeed.tsx genutzt
|
||||
============================================================ */
|
||||
|
||||
.feedContainer {
|
||||
|
|
@ -344,7 +671,7 @@
|
|||
}
|
||||
|
||||
/* ============================================================
|
||||
Relationships Card (rechte Spalte)
|
||||
Relationships Card
|
||||
============================================================ */
|
||||
|
||||
.relationItem {
|
||||
|
|
@ -449,40 +776,3 @@
|
|||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Compact Table (rechte Spalte)
|
||||
============================================================ */
|
||||
|
||||
.compactTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.compactTable th {
|
||||
text-align: left;
|
||||
padding: 0.375rem 0;
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.compactTable td {
|
||||
padding: 0.375rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.compactTable tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.compactTable tr[data-clickable='true'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.compactTable tr[data-clickable='true']:hover td {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,59 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { useCompany, useDeleteCompany } from '../hooks';
|
||||
import {
|
||||
useCompany,
|
||||
useDeleteCompany,
|
||||
useCompanyVouchers,
|
||||
useRefreshCompanyVouchers,
|
||||
useLinkLexwareCompany,
|
||||
useUnlinkLexwareCompany,
|
||||
useSyncFromLexware,
|
||||
usePushToLexware,
|
||||
} from '../hooks';
|
||||
import { useCrmSettings } from '../settings/CrmSettingsContext';
|
||||
import { CompanyFormModal } from './CompanyFormModal';
|
||||
import { ActivityFeed } from './ActivityFeed';
|
||||
import { CompanyRelationshipsCard } from './CompanyRelationshipsCard';
|
||||
import { ContractsCard } from './ContractsCard';
|
||||
import { LexwareSearchModal } from '../lexware/LexwareSearchModal';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import { LexwareSection } from '../lexware/LexwareSection';
|
||||
import type { DealStatus } from '../types';
|
||||
import type { DealStatus, LexwareVoucher, Deal } from '../types';
|
||||
import { VOUCHER_TYPE_LABELS } from '../types';
|
||||
import styles from './CompanyDetailPage.module.css';
|
||||
|
||||
// ============================================================
|
||||
// Constants
|
||||
// Types
|
||||
// ============================================================
|
||||
|
||||
type DetailTab = 'company' | 'activities' | 'deals' | 'contracts';
|
||||
type SourceFilter = 'ALL' | 'CRM' | 'LEXWARE';
|
||||
|
||||
interface UnifiedItem {
|
||||
id: string;
|
||||
source: 'CRM' | 'LEXWARE';
|
||||
title: string;
|
||||
type: string;
|
||||
date: string | null;
|
||||
status: string;
|
||||
statusColor?: { bg: string; color: string };
|
||||
amount: number | null;
|
||||
currency: string;
|
||||
link: string;
|
||||
openExternal?: boolean;
|
||||
stageColor?: string;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Constants & Helpers
|
||||
// ============================================================
|
||||
|
||||
const TAB_LABELS: Record<DetailTab, string> = {
|
||||
company: 'Unternehmen',
|
||||
activities: 'Aktivitäten',
|
||||
deals: 'Vorgänge',
|
||||
contracts: 'Verträge',
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<DealStatus, { bg: string; color: string }> = {
|
||||
OPEN: { bg: '#dbeafe', color: '#1e40af' },
|
||||
WON: { bg: '#d1fae5', color: '#065f46' },
|
||||
|
|
@ -46,12 +86,38 @@ function formatDate(iso: string): string {
|
|||
export function CompanyDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { isModuleEnabled } = useCrmSettings();
|
||||
const lexwareEnabled = isModuleEnabled('lexware');
|
||||
|
||||
// ---- Core data ----
|
||||
const { data, isLoading, error } = useCompany(id!);
|
||||
const deleteMutation = useDeleteCompany();
|
||||
|
||||
// ---- UI State ----
|
||||
const [activeTab, setActiveTab] = useState<DetailTab>('company');
|
||||
const [isEditOpen, setEditOpen] = useState(false);
|
||||
const [isDeleteOpen, setDeleteOpen] = useState(false);
|
||||
const [isLexwareSearchOpen, setLexwareSearchOpen] = useState(false);
|
||||
const [sourceFilter, setSourceFilter] = useState<SourceFilter>('ALL');
|
||||
const [typeFilter, setTypeFilter] = useState('');
|
||||
|
||||
// ---- Lexware Mutations (must be called before conditional returns) ----
|
||||
const linkCompany = useLinkLexwareCompany();
|
||||
const unlinkCompany = useUnlinkLexwareCompany();
|
||||
const syncFromLexware = useSyncFromLexware();
|
||||
const pushToLexware = usePushToLexware();
|
||||
const refreshVouchers = useRefreshCompanyVouchers();
|
||||
|
||||
// ---- Lexware Vouchers ----
|
||||
const companyData = data?.data;
|
||||
const isLinked = !!companyData?.lexwareContactId;
|
||||
|
||||
const vouchersQuery = useCompanyVouchers(
|
||||
id!,
|
||||
isLinked && lexwareEnabled ? { pageSize: 100 } : undefined,
|
||||
);
|
||||
|
||||
// ---- Loading / Error ----
|
||||
if (isLoading) return <p>Laden...</p>;
|
||||
if (error || !data)
|
||||
return (
|
||||
|
|
@ -63,11 +129,20 @@ export function CompanyDetailPage() {
|
|||
const company = data.data;
|
||||
const contacts = company.contacts ?? [];
|
||||
const deals = company.deals ?? [];
|
||||
const vouchers: LexwareVoucher[] = vouchersQuery.data?.data ?? [];
|
||||
|
||||
// Industry badge color
|
||||
// Industry badge
|
||||
const industryColor = company.industryRef?.color ?? '#6366f1';
|
||||
const industryName = company.industryRef?.name ?? company.industry;
|
||||
|
||||
// Lexware mutation state
|
||||
const isAnyLexwareMutating =
|
||||
linkCompany.isPending ||
|
||||
unlinkCompany.isPending ||
|
||||
syncFromLexware.isPending ||
|
||||
pushToLexware.isPending ||
|
||||
refreshVouchers.isPending;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Zurück */}
|
||||
|
|
@ -142,259 +217,276 @@ export function CompanyDetailPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3-Spalten Layout */}
|
||||
<div className={styles.layout}>
|
||||
{/* ======== Linke Spalte: Stammdaten ======== */}
|
||||
<div>
|
||||
<div className={styles.card}>
|
||||
<h2 className={styles.cardTitle}>Unternehmensdaten</h2>
|
||||
<div className={styles.infoGrid}>
|
||||
{company.accountType && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Kontotyp</span>
|
||||
<span className={styles.infoValue}>{company.accountType.name}</span>
|
||||
</>
|
||||
)}
|
||||
{company.ownerName && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Zuständig</span>
|
||||
<span className={styles.infoValue}>{company.ownerName}</span>
|
||||
</>
|
||||
)}
|
||||
{company.email && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>E-Mail</span>
|
||||
<span className={styles.infoValue}>
|
||||
<a
|
||||
href={`mailto:${company.email}`}
|
||||
style={{ color: 'var(--color-primary)' }}
|
||||
>
|
||||
{company.email}
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{company.phone && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Telefon</span>
|
||||
<span className={styles.infoValue}>{company.phone}</span>
|
||||
</>
|
||||
)}
|
||||
{company.website && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Website</span>
|
||||
<span className={styles.infoValue}>
|
||||
<a
|
||||
href={company.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'var(--color-primary)' }}
|
||||
>
|
||||
{company.website}
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{(company.street || company.zip || company.city) && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Adresse</span>
|
||||
<span className={styles.infoValue}>
|
||||
{company.street && <>{company.street}<br /></>}
|
||||
{company.zip} {company.city}
|
||||
{company.country && company.country !== 'DE' && (
|
||||
<>, {company.country}</>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className={styles.infoLabel}>Erstellt</span>
|
||||
<span className={styles.infoValue}>
|
||||
{formatDate(company.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
{/* ======== Tab Bar ======== */}
|
||||
<div className={styles.tabBar}>
|
||||
{(Object.keys(TAB_LABELS) as DetailTab[]).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`${styles.tab} ${activeTab === tab ? styles.tabActive : ''}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
{TAB_LABELS[tab]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{company.tags && company.tags.length > 0 && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<span
|
||||
className={styles.infoLabel}
|
||||
style={{ display: 'block', marginBottom: '0.375rem' }}
|
||||
>
|
||||
Tags
|
||||
</span>
|
||||
<div className={styles.tags}>
|
||||
{company.tags.map((tag) => (
|
||||
<span key={tag} className={styles.tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notizen */}
|
||||
{company.notes && (
|
||||
<div className={styles.notesSection}>
|
||||
<span
|
||||
className={styles.infoLabel}
|
||||
style={{ display: 'block', marginBottom: '0.375rem' }}
|
||||
>
|
||||
Notizen
|
||||
</span>
|
||||
<p className={styles.notesText}>{company.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ======== Mittlere Spalte: Activity Feed ======== */}
|
||||
<div>
|
||||
<ActivityFeed companyId={company.id} />
|
||||
</div>
|
||||
|
||||
{/* ======== Rechte Spalte: Relations ======== */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
{/* Kontakte */}
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardTitleRow}>
|
||||
<h2 className={styles.cardTitle} style={{ marginBottom: 0 }}>
|
||||
Kontakte ({company._count?.contacts ?? contacts.length})
|
||||
</h2>
|
||||
</div>
|
||||
{contacts.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.8125rem' }}>
|
||||
Keine Kontakte vorhanden
|
||||
</p>
|
||||
) : (
|
||||
<table className={styles.compactTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Position</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{contacts.map((c) => {
|
||||
const name = [c.firstName, c.lastName]
|
||||
.filter(Boolean)
|
||||
.join(' ') || '—';
|
||||
return (
|
||||
<tr
|
||||
key={c.id}
|
||||
data-clickable="true"
|
||||
onClick={() => navigate(`/crm/contacts/${c.id}`)}
|
||||
>
|
||||
<td style={{ fontWeight: 500 }}>{name}</td>
|
||||
<td style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{c.position ?? '—'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Vorgänge */}
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardTitleRow}>
|
||||
<h2 className={styles.cardTitle} style={{ marginBottom: 0 }}>
|
||||
Vorgänge ({company._count?.deals ?? deals.length})
|
||||
</h2>
|
||||
</div>
|
||||
{deals.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.8125rem' }}>
|
||||
Keine Vorgänge vorhanden
|
||||
</p>
|
||||
) : (
|
||||
<table className={styles.compactTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Stufe</th>
|
||||
<th style={{ textAlign: 'right' }}>Wert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{deals.map((deal) => (
|
||||
<tr
|
||||
key={deal.id}
|
||||
data-clickable="true"
|
||||
onClick={() => navigate(`/crm/deals/${deal.id}`)}
|
||||
>
|
||||
<td>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<span style={{ fontWeight: 500 }}>{deal.title}</span>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 0.25rem',
|
||||
borderRadius: '9999px',
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 500,
|
||||
background: STATUS_COLORS[deal.status].bg,
|
||||
color: STATUS_COLORS[deal.status].color,
|
||||
}}
|
||||
>
|
||||
{STATUS_LABELS[deal.status]}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ minWidth: 80 }}>
|
||||
{deal.stage && (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: deal.stage.color,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
{deal.stage.name}
|
||||
</span>
|
||||
{/* ======== Tab Content ======== */}
|
||||
<div className={styles.tabContent}>
|
||||
{/* ---- Tab 1: Unternehmensdaten ---- */}
|
||||
{activeTab === 'company' && (
|
||||
<div className={styles.overviewGrid}>
|
||||
{/* Left: Stammdaten */}
|
||||
<div>
|
||||
<div className={styles.card}>
|
||||
<h2 className={styles.cardTitle}>Unternehmensdaten</h2>
|
||||
<div className={styles.infoGrid}>
|
||||
{company.accountType && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Kontotyp</span>
|
||||
<span className={styles.infoValue}>{company.accountType.name}</span>
|
||||
</>
|
||||
)}
|
||||
{company.ownerName && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Zuständig</span>
|
||||
<span className={styles.infoValue}>{company.ownerName}</span>
|
||||
</>
|
||||
)}
|
||||
{company.email && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>E-Mail</span>
|
||||
<span className={styles.infoValue}>
|
||||
<a
|
||||
href={`mailto:${company.email}`}
|
||||
style={{ color: 'var(--color-primary)' }}
|
||||
>
|
||||
{company.email}
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{company.phone && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Telefon</span>
|
||||
<span className={styles.infoValue}>{company.phone}</span>
|
||||
</>
|
||||
)}
|
||||
{company.website && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Website</span>
|
||||
<span className={styles.infoValue}>
|
||||
<a
|
||||
href={company.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'var(--color-primary)' }}
|
||||
>
|
||||
{company.website}
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{(company.street || company.zip || company.city) && (
|
||||
<>
|
||||
<span className={styles.infoLabel}>Adresse</span>
|
||||
<span className={styles.infoValue}>
|
||||
{company.street && <>{company.street}<br /></>}
|
||||
{company.zip} {company.city}
|
||||
{company.country && company.country !== 'DE' && (
|
||||
<>, {company.country}</>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||
{deal.value
|
||||
? currencyFormatter.format(parseFloat(deal.value))
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className={styles.infoLabel}>Erstellt</span>
|
||||
<span className={styles.infoValue}>
|
||||
{formatDate(company.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{company.tags && company.tags.length > 0 && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<span
|
||||
className={styles.infoLabel}
|
||||
style={{ display: 'block', marginBottom: '0.375rem' }}
|
||||
>
|
||||
Tags
|
||||
</span>
|
||||
<div className={styles.tags}>
|
||||
{company.tags.map((tag) => (
|
||||
<span key={tag} className={styles.tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notizen */}
|
||||
{company.notes && (
|
||||
<div className={styles.notesSection}>
|
||||
<span
|
||||
className={styles.infoLabel}
|
||||
style={{ display: 'block', marginBottom: '0.375rem' }}
|
||||
>
|
||||
Notizen
|
||||
</span>
|
||||
<p className={styles.notesText}>{company.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lexware Verknüpfung */}
|
||||
{lexwareEnabled && (
|
||||
<div className={styles.lexwareInfo}>
|
||||
<div className={styles.lexwareStatusRow}>
|
||||
<span
|
||||
className={`${styles.lexwareBadge} ${
|
||||
isLinked ? styles.lexwareBadgeLinked : styles.lexwareBadgeNotLinked
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: isLinked ? '#059669' : 'var(--color-text-muted)',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
LX {isLinked ? 'Verknüpft' : 'Nicht verknüpft'}
|
||||
</span>
|
||||
{isLinked && company.lexwareSyncedAt && (
|
||||
<span style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>
|
||||
Sync: {formatDate(company.lexwareSyncedAt)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.lexwareActions}>
|
||||
{!isLinked ? (
|
||||
<button
|
||||
className={styles.lexwareActionBtnPrimary}
|
||||
onClick={() => setLexwareSearchOpen(true)}
|
||||
disabled={isAnyLexwareMutating}
|
||||
>
|
||||
Lexware Kontakt suchen
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={styles.lexwareActionBtn}
|
||||
onClick={() => syncFromLexware.mutate({ entityType: 'company', entityId: company.id })}
|
||||
disabled={isAnyLexwareMutating}
|
||||
>
|
||||
{syncFromLexware.isPending ? 'Sync...' : 'Sync'}
|
||||
</button>
|
||||
<button
|
||||
className={styles.lexwareActionBtn}
|
||||
onClick={() => pushToLexware.mutate({ entityType: 'company', entityId: company.id })}
|
||||
disabled={isAnyLexwareMutating}
|
||||
>
|
||||
{pushToLexware.isPending ? 'Push...' : 'Push'}
|
||||
</button>
|
||||
<button
|
||||
className={styles.lexwareActionBtnDanger}
|
||||
onClick={() => unlinkCompany.mutate(company.id)}
|
||||
disabled={isAnyLexwareMutating}
|
||||
>
|
||||
{unlinkCompany.isPending ? 'Trennen...' : 'Trennen'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Kontakte + Beziehungen */}
|
||||
<div className={styles.rightStack}>
|
||||
{/* Kontakte */}
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardTitleRow}>
|
||||
<h2 className={styles.cardTitle} style={{ marginBottom: 0 }}>
|
||||
Kontakte ({company._count?.contacts ?? contacts.length})
|
||||
</h2>
|
||||
</div>
|
||||
{contacts.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.8125rem' }}>
|
||||
Keine Kontakte vorhanden
|
||||
</p>
|
||||
) : (
|
||||
<table className={styles.compactTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Position</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{contacts.map((c) => {
|
||||
const cName = [c.firstName, c.lastName]
|
||||
.filter(Boolean)
|
||||
.join(' ') || '—';
|
||||
return (
|
||||
<tr
|
||||
key={c.id}
|
||||
data-clickable="true"
|
||||
onClick={() => navigate(`/crm/contacts/${c.id}`)}
|
||||
>
|
||||
<td style={{ fontWeight: 500 }}>{cName}</td>
|
||||
<td style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{c.position ?? '—'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Beziehungen */}
|
||||
<CompanyRelationshipsCard companyId={company.id} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Beziehungen */}
|
||||
<CompanyRelationshipsCard companyId={company.id} />
|
||||
{/* ---- Tab 2: Aktivitäten ---- */}
|
||||
{activeTab === 'activities' && (
|
||||
<div style={{ maxWidth: 800 }}>
|
||||
<ActivityFeed companyId={company.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Verträge (Platzhalter) */}
|
||||
{/* ---- Tab 3: Vorgänge ---- */}
|
||||
{activeTab === 'deals' && (
|
||||
<VorgaengeTab
|
||||
deals={deals}
|
||||
vouchers={vouchers}
|
||||
isLoadingVouchers={vouchersQuery.isLoading}
|
||||
isLinked={isLinked}
|
||||
lexwareEnabled={lexwareEnabled}
|
||||
companyId={company.id}
|
||||
refreshVouchers={refreshVouchers}
|
||||
sourceFilter={sourceFilter}
|
||||
setSourceFilter={setSourceFilter}
|
||||
typeFilter={typeFilter}
|
||||
setTypeFilter={setTypeFilter}
|
||||
navigate={navigate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ---- Tab 4: Verträge ---- */}
|
||||
{activeTab === 'contracts' && (
|
||||
<ContractsCard
|
||||
companyId={company.id}
|
||||
contractCount={company._count?.contracts ?? 0}
|
||||
/>
|
||||
|
||||
{/* Lexware Office */}
|
||||
<LexwareSection
|
||||
entityType="company"
|
||||
entityId={company.id}
|
||||
lexwareContactId={company.lexwareContactId ?? null}
|
||||
lexwareSyncedAt={company.lexwareSyncedAt ?? null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{/* ======== Modals ======== */}
|
||||
<CompanyFormModal
|
||||
isOpen={isEditOpen}
|
||||
onClose={() => setEditOpen(false)}
|
||||
|
|
@ -402,6 +494,18 @@ export function CompanyDetailPage() {
|
|||
onSuccess={() => setEditOpen(false)}
|
||||
/>
|
||||
|
||||
<LexwareSearchModal
|
||||
isOpen={isLexwareSearchOpen}
|
||||
onClose={() => setLexwareSearchOpen(false)}
|
||||
onLink={(lexwareId) => {
|
||||
linkCompany.mutate(
|
||||
{ lexwareContactId: lexwareId, companyId: company.id },
|
||||
{ onSuccess: () => setLexwareSearchOpen(false) },
|
||||
);
|
||||
}}
|
||||
isLinking={linkCompany.isPending}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
|
|
@ -475,3 +579,263 @@ export function CompanyDetailPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// VorgaengeTab — CRM Deals + Lexware Belege kombiniert
|
||||
// ============================================================
|
||||
|
||||
function VorgaengeTab({
|
||||
deals,
|
||||
vouchers,
|
||||
isLoadingVouchers,
|
||||
isLinked,
|
||||
lexwareEnabled,
|
||||
companyId,
|
||||
refreshVouchers,
|
||||
sourceFilter,
|
||||
setSourceFilter,
|
||||
typeFilter,
|
||||
setTypeFilter,
|
||||
navigate,
|
||||
}: {
|
||||
deals: Deal[];
|
||||
vouchers: LexwareVoucher[];
|
||||
isLoadingVouchers: boolean;
|
||||
isLinked: boolean;
|
||||
lexwareEnabled: boolean;
|
||||
companyId: string;
|
||||
refreshVouchers: ReturnType<typeof useRefreshCompanyVouchers>;
|
||||
sourceFilter: SourceFilter;
|
||||
setSourceFilter: (v: SourceFilter) => void;
|
||||
typeFilter: string;
|
||||
setTypeFilter: (v: string) => void;
|
||||
navigate: ReturnType<typeof useNavigate>;
|
||||
}) {
|
||||
// Map CRM deals to unified format
|
||||
const crmItems: UnifiedItem[] = useMemo(
|
||||
() =>
|
||||
deals.map((deal) => ({
|
||||
id: `crm-${deal.id}`,
|
||||
source: 'CRM' as const,
|
||||
title: deal.title,
|
||||
type: deal.stage?.name ?? deal.pipeline?.name ?? '—',
|
||||
date: deal.createdAt,
|
||||
status: STATUS_LABELS[deal.status] ?? deal.status,
|
||||
statusColor: STATUS_COLORS[deal.status],
|
||||
amount: deal.value ? parseFloat(deal.value) : null,
|
||||
currency: deal.currency ?? 'EUR',
|
||||
link: `/crm/deals/${deal.id}`,
|
||||
stageColor: deal.stage?.color,
|
||||
})),
|
||||
[deals],
|
||||
);
|
||||
|
||||
// Map Lexware vouchers to unified format
|
||||
const lexwareItems: UnifiedItem[] = useMemo(
|
||||
() =>
|
||||
vouchers.map((v) => ({
|
||||
id: `lx-${v.id}`,
|
||||
source: 'LEXWARE' as const,
|
||||
title: v.title || v.voucherNumber || '—',
|
||||
type: VOUCHER_TYPE_LABELS[v.voucherType] ?? v.voucherType,
|
||||
date: v.voucherDate,
|
||||
status: v.voucherStatus ?? '—',
|
||||
amount: v.totalGrossAmount ? parseFloat(v.totalGrossAmount) : null,
|
||||
currency: v.currency ?? 'EUR',
|
||||
link: v.lexwareDeepLink ?? '#',
|
||||
openExternal: true,
|
||||
})),
|
||||
[vouchers],
|
||||
);
|
||||
|
||||
// Merge + sort by date descending
|
||||
const allItems = useMemo(() => {
|
||||
let items = [...crmItems, ...lexwareItems];
|
||||
|
||||
// Source filter
|
||||
if (sourceFilter === 'CRM') items = items.filter((i) => i.source === 'CRM');
|
||||
if (sourceFilter === 'LEXWARE') items = items.filter((i) => i.source === 'LEXWARE');
|
||||
|
||||
// Type filter
|
||||
if (typeFilter) items = items.filter((i) => i.type === typeFilter);
|
||||
|
||||
// Sort by date (newest first)
|
||||
items.sort((a, b) => {
|
||||
if (!a.date && !b.date) return 0;
|
||||
if (!a.date) return 1;
|
||||
if (!b.date) return -1;
|
||||
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [crmItems, lexwareItems, sourceFilter, typeFilter]);
|
||||
|
||||
// Collect all unique types for filter dropdown
|
||||
const allTypes = useMemo(() => {
|
||||
const types = new Set<string>();
|
||||
[...crmItems, ...lexwareItems].forEach((i) => {
|
||||
if (i.type && i.type !== '—') types.add(i.type);
|
||||
});
|
||||
return Array.from(types).sort();
|
||||
}, [crmItems, lexwareItems]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Filter + Actions */}
|
||||
<div className={styles.vorgaengeHeader}>
|
||||
<div className={styles.filterRow}>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={sourceFilter}
|
||||
onChange={(e) => setSourceFilter(e.target.value as SourceFilter)}
|
||||
>
|
||||
<option value="ALL">Alle Quellen</option>
|
||||
<option value="CRM">Nur CRM</option>
|
||||
<option value="LEXWARE">Nur Lexware</option>
|
||||
</select>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
{allTypes.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{isLinked && lexwareEnabled && (
|
||||
<button
|
||||
className={styles.refreshBtn}
|
||||
onClick={() => refreshVouchers.mutate(companyId)}
|
||||
disabled={refreshVouchers.isPending}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M1 1v5h5M15 15v-5h-5" />
|
||||
<path d="M13.5 6A6 6 0 003.3 3.3L1 6M2.5 10a6 6 0 0010.2 2.7L15 10" />
|
||||
</svg>
|
||||
{refreshVouchers.isPending ? 'Aktualisieren...' : 'Belege aktualisieren'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Loading */}
|
||||
{isLoadingVouchers && isLinked && (
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.8125rem', marginBottom: '1rem' }}>
|
||||
Lexware Belege werden geladen...
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Tabelle */}
|
||||
<div className={styles.vorgaengeTableCard}>
|
||||
<table className={styles.vorgaengeTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 60 }}>Quelle</th>
|
||||
<th>Bezeichnung</th>
|
||||
<th>Typ</th>
|
||||
<th>Datum</th>
|
||||
<th>Status</th>
|
||||
<th style={{ textAlign: 'right' }}>Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allItems.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className={styles.emptyRow}>
|
||||
Keine Vorgänge oder Belege vorhanden
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{allItems.map((item) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={item.source === 'LEXWARE' ? styles.lexwareRow : undefined}
|
||||
onClick={() => {
|
||||
if (item.openExternal) {
|
||||
window.open(item.link, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
navigate(item.link);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Quelle */}
|
||||
<td>
|
||||
<span
|
||||
className={`${styles.sourceBadge} ${
|
||||
item.source === 'LEXWARE'
|
||||
? styles.sourceBadgeLexware
|
||||
: styles.sourceBadgeCrm
|
||||
}`}
|
||||
>
|
||||
{item.source === 'LEXWARE' ? 'LX' : 'CRM'}
|
||||
</span>
|
||||
</td>
|
||||
{/* Bezeichnung */}
|
||||
<td style={{ fontWeight: 500, color: 'var(--color-text)' }}>
|
||||
{item.title}
|
||||
</td>
|
||||
{/* Typ */}
|
||||
<td>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
{item.stageColor && (
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: item.stageColor,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type}
|
||||
</span>
|
||||
</td>
|
||||
{/* Datum */}
|
||||
<td>{item.date ? formatDate(item.date) : '—'}</td>
|
||||
{/* Status */}
|
||||
<td>
|
||||
{item.statusColor ? (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 0.375rem',
|
||||
borderRadius: '9999px',
|
||||
fontSize: '0.6875rem',
|
||||
fontWeight: 500,
|
||||
background: item.statusColor.bg,
|
||||
color: item.statusColor.color,
|
||||
}}
|
||||
>
|
||||
{item.status}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: '0.8125rem' }}>{item.status}</span>
|
||||
)}
|
||||
</td>
|
||||
{/* Betrag */}
|
||||
<td style={{ textAlign: 'right', fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||
{item.amount != null
|
||||
? currencyFormatter.format(item.amount)
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue