mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 01:36:39 +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 {
|
.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;
|
display: grid;
|
||||||
grid-template-columns: 300px 1fr 360px;
|
grid-template-columns: 2fr 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 900px) {
|
||||||
.layout {
|
.overviewGrid {
|
||||||
grid-template-columns: 1fr 360px;
|
grid-template-columns: 1fr;
|
||||||
}
|
|
||||||
.layout > :first-child {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.rightStack {
|
||||||
.layout {
|
display: flex;
|
||||||
grid-template-columns: 1fr;
|
flex-direction: column;
|
||||||
}
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|
@ -105,12 +139,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Info Grid (linke Spalte)
|
Info Grid (Stammdaten)
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
.infoGrid {
|
.infoGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100px 1fr;
|
grid-template-columns: 120px 1fr;
|
||||||
gap: 0.5rem 0.75rem;
|
gap: 0.5rem 0.75rem;
|
||||||
font-size: 0.8125rem;
|
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 {
|
.feedContainer {
|
||||||
|
|
@ -344,7 +671,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Relationships Card (rechte Spalte)
|
Relationships Card
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
.relationItem {
|
.relationItem {
|
||||||
|
|
@ -449,40 +776,3 @@
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
color: white;
|
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 { 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 { CompanyFormModal } from './CompanyFormModal';
|
||||||
import { ActivityFeed } from './ActivityFeed';
|
import { ActivityFeed } from './ActivityFeed';
|
||||||
import { CompanyRelationshipsCard } from './CompanyRelationshipsCard';
|
import { CompanyRelationshipsCard } from './CompanyRelationshipsCard';
|
||||||
import { ContractsCard } from './ContractsCard';
|
import { ContractsCard } from './ContractsCard';
|
||||||
|
import { LexwareSearchModal } from '../lexware/LexwareSearchModal';
|
||||||
import { Modal } from '../../components/Modal';
|
import { Modal } from '../../components/Modal';
|
||||||
import { LexwareSection } from '../lexware/LexwareSection';
|
import type { DealStatus, LexwareVoucher, Deal } from '../types';
|
||||||
import type { DealStatus } from '../types';
|
import { VOUCHER_TYPE_LABELS } from '../types';
|
||||||
import styles from './CompanyDetailPage.module.css';
|
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 }> = {
|
const STATUS_COLORS: Record<DealStatus, { bg: string; color: string }> = {
|
||||||
OPEN: { bg: '#dbeafe', color: '#1e40af' },
|
OPEN: { bg: '#dbeafe', color: '#1e40af' },
|
||||||
WON: { bg: '#d1fae5', color: '#065f46' },
|
WON: { bg: '#d1fae5', color: '#065f46' },
|
||||||
|
|
@ -46,12 +86,38 @@ function formatDate(iso: string): string {
|
||||||
export function CompanyDetailPage() {
|
export function CompanyDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { isModuleEnabled } = useCrmSettings();
|
||||||
|
const lexwareEnabled = isModuleEnabled('lexware');
|
||||||
|
|
||||||
|
// ---- Core data ----
|
||||||
const { data, isLoading, error } = useCompany(id!);
|
const { data, isLoading, error } = useCompany(id!);
|
||||||
const deleteMutation = useDeleteCompany();
|
const deleteMutation = useDeleteCompany();
|
||||||
|
|
||||||
|
// ---- UI State ----
|
||||||
|
const [activeTab, setActiveTab] = useState<DetailTab>('company');
|
||||||
const [isEditOpen, setEditOpen] = useState(false);
|
const [isEditOpen, setEditOpen] = useState(false);
|
||||||
const [isDeleteOpen, setDeleteOpen] = 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 (isLoading) return <p>Laden...</p>;
|
||||||
if (error || !data)
|
if (error || !data)
|
||||||
return (
|
return (
|
||||||
|
|
@ -63,11 +129,20 @@ export function CompanyDetailPage() {
|
||||||
const company = data.data;
|
const company = data.data;
|
||||||
const contacts = company.contacts ?? [];
|
const contacts = company.contacts ?? [];
|
||||||
const deals = company.deals ?? [];
|
const deals = company.deals ?? [];
|
||||||
|
const vouchers: LexwareVoucher[] = vouchersQuery.data?.data ?? [];
|
||||||
|
|
||||||
// Industry badge color
|
// Industry badge
|
||||||
const industryColor = company.industryRef?.color ?? '#6366f1';
|
const industryColor = company.industryRef?.color ?? '#6366f1';
|
||||||
const industryName = company.industryRef?.name ?? company.industry;
|
const industryName = company.industryRef?.name ?? company.industry;
|
||||||
|
|
||||||
|
// Lexware mutation state
|
||||||
|
const isAnyLexwareMutating =
|
||||||
|
linkCompany.isPending ||
|
||||||
|
unlinkCompany.isPending ||
|
||||||
|
syncFromLexware.isPending ||
|
||||||
|
pushToLexware.isPending ||
|
||||||
|
refreshVouchers.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Zurück */}
|
{/* Zurück */}
|
||||||
|
|
@ -142,259 +217,276 @@ export function CompanyDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3-Spalten Layout */}
|
{/* ======== Tab Bar ======== */}
|
||||||
<div className={styles.layout}>
|
<div className={styles.tabBar}>
|
||||||
{/* ======== Linke Spalte: Stammdaten ======== */}
|
{(Object.keys(TAB_LABELS) as DetailTab[]).map((tab) => (
|
||||||
<div>
|
<button
|
||||||
<div className={styles.card}>
|
key={tab}
|
||||||
<h2 className={styles.cardTitle}>Unternehmensdaten</h2>
|
className={`${styles.tab} ${activeTab === tab ? styles.tabActive : ''}`}
|
||||||
<div className={styles.infoGrid}>
|
onClick={() => setActiveTab(tab)}
|
||||||
{company.accountType && (
|
>
|
||||||
<>
|
{TAB_LABELS[tab]}
|
||||||
<span className={styles.infoLabel}>Kontotyp</span>
|
</button>
|
||||||
<span className={styles.infoValue}>{company.accountType.name}</span>
|
))}
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
{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>
|
|
||||||
|
|
||||||
{/* Tags */}
|
{/* ======== Tab Content ======== */}
|
||||||
{company.tags && company.tags.length > 0 && (
|
<div className={styles.tabContent}>
|
||||||
<div style={{ marginTop: '1rem' }}>
|
{/* ---- Tab 1: Unternehmensdaten ---- */}
|
||||||
<span
|
{activeTab === 'company' && (
|
||||||
className={styles.infoLabel}
|
<div className={styles.overviewGrid}>
|
||||||
style={{ display: 'block', marginBottom: '0.375rem' }}
|
{/* Left: Stammdaten */}
|
||||||
>
|
<div>
|
||||||
Tags
|
<div className={styles.card}>
|
||||||
</span>
|
<h2 className={styles.cardTitle}>Unternehmensdaten</h2>
|
||||||
<div className={styles.tags}>
|
<div className={styles.infoGrid}>
|
||||||
{company.tags.map((tag) => (
|
{company.accountType && (
|
||||||
<span key={tag} className={styles.tag}>
|
<>
|
||||||
{tag}
|
<span className={styles.infoLabel}>Kontotyp</span>
|
||||||
</span>
|
<span className={styles.infoValue}>{company.accountType.name}</span>
|
||||||
))}
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
{company.ownerName && (
|
||||||
)}
|
<>
|
||||||
|
<span className={styles.infoLabel}>Zuständig</span>
|
||||||
{/* Notizen */}
|
<span className={styles.infoValue}>{company.ownerName}</span>
|
||||||
{company.notes && (
|
</>
|
||||||
<div className={styles.notesSection}>
|
)}
|
||||||
<span
|
{company.email && (
|
||||||
className={styles.infoLabel}
|
<>
|
||||||
style={{ display: 'block', marginBottom: '0.375rem' }}
|
<span className={styles.infoLabel}>E-Mail</span>
|
||||||
>
|
<span className={styles.infoValue}>
|
||||||
Notizen
|
<a
|
||||||
</span>
|
href={`mailto:${company.email}`}
|
||||||
<p className={styles.notesText}>{company.notes}</p>
|
style={{ color: 'var(--color-primary)' }}
|
||||||
</div>
|
>
|
||||||
)}
|
{company.email}
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</span>
|
||||||
|
</>
|
||||||
{/* ======== Mittlere Spalte: Activity Feed ======== */}
|
)}
|
||||||
<div>
|
{company.phone && (
|
||||||
<ActivityFeed companyId={company.id} />
|
<>
|
||||||
</div>
|
<span className={styles.infoLabel}>Telefon</span>
|
||||||
|
<span className={styles.infoValue}>{company.phone}</span>
|
||||||
{/* ======== Rechte Spalte: Relations ======== */}
|
</>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
)}
|
||||||
{/* Kontakte */}
|
{company.website && (
|
||||||
<div className={styles.card}>
|
<>
|
||||||
<div className={styles.cardTitleRow}>
|
<span className={styles.infoLabel}>Website</span>
|
||||||
<h2 className={styles.cardTitle} style={{ marginBottom: 0 }}>
|
<span className={styles.infoValue}>
|
||||||
Kontakte ({company._count?.contacts ?? contacts.length})
|
<a
|
||||||
</h2>
|
href={company.website}
|
||||||
</div>
|
target="_blank"
|
||||||
{contacts.length === 0 ? (
|
rel="noopener noreferrer"
|
||||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.8125rem' }}>
|
style={{ color: 'var(--color-primary)' }}
|
||||||
Keine Kontakte vorhanden
|
>
|
||||||
</p>
|
{company.website}
|
||||||
) : (
|
</a>
|
||||||
<table className={styles.compactTable}>
|
</span>
|
||||||
<thead>
|
</>
|
||||||
<tr>
|
)}
|
||||||
<th>Name</th>
|
{(company.street || company.zip || company.city) && (
|
||||||
<th>Position</th>
|
<>
|
||||||
</tr>
|
<span className={styles.infoLabel}>Adresse</span>
|
||||||
</thead>
|
<span className={styles.infoValue}>
|
||||||
<tbody>
|
{company.street && <>{company.street}<br /></>}
|
||||||
{contacts.map((c) => {
|
{company.zip} {company.city}
|
||||||
const name = [c.firstName, c.lastName]
|
{company.country && company.country !== 'DE' && (
|
||||||
.filter(Boolean)
|
<>, {company.country}</>
|
||||||
.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>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</span>
|
||||||
<td style={{ textAlign: 'right', fontWeight: 500, whiteSpace: 'nowrap' }}>
|
</>
|
||||||
{deal.value
|
)}
|
||||||
? currencyFormatter.format(parseFloat(deal.value))
|
<span className={styles.infoLabel}>Erstellt</span>
|
||||||
: '—'}
|
<span className={styles.infoValue}>
|
||||||
</td>
|
{formatDate(company.createdAt)}
|
||||||
</tr>
|
</span>
|
||||||
))}
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
{/* 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>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Beziehungen */}
|
{/* ---- Tab 2: Aktivitäten ---- */}
|
||||||
<CompanyRelationshipsCard companyId={company.id} />
|
{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
|
<ContractsCard
|
||||||
companyId={company.id}
|
companyId={company.id}
|
||||||
contractCount={company._count?.contracts ?? 0}
|
contractCount={company._count?.contracts ?? 0}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{/* Lexware Office */}
|
|
||||||
<LexwareSection
|
|
||||||
entityType="company"
|
|
||||||
entityId={company.id}
|
|
||||||
lexwareContactId={company.lexwareContactId ?? null}
|
|
||||||
lexwareSyncedAt={company.lexwareSyncedAt ?? null}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* ======== Modals ======== */}
|
||||||
<CompanyFormModal
|
<CompanyFormModal
|
||||||
isOpen={isEditOpen}
|
isOpen={isEditOpen}
|
||||||
onClose={() => setEditOpen(false)}
|
onClose={() => setEditOpen(false)}
|
||||||
|
|
@ -402,6 +494,18 @@ export function CompanyDetailPage() {
|
||||||
onSuccess={() => setEditOpen(false)}
|
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
|
<Modal
|
||||||
isOpen={isDeleteOpen}
|
isOpen={isDeleteOpen}
|
||||||
onClose={() => setDeleteOpen(false)}
|
onClose={() => setDeleteOpen(false)}
|
||||||
|
|
@ -475,3 +579,263 @@ export function CompanyDetailPage() {
|
||||||
</div>
|
</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