mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +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,
|
||||
contactSearch: (params: LexwareContactSearchParams) =>
|
||||
['crm', 'lexware', 'contacts', 'search', params] as const,
|
||||
contactList: (params: LexwareContactSearchParams) =>
|
||||
['crm', 'lexware', 'contacts', 'list', params] as const,
|
||||
vouchersCompany: (companyId: string, params?: LexwareVouchersQueryParams) =>
|
||||
['crm', 'lexware', 'vouchers', 'company', companyId, params] as const,
|
||||
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() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
|
|
|
|||
|
|
@ -127,12 +127,10 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
/* Result card */
|
||||
/* Result card — column layout for expandable Ansprechpartner */
|
||||
.resultCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
padding: 0.875rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
|
@ -141,6 +139,13 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
.resultCardMain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.resultInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
@ -436,3 +441,141 @@
|
|||
opacity: 0.5;
|
||||
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 { useAuth } from '../../auth/AuthContext';
|
||||
import { useCrmSettings } from '../settings/CrmSettingsContext';
|
||||
import {
|
||||
useLexwareContactSearch,
|
||||
useLexwareContacts,
|
||||
useImportLexwareAsCompany,
|
||||
useImportLexwareAsContact,
|
||||
useCreateContact,
|
||||
useCompanies,
|
||||
useContacts,
|
||||
usePushToLexware,
|
||||
} from '../hooks';
|
||||
import type { LexwareContact, Company, Contact } from '../types';
|
||||
import type {
|
||||
LexwareContact,
|
||||
LexwareContactPersonEntry,
|
||||
LexwareContactSearchParams,
|
||||
Company,
|
||||
Contact,
|
||||
} from '../types';
|
||||
import styles from './LexwareSyncPage.module.css';
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -59,31 +66,39 @@ function lexwareRoles(c: LexwareContact): string {
|
|||
function ImportTab() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedTerm, setDebouncedTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const importAsCompany = useImportLexwareAsCompany();
|
||||
const importAsContact = useImportLexwareAsContact();
|
||||
const createContact = useCreateContact();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => inputRef.current?.focus(), 100);
|
||||
}, []);
|
||||
|
||||
// Debounce
|
||||
// Debounce search
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedTerm(searchTerm.trim());
|
||||
setCurrentPage(0);
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
|
||||
const { data, isLoading } = useLexwareContactSearch(
|
||||
{ name: debouncedTerm },
|
||||
debouncedTerm.length >= 2,
|
||||
);
|
||||
// Build params: name only when >= 3 chars (backend MinLength(3))
|
||||
const searchParams: LexwareContactSearchParams = {
|
||||
page: currentPage,
|
||||
size: 25,
|
||||
...(debouncedTerm.length >= 3 ? { name: debouncedTerm } : {}),
|
||||
};
|
||||
|
||||
const { data, isLoading } = useLexwareContacts(searchParams);
|
||||
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) => {
|
||||
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 (
|
||||
<div>
|
||||
<p className={styles.tabDesc}>
|
||||
Lexware Office Kontakte durchsuchen und als Unternehmen oder Kontakt in
|
||||
das CRM importieren.
|
||||
Alle Lexware Office Kontakte durchsuchen und als Unternehmen oder
|
||||
Kontakt ins CRM importieren. Ansprechpartner können einzeln importiert
|
||||
werden.
|
||||
</p>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className={styles.searchInput}
|
||||
placeholder="Name oder Firma in Lexware suchen..."
|
||||
placeholder="Kontakte filtern (min. 3 Zeichen)..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
|
|
@ -143,25 +194,30 @@ function ImportTab() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{(importAsCompany.isError || importAsContact.isError) && (
|
||||
{(importAsCompany.isError ||
|
||||
importAsContact.isError ||
|
||||
createContact.isError) && (
|
||||
<div className={styles.errorBanner}>
|
||||
Import fehlgeschlagen:{' '}
|
||||
{(importAsCompany.error ?? importAsContact.error)?.message ??
|
||||
'Unbekannter Fehler'}
|
||||
{(
|
||||
importAsCompany.error ??
|
||||
importAsContact.error ??
|
||||
createContact.error
|
||||
)?.message ?? 'Unbekannter Fehler'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.resultsList}>
|
||||
{debouncedTerm.length < 2 && (
|
||||
<p className={styles.hint}>Mindestens 2 Zeichen eingeben...</p>
|
||||
{isLoading && (
|
||||
<p className={styles.hint}>Lade Lexware Office Kontakte...</p>
|
||||
)}
|
||||
|
||||
{debouncedTerm.length >= 2 && isLoading && (
|
||||
<p className={styles.hint}>Suche in Lexware Office...</p>
|
||||
)}
|
||||
|
||||
{debouncedTerm.length >= 2 && !isLoading && results.length === 0 && (
|
||||
<p className={styles.hint}>Keine Ergebnisse gefunden</p>
|
||||
{!isLoading && results.length === 0 && (
|
||||
<p className={styles.hint}>
|
||||
{debouncedTerm.length >= 3
|
||||
? 'Keine Ergebnisse für diese Suche'
|
||||
: 'Keine Kontakte in Lexware Office gefunden'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{results.map((contact) => {
|
||||
|
|
@ -169,67 +225,220 @@ function ImportTab() {
|
|||
const email = lexwareEmail(contact);
|
||||
const address = lexwareAddress(contact);
|
||||
const roles = lexwareRoles(contact);
|
||||
const contactPersons = contact.company?.contactPersons ?? [];
|
||||
const isExpanded = expandedIds.has(contact.id);
|
||||
|
||||
return (
|
||||
<div key={contact.id} className={styles.resultCard}>
|
||||
<div className={styles.resultInfo}>
|
||||
<div className={styles.resultName}>{name}</div>
|
||||
<div className={styles.resultMeta}>
|
||||
{roles !== '—' && (
|
||||
<span className={styles.roleBadge}>{roles}</span>
|
||||
{/* Main row */}
|
||||
<div className={styles.resultCardMain}>
|
||||
<div className={styles.resultInfo}>
|
||||
<div className={styles.resultName}>{name}</div>
|
||||
<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>}
|
||||
{address && <span>{address}</span>}
|
||||
<button
|
||||
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 className={styles.resultActions}>
|
||||
<button
|
||||
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>
|
||||
|
||||
{/* Ansprechpartner expandable section */}
|
||||
{isExpanded && contactPersons.length > 0 && (
|
||||
<div className={styles.cpSection}>
|
||||
<div className={styles.cpTitle}>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="6" cy="5" r="2.5" />
|
||||
<path d="M1 13c0-2 2-3.5 5-3.5" />
|
||||
<circle cx="12" cy="6" r="2" />
|
||||
<path d="M9 13c0-1.5 1.5-2.5 3-2.5s3 1 3 2.5" />
|
||||
</svg>
|
||||
Ansprechpartner ({contactPersons.length})
|
||||
</div>
|
||||
{contactPersons.map((cp, idx) => {
|
||||
const cpName =
|
||||
[cp.firstName, cp.lastName]
|
||||
.filter(Boolean)
|
||||
.join(' ') || 'Unbenannt';
|
||||
return (
|
||||
<div key={idx} className={styles.cpRow}>
|
||||
<div className={styles.cpInfo}>
|
||||
<span className={styles.cpName}>
|
||||
{cpName}
|
||||
{cp.primary && (
|
||||
<span className={styles.cpPrimary}>Haupt</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.cpMeta}>
|
||||
{[cp.emailAddress, cp.phoneNumber]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || 'Keine Kontaktdaten'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,11 +359,25 @@ export interface DealVoucher {
|
|||
voucher: LexwareVoucher;
|
||||
}
|
||||
|
||||
export interface LexwareContactPersonEntry {
|
||||
salutation?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
primary?: boolean;
|
||||
emailAddress?: string;
|
||||
phoneNumber?: string;
|
||||
}
|
||||
|
||||
export interface LexwareContact {
|
||||
id: string;
|
||||
version: number;
|
||||
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 };
|
||||
addresses?: {
|
||||
billing?: { street?: string; zip?: string; city?: string; countryCode?: string }[];
|
||||
|
|
@ -378,6 +392,10 @@ export interface LexwareContact {
|
|||
export interface LexwareContactSearchParams {
|
||||
name?: string;
|
||||
email?: string;
|
||||
customer?: boolean;
|
||||
vendor?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface LexwareVouchersQueryParams {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue