mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 01:36:39 +02:00
feat(frontend): redesign Lexware Import with browsable list + Ansprechpartner
- Replace search-only ImportTab with paginated browsable list of all Lexware contacts - Add expandable Ansprechpartner (contact persons) section per company - Individual Ansprechpartner can be imported as CRM contacts - Add pagination controls for navigating large contact lists - Search filter still available (min. 3 chars, backend MinLength constraint) - Clean up useLexwareContacts hook with proper query key Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4f05026bc8
commit
55329188f6
4 changed files with 469 additions and 85 deletions
|
|
@ -69,6 +69,8 @@ export const crmKeys = {
|
||||||
all: ['crm', 'lexware'] as const,
|
all: ['crm', 'lexware'] as const,
|
||||||
contactSearch: (params: LexwareContactSearchParams) =>
|
contactSearch: (params: LexwareContactSearchParams) =>
|
||||||
['crm', 'lexware', 'contacts', 'search', params] as const,
|
['crm', 'lexware', 'contacts', 'search', params] as const,
|
||||||
|
contactList: (params: LexwareContactSearchParams) =>
|
||||||
|
['crm', 'lexware', 'contacts', 'list', params] as const,
|
||||||
vouchersCompany: (companyId: string, params?: LexwareVouchersQueryParams) =>
|
vouchersCompany: (companyId: string, params?: LexwareVouchersQueryParams) =>
|
||||||
['crm', 'lexware', 'vouchers', 'company', companyId, params] as const,
|
['crm', 'lexware', 'vouchers', 'company', companyId, params] as const,
|
||||||
vouchersContact: (contactId: string, params?: LexwareVouchersQueryParams) =>
|
vouchersContact: (contactId: string, params?: LexwareVouchersQueryParams) =>
|
||||||
|
|
@ -405,6 +407,18 @@ export function useLexwareContactSearch(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch Lexware contacts (paginated, no search filter required) */
|
||||||
|
export function useLexwareContacts(
|
||||||
|
params: LexwareContactSearchParams = {},
|
||||||
|
enabled = true,
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: crmKeys.lexware.contactList(params),
|
||||||
|
queryFn: () => lexwareContactsApi.search(params),
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useLinkLexwareCompany() {
|
export function useLinkLexwareCompany() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,10 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Result card */
|
/* Result card — column layout for expandable Ansprechpartner */
|
||||||
.resultCard {
|
.resultCard {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.875rem 0;
|
padding: 0.875rem 0;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +139,13 @@
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resultCardMain {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.resultInfo {
|
.resultInfo {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -436,3 +441,141 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Expand button for Ansprechpartner */
|
||||||
|
.expandBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandBtn:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandBtnActive {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: var(--color-primary-bg, #eff6ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ansprechpartner (Contact Persons) section */
|
||||||
|
.cpSection {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpRow:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpInfo {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpName {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpMeta {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpPrimary {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 0.375rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global([data-theme='dark']) .cpPrimary {
|
||||||
|
background: #1e3a5f;
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem 0 0;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.5rem 0.875rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn:hover:not(:disabled) {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageInfo {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link, Navigate } from 'react-router-dom';
|
import { Link, Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../auth/AuthContext';
|
import { useAuth } from '../../auth/AuthContext';
|
||||||
import { useCrmSettings } from '../settings/CrmSettingsContext';
|
import { useCrmSettings } from '../settings/CrmSettingsContext';
|
||||||
import {
|
import {
|
||||||
useLexwareContactSearch,
|
useLexwareContacts,
|
||||||
useImportLexwareAsCompany,
|
useImportLexwareAsCompany,
|
||||||
useImportLexwareAsContact,
|
useImportLexwareAsContact,
|
||||||
|
useCreateContact,
|
||||||
useCompanies,
|
useCompanies,
|
||||||
useContacts,
|
useContacts,
|
||||||
usePushToLexware,
|
usePushToLexware,
|
||||||
} from '../hooks';
|
} from '../hooks';
|
||||||
import type { LexwareContact, Company, Contact } from '../types';
|
import type {
|
||||||
|
LexwareContact,
|
||||||
|
LexwareContactPersonEntry,
|
||||||
|
LexwareContactSearchParams,
|
||||||
|
Company,
|
||||||
|
Contact,
|
||||||
|
} from '../types';
|
||||||
import styles from './LexwareSyncPage.module.css';
|
import styles from './LexwareSyncPage.module.css';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -59,31 +66,39 @@ function lexwareRoles(c: LexwareContact): string {
|
||||||
function ImportTab() {
|
function ImportTab() {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [debouncedTerm, setDebouncedTerm] = useState('');
|
const [debouncedTerm, setDebouncedTerm] = useState('');
|
||||||
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||||
const [successMessage, setSuccessMessage] = useState('');
|
const [successMessage, setSuccessMessage] = useState('');
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const importAsCompany = useImportLexwareAsCompany();
|
const importAsCompany = useImportLexwareAsCompany();
|
||||||
const importAsContact = useImportLexwareAsContact();
|
const importAsContact = useImportLexwareAsContact();
|
||||||
|
const createContact = useCreateContact();
|
||||||
|
|
||||||
useEffect(() => {
|
// Debounce search
|
||||||
setTimeout(() => inputRef.current?.focus(), 100);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Debounce
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setDebouncedTerm(searchTerm.trim());
|
setDebouncedTerm(searchTerm.trim());
|
||||||
|
setCurrentPage(0);
|
||||||
}, 400);
|
}, 400);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
const { data, isLoading } = useLexwareContactSearch(
|
// Build params: name only when >= 3 chars (backend MinLength(3))
|
||||||
{ name: debouncedTerm },
|
const searchParams: LexwareContactSearchParams = {
|
||||||
debouncedTerm.length >= 2,
|
page: currentPage,
|
||||||
);
|
size: 25,
|
||||||
|
...(debouncedTerm.length >= 3 ? { name: debouncedTerm } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading } = useLexwareContacts(searchParams);
|
||||||
const results: LexwareContact[] = data?.data ?? [];
|
const results: LexwareContact[] = data?.data ?? [];
|
||||||
const isImporting = importAsCompany.isPending || importAsContact.isPending;
|
const pagination = data?.pagination;
|
||||||
|
const totalPages = pagination?.totalPages ?? 1;
|
||||||
|
|
||||||
|
const isImporting =
|
||||||
|
importAsCompany.isPending ||
|
||||||
|
importAsContact.isPending ||
|
||||||
|
createContact.isPending;
|
||||||
|
|
||||||
const handleImportAsCompany = (lexwareContactId: string, name: string) => {
|
const handleImportAsCompany = (lexwareContactId: string, name: string) => {
|
||||||
importAsCompany.mutate(
|
importAsCompany.mutate(
|
||||||
|
|
@ -109,18 +124,54 @@ function ImportTab() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportContactPerson = (
|
||||||
|
person: LexwareContactPersonEntry,
|
||||||
|
companyName: string,
|
||||||
|
) => {
|
||||||
|
const cpName =
|
||||||
|
[person.firstName, person.lastName].filter(Boolean).join(' ') ||
|
||||||
|
'Ansprechpartner';
|
||||||
|
createContact.mutate(
|
||||||
|
{
|
||||||
|
firstName: person.firstName || undefined,
|
||||||
|
lastName: person.lastName || undefined,
|
||||||
|
email: person.emailAddress || undefined,
|
||||||
|
phone: person.phoneNumber || undefined,
|
||||||
|
companyName: companyName || undefined,
|
||||||
|
type: 'PERSON',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setSuccessMessage(
|
||||||
|
`Ansprechpartner "${cpName}" als Kontakt importiert`,
|
||||||
|
);
|
||||||
|
setTimeout(() => setSuccessMessage(''), 4000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExpanded = (id: string) => {
|
||||||
|
setExpandedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className={styles.tabDesc}>
|
<p className={styles.tabDesc}>
|
||||||
Lexware Office Kontakte durchsuchen und als Unternehmen oder Kontakt in
|
Alle Lexware Office Kontakte durchsuchen und als Unternehmen oder
|
||||||
das CRM importieren.
|
Kontakt ins CRM importieren. Ansprechpartner können einzeln importiert
|
||||||
|
werden.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
type="text"
|
||||||
className={styles.searchInput}
|
className={styles.searchInput}
|
||||||
placeholder="Name oder Firma in Lexware suchen..."
|
placeholder="Kontakte filtern (min. 3 Zeichen)..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -143,25 +194,30 @@ function ImportTab() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(importAsCompany.isError || importAsContact.isError) && (
|
{(importAsCompany.isError ||
|
||||||
|
importAsContact.isError ||
|
||||||
|
createContact.isError) && (
|
||||||
<div className={styles.errorBanner}>
|
<div className={styles.errorBanner}>
|
||||||
Import fehlgeschlagen:{' '}
|
Import fehlgeschlagen:{' '}
|
||||||
{(importAsCompany.error ?? importAsContact.error)?.message ??
|
{(
|
||||||
'Unbekannter Fehler'}
|
importAsCompany.error ??
|
||||||
|
importAsContact.error ??
|
||||||
|
createContact.error
|
||||||
|
)?.message ?? 'Unbekannter Fehler'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.resultsList}>
|
<div className={styles.resultsList}>
|
||||||
{debouncedTerm.length < 2 && (
|
{isLoading && (
|
||||||
<p className={styles.hint}>Mindestens 2 Zeichen eingeben...</p>
|
<p className={styles.hint}>Lade Lexware Office Kontakte...</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{debouncedTerm.length >= 2 && isLoading && (
|
{!isLoading && results.length === 0 && (
|
||||||
<p className={styles.hint}>Suche in Lexware Office...</p>
|
<p className={styles.hint}>
|
||||||
)}
|
{debouncedTerm.length >= 3
|
||||||
|
? 'Keine Ergebnisse für diese Suche'
|
||||||
{debouncedTerm.length >= 2 && !isLoading && results.length === 0 && (
|
: 'Keine Kontakte in Lexware Office gefunden'}
|
||||||
<p className={styles.hint}>Keine Ergebnisse gefunden</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{results.map((contact) => {
|
{results.map((contact) => {
|
||||||
|
|
@ -169,67 +225,220 @@ function ImportTab() {
|
||||||
const email = lexwareEmail(contact);
|
const email = lexwareEmail(contact);
|
||||||
const address = lexwareAddress(contact);
|
const address = lexwareAddress(contact);
|
||||||
const roles = lexwareRoles(contact);
|
const roles = lexwareRoles(contact);
|
||||||
|
const contactPersons = contact.company?.contactPersons ?? [];
|
||||||
|
const isExpanded = expandedIds.has(contact.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={contact.id} className={styles.resultCard}>
|
<div key={contact.id} className={styles.resultCard}>
|
||||||
<div className={styles.resultInfo}>
|
{/* Main row */}
|
||||||
<div className={styles.resultName}>{name}</div>
|
<div className={styles.resultCardMain}>
|
||||||
<div className={styles.resultMeta}>
|
<div className={styles.resultInfo}>
|
||||||
{roles !== '—' && (
|
<div className={styles.resultName}>{name}</div>
|
||||||
<span className={styles.roleBadge}>{roles}</span>
|
<div className={styles.resultMeta}>
|
||||||
|
{roles !== '—' && (
|
||||||
|
<span className={styles.roleBadge}>{roles}</span>
|
||||||
|
)}
|
||||||
|
{email && <span>{email}</span>}
|
||||||
|
{address && <span>{address}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.resultActions}>
|
||||||
|
{contactPersons.length > 0 && (
|
||||||
|
<button
|
||||||
|
className={`${styles.expandBtn} ${isExpanded ? styles.expandBtnActive : ''}`}
|
||||||
|
onClick={() => toggleExpanded(contact.id)}
|
||||||
|
title={
|
||||||
|
isExpanded
|
||||||
|
? 'Ansprechpartner ausblenden'
|
||||||
|
: 'Ansprechpartner anzeigen'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{
|
||||||
|
transform: isExpanded
|
||||||
|
? 'rotate(180deg)'
|
||||||
|
: 'none',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path d="M4 6l4 4 4-4" />
|
||||||
|
</svg>
|
||||||
|
{contactPersons.length} AP
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
{email && <span>{email}</span>}
|
<button
|
||||||
{address && <span>{address}</span>}
|
className={styles.importBtn}
|
||||||
|
disabled={isImporting}
|
||||||
|
onClick={() => handleImportAsCompany(contact.id, name)}
|
||||||
|
title="Als Unternehmen importieren"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="2" y="6" width="12" height="9" rx="1" />
|
||||||
|
<path d="M5 6V3a1 1 0 011-1h4a1 1 0 011 1v3" />
|
||||||
|
</svg>
|
||||||
|
Unternehmen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.importBtn}
|
||||||
|
disabled={isImporting}
|
||||||
|
onClick={() => handleImportAsContact(contact.id, name)}
|
||||||
|
title="Als Kontakt importieren"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="5" r="3" />
|
||||||
|
<path d="M2 14c0-2.5 2.5-4.5 6-4.5s6 2 6 4.5" />
|
||||||
|
</svg>
|
||||||
|
Kontakt
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.resultActions}>
|
|
||||||
<button
|
{/* Ansprechpartner expandable section */}
|
||||||
className={styles.importBtn}
|
{isExpanded && contactPersons.length > 0 && (
|
||||||
disabled={isImporting}
|
<div className={styles.cpSection}>
|
||||||
onClick={() => handleImportAsCompany(contact.id, name)}
|
<div className={styles.cpTitle}>
|
||||||
title="Als Unternehmen importieren"
|
<svg
|
||||||
>
|
width="14"
|
||||||
<svg
|
height="14"
|
||||||
width="14"
|
viewBox="0 0 16 16"
|
||||||
height="14"
|
fill="none"
|
||||||
viewBox="0 0 16 16"
|
stroke="currentColor"
|
||||||
fill="none"
|
strokeWidth="1.5"
|
||||||
stroke="currentColor"
|
strokeLinecap="round"
|
||||||
strokeWidth="1.5"
|
strokeLinejoin="round"
|
||||||
strokeLinecap="round"
|
>
|
||||||
strokeLinejoin="round"
|
<circle cx="6" cy="5" r="2.5" />
|
||||||
>
|
<path d="M1 13c0-2 2-3.5 5-3.5" />
|
||||||
<rect x="2" y="6" width="12" height="9" rx="1" />
|
<circle cx="12" cy="6" r="2" />
|
||||||
<path d="M5 6V3a1 1 0 011-1h4a1 1 0 011 1v3" />
|
<path d="M9 13c0-1.5 1.5-2.5 3-2.5s3 1 3 2.5" />
|
||||||
</svg>
|
</svg>
|
||||||
Unternehmen
|
Ansprechpartner ({contactPersons.length})
|
||||||
</button>
|
</div>
|
||||||
<button
|
{contactPersons.map((cp, idx) => {
|
||||||
className={styles.importBtn}
|
const cpName =
|
||||||
disabled={isImporting}
|
[cp.firstName, cp.lastName]
|
||||||
onClick={() => handleImportAsContact(contact.id, name)}
|
.filter(Boolean)
|
||||||
title="Als Kontakt importieren"
|
.join(' ') || 'Unbenannt';
|
||||||
>
|
return (
|
||||||
<svg
|
<div key={idx} className={styles.cpRow}>
|
||||||
width="14"
|
<div className={styles.cpInfo}>
|
||||||
height="14"
|
<span className={styles.cpName}>
|
||||||
viewBox="0 0 16 16"
|
{cpName}
|
||||||
fill="none"
|
{cp.primary && (
|
||||||
stroke="currentColor"
|
<span className={styles.cpPrimary}>Haupt</span>
|
||||||
strokeWidth="1.5"
|
)}
|
||||||
strokeLinecap="round"
|
</span>
|
||||||
strokeLinejoin="round"
|
<span className={styles.cpMeta}>
|
||||||
>
|
{[cp.emailAddress, cp.phoneNumber]
|
||||||
<circle cx="8" cy="5" r="3" />
|
.filter(Boolean)
|
||||||
<path d="M2 14c0-2.5 2.5-4.5 6-4.5s6 2 6 4.5" />
|
.join(' · ') || 'Keine Kontaktdaten'}
|
||||||
</svg>
|
</span>
|
||||||
Kontakt
|
</div>
|
||||||
</button>
|
<button
|
||||||
</div>
|
className={styles.importBtn}
|
||||||
|
disabled={isImporting}
|
||||||
|
onClick={() =>
|
||||||
|
handleImportContactPerson(
|
||||||
|
cp,
|
||||||
|
contact.company?.name ?? '',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={`${cpName} als CRM-Kontakt importieren`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="5" r="3" />
|
||||||
|
<path d="M2 14c0-2.5 2.5-4.5 6-4.5s6 2 6 4.5" />
|
||||||
|
</svg>
|
||||||
|
Als Kontakt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{pagination && totalPages > 1 && (
|
||||||
|
<div className={styles.pagination}>
|
||||||
|
<button
|
||||||
|
className={styles.pageBtn}
|
||||||
|
disabled={currentPage === 0}
|
||||||
|
onClick={() => setCurrentPage((p) => p - 1)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
>
|
||||||
|
<path d="M9 2L4 7l5 5" />
|
||||||
|
</svg>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
<span className={styles.pageInfo}>
|
||||||
|
Seite {currentPage + 1} von {totalPages}
|
||||||
|
{pagination.total != null && ` (${pagination.total} Kontakte)`}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className={styles.pageBtn}
|
||||||
|
disabled={currentPage + 1 >= totalPages}
|
||||||
|
onClick={() => setCurrentPage((p) => p + 1)}
|
||||||
|
>
|
||||||
|
Weiter
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
>
|
||||||
|
<path d="M5 2l5 5-5 5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -359,11 +359,25 @@ export interface DealVoucher {
|
||||||
voucher: LexwareVoucher;
|
voucher: LexwareVoucher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LexwareContactPersonEntry {
|
||||||
|
salutation?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
primary?: boolean;
|
||||||
|
emailAddress?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LexwareContact {
|
export interface LexwareContact {
|
||||||
id: string;
|
id: string;
|
||||||
version: number;
|
version: number;
|
||||||
roles: { customer?: Record<string, unknown>; vendor?: Record<string, unknown> };
|
roles: { customer?: Record<string, unknown>; vendor?: Record<string, unknown> };
|
||||||
company?: { name?: string; taxId?: string; allowTaxFreeInvoices?: boolean };
|
company?: {
|
||||||
|
name?: string;
|
||||||
|
taxId?: string;
|
||||||
|
allowTaxFreeInvoices?: boolean;
|
||||||
|
contactPersons?: LexwareContactPersonEntry[];
|
||||||
|
};
|
||||||
person?: { firstName?: string; lastName?: string; salutation?: string };
|
person?: { firstName?: string; lastName?: string; salutation?: string };
|
||||||
addresses?: {
|
addresses?: {
|
||||||
billing?: { street?: string; zip?: string; city?: string; countryCode?: string }[];
|
billing?: { street?: string; zip?: string; city?: string; countryCode?: string }[];
|
||||||
|
|
@ -378,6 +392,10 @@ export interface LexwareContact {
|
||||||
export interface LexwareContactSearchParams {
|
export interface LexwareContactSearchParams {
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
customer?: boolean;
|
||||||
|
vendor?: boolean;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LexwareVouchersQueryParams {
|
export interface LexwareVouchersQueryParams {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue