feat: Mini-Apps registry, Stammdaten backend persistence, bank accounts
- Register 6 mini-apps in kommunikation plugin (contact_picker, file_share, calendar_invite, mail_forward, ai_search, task_create) - Connect AdressenTab to backend API (GET/POST/PATCH/DELETE /addresses) - Create BankAccount model, schema, service, routes + migration 0033 - Connect KontenTab to backend API (GET/POST/PATCH/DELETE /bank-accounts) - AI tools already working: 7 tools registered (hybrid_search, get_contact_mails, etc.)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SettingsFirmendatenPage } from './SettingsFirmendaten';
|
||||
import { SettingsCurrenciesPage } from './SettingsCurrencies';
|
||||
@@ -10,6 +10,33 @@ import { Button } from '@/components/ui/Button';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { Table, TableColumn } from '@/components/ui/Table';
|
||||
import { Plus, Trash2, Pencil } from 'lucide-react';
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from '@/api/client';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
|
||||
// --- Address type mapping (frontend German -> backend English) ---
|
||||
const addressTypeToBackend: Record<string, string> = {
|
||||
standort: 'headquarters',
|
||||
rechnung: 'billing',
|
||||
lieferung: 'shipping',
|
||||
eigene: 'other',
|
||||
};
|
||||
|
||||
const addressTypeToFrontend: Record<string, string> = {
|
||||
headquarters: 'standort',
|
||||
billing: 'rechnung',
|
||||
shipping: 'lieferung',
|
||||
branch: 'standort',
|
||||
private: 'eigene',
|
||||
other: 'eigene',
|
||||
};
|
||||
|
||||
const addressTypeOptions = [
|
||||
{ value: 'standort', label: 'Standort' },
|
||||
{ value: 'rechnung', label: 'Rechnungsadresse' },
|
||||
{ value: 'lieferung', label: 'Lieferadresse' },
|
||||
{ value: 'eigene', label: 'Eigene Bezeichnung' },
|
||||
];
|
||||
|
||||
// --- Adressen Tab ---
|
||||
interface Address {
|
||||
@@ -22,18 +49,54 @@ interface Address {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const addressTypeOptions = [
|
||||
{ value: 'standort', label: 'Standort' },
|
||||
{ value: 'rechnung', label: 'Rechnungsadresse' },
|
||||
{ value: 'lieferung', label: 'Lieferadresse' },
|
||||
{ value: 'eigene', label: 'Eigene Bezeichnung' },
|
||||
];
|
||||
interface ApiAddress {
|
||||
id: string;
|
||||
address_type: string;
|
||||
street: string;
|
||||
zip: string;
|
||||
city: string;
|
||||
country: string;
|
||||
label: string;
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
function AdressenTab() {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const currentTenant = useAuthStore((state) => state.currentTenant);
|
||||
const [addresses, setAddresses] = useState<Address[]>([]);
|
||||
const [editing, setEditing] = useState<Address | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchAddresses = useCallback(async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await apiGet<{ items: ApiAddress[]; total: number }>(
|
||||
`/addresses?entity_type=contact&entity_id=${currentTenant.id}`
|
||||
);
|
||||
setAddresses(
|
||||
data.items.map((a) => ({
|
||||
id: a.id,
|
||||
type: addressTypeToFrontend[a.address_type] || 'eigene',
|
||||
street: a.street || '',
|
||||
zip: a.zip || '',
|
||||
city: a.city || '',
|
||||
country: a.country || 'DE',
|
||||
label: a.label || '',
|
||||
}))
|
||||
);
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Fehler beim Laden der Adressen');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentTenant?.id, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAddresses();
|
||||
}, [fetchAddresses]);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditing({ id: '', type: 'standort', street: '', zip: '', city: '', country: 'DE', label: '' });
|
||||
@@ -45,19 +108,52 @@ function AdressenTab() {
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setAddresses(addresses.filter(a => a.id !== id));
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await apiDelete(`/addresses/${id}`);
|
||||
setAddresses(addresses.filter(a => a.id !== id));
|
||||
toast.success('Adresse gelöscht');
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editing) return;
|
||||
if (editing.id) {
|
||||
setAddresses(addresses.map(a => a.id === editing.id ? editing : a));
|
||||
} else {
|
||||
setAddresses([...addresses, { ...editing, id: Date.now().toString() }]);
|
||||
const handleSave = async () => {
|
||||
if (!editing || !currentTenant?.id) return;
|
||||
try {
|
||||
const backendType = addressTypeToBackend[editing.type] || 'other';
|
||||
const payload = {
|
||||
entity_type: 'contact',
|
||||
entity_id: currentTenant.id,
|
||||
label: editing.type === 'eigene' ? editing.label : addressTypeOptions.find(o => o.value === editing.type)?.label || editing.label,
|
||||
address_type: backendType,
|
||||
street: editing.street,
|
||||
zip: editing.zip,
|
||||
city: editing.city,
|
||||
country: editing.country,
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
if (editing.id) {
|
||||
await apiPatch(`/addresses/${editing.id}`, {
|
||||
label: payload.label,
|
||||
address_type: payload.address_type,
|
||||
street: payload.street,
|
||||
zip: payload.zip,
|
||||
city: payload.city,
|
||||
country: payload.country,
|
||||
});
|
||||
toast.success('Adresse aktualisiert');
|
||||
} else {
|
||||
await apiPost('/addresses', payload);
|
||||
toast.success('Adresse angelegt');
|
||||
}
|
||||
setShowForm(false);
|
||||
setEditing(null);
|
||||
await fetchAddresses();
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Fehler beim Speichern');
|
||||
}
|
||||
setShowForm(false);
|
||||
setEditing(null);
|
||||
};
|
||||
|
||||
const columns: TableColumn<Address>[] = [
|
||||
@@ -89,7 +185,11 @@ function AdressenTab() {
|
||||
{t('common.add', 'Hinzufügen')}
|
||||
</Button>
|
||||
}>
|
||||
<Table columns={columns} data={addresses} rowKey={(row) => row.id} emptyMessage="Noch keine Adressen angelegt" />
|
||||
{loading ? (
|
||||
<div className="text-center py-4 text-secondary-500">Lade Adressen...</div>
|
||||
) : (
|
||||
<Table columns={columns} data={addresses} rowKey={(row) => row.id} emptyMessage="Noch keine Adressen angelegt" />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{showForm && editing && (
|
||||
@@ -152,11 +252,48 @@ interface BankAccount {
|
||||
defaultTax: string;
|
||||
}
|
||||
|
||||
interface ApiBankAccount {
|
||||
id: string;
|
||||
bank_name: string;
|
||||
iban: string;
|
||||
bic: string | null;
|
||||
account_holder: string | null;
|
||||
default_tax: string | null;
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
function KontenTab() {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [accounts, setAccounts] = useState<BankAccount[]>([]);
|
||||
const [editing, setEditing] = useState<BankAccount | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await apiGet<{ items: ApiBankAccount[]; total: number }>('/bank-accounts');
|
||||
setAccounts(
|
||||
data.items.map((a) => ({
|
||||
id: a.id,
|
||||
bankName: a.bank_name,
|
||||
iban: a.iban,
|
||||
bic: a.bic || '',
|
||||
accountHolder: a.account_holder || '',
|
||||
defaultTax: a.default_tax || '',
|
||||
}))
|
||||
);
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Fehler beim Laden der Konten');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts();
|
||||
}, [fetchAccounts]);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditing({ id: '', bankName: '', iban: '', bic: '', accountHolder: '', defaultTax: '' });
|
||||
@@ -168,19 +305,41 @@ function KontenTab() {
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setAccounts(accounts.filter(a => a.id !== id));
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await apiDelete(`/bank-accounts/${id}`);
|
||||
setAccounts(accounts.filter(a => a.id !== id));
|
||||
toast.success('Konto gelöscht');
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
if (!editing) return;
|
||||
if (editing.id) {
|
||||
setAccounts(accounts.map(a => a.id === editing.id ? editing : a));
|
||||
} else {
|
||||
setAccounts([...accounts, { ...editing, id: Date.now().toString() }]);
|
||||
try {
|
||||
const payload = {
|
||||
bank_name: editing.bankName,
|
||||
iban: editing.iban,
|
||||
bic: editing.bic || null,
|
||||
account_holder: editing.accountHolder || null,
|
||||
default_tax: editing.defaultTax || null,
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
if (editing.id) {
|
||||
await apiPatch(`/bank-accounts/${editing.id}`, payload);
|
||||
toast.success('Konto aktualisiert');
|
||||
} else {
|
||||
await apiPost('/bank-accounts', payload);
|
||||
toast.success('Konto angelegt');
|
||||
}
|
||||
setShowForm(false);
|
||||
setEditing(null);
|
||||
await fetchAccounts();
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Fehler beim Speichern');
|
||||
}
|
||||
setShowForm(false);
|
||||
setEditing(null);
|
||||
};
|
||||
|
||||
const columns: TableColumn<BankAccount>[] = [
|
||||
@@ -211,7 +370,11 @@ function KontenTab() {
|
||||
{t('common.add', 'Hinzufügen')}
|
||||
</Button>
|
||||
}>
|
||||
<Table columns={columns} data={accounts} rowKey={(row) => row.id} emptyMessage="Noch keine Konten angelegt" />
|
||||
{loading ? (
|
||||
<div className="text-center py-4 text-secondary-500">Lade Konten...</div>
|
||||
) : (
|
||||
<Table columns={columns} data={accounts} rowKey={(row) => row.id} emptyMessage="Noch keine Konten angelegt" />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{showForm && editing && (
|
||||
|
||||
Reference in New Issue
Block a user