diff --git a/frontend/src/components/AddressList.tsx b/frontend/src/components/AddressList.tsx new file mode 100644 index 0000000..bc778ad --- /dev/null +++ b/frontend/src/components/AddressList.tsx @@ -0,0 +1,288 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { apiGet, apiPost, apiPatch, apiDelete } from '@/api/client'; + +interface Address { + id: string; + entity_type: string; + entity_id: string; + label: string; + address_type: string; + street: string | null; + street_number: string | null; + city: string | null; + zip: string | null; + state: string | null; + country: string | null; + is_default: boolean; + created_at: string | null; + updated_at: string | null; +} + +interface AddressListProps { + entityType: 'company' | 'contact'; + entityId: string; +} + +const ADDRESS_TYPE_COLORS: Record = { + billing: 'bg-blue-100 text-blue-800', + shipping: 'bg-purple-100 text-purple-800', + headquarters: 'bg-green-100 text-green-800', + branch: 'bg-yellow-100 text-yellow-800', + private: 'bg-pink-100 text-pink-800', + other: 'bg-gray-100 text-gray-800', +}; + +export function AddressList({ entityType, entityId }: AddressListProps) { + const { t } = useTranslation(); + const [addresses, setAddresses] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showForm, setShowForm] = useState(false); + const [editingAddress, setEditingAddress] = useState
(null); + + const fetchAddresses = React.useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await apiGet<{ items: Address[]; total: number }>( + `/addresses?entity_type=${entityType}&entity_id=${entityId}` + ); + setAddresses(data.items); + } catch (err: any) { + setError(err.message || t('common.error')); + } finally { + setLoading(false); + } + }, [entityType, entityId, t]); + + React.useEffect(() => { + fetchAddresses(); + }, [fetchAddresses]); + + const handleDelete = async (id: string) => { + if (!window.confirm(t('address.confirmDelete'))) return; + try { + await apiDelete(`/addresses/${id}`); + await fetchAddresses(); + } catch (err: any) { + setError(err.message || t('common.error')); + } + }; + + const handleSetDefault = async (id: string) => { + try { + await apiPatch(`/addresses/${id}`, { is_default: true }); + await fetchAddresses(); + } catch (err: any) { + setError(err.message || t('common.error')); + } + }; + + const handleSave = async (data: Partial
) => { + try { + if (editingAddress) { + await apiPatch(`/addresses/${editingAddress.id}`, data); + } else { + await apiPost('/addresses', { ...data, entity_type: entityType, entity_id: entityId }); + } + setShowForm(false); + setEditingAddress(null); + await fetchAddresses(); + } catch (err: any) { + setError(err.message || t('common.error')); + } + }; + + if (loading) { + return
{t('common.loading')}
; + } + + return ( +
+
+

{t('address.title')}

+ +
+ + {error && ( +
{error}
+ )} + + {showForm && ( + { setShowForm(false); setEditingAddress(null); }} + /> + )} + + {addresses.length === 0 && !showForm ? ( +

{t('address.noAddresses')}

+ ) : ( +
+ {addresses.map((addr) => ( +
+
+
+ {addr.label} + + {t(`addressType.${addr.address_type}`)} + + {addr.is_default && ( + + {t('address.defaultAddress')} + + )} +
+
+ {addr.street && {addr.street}} + {addr.street_number && {addr.street_number}} + {(addr.street || addr.street_number) &&
} + {addr.zip && {addr.zip} } + {addr.city && {addr.city}} + {(addr.zip || addr.city) &&
} + {addr.state && {addr.state}, } + {addr.country && {addr.country}} +
+
+
+ {!addr.is_default && ( + + )} + + +
+
+ ))} +
+ )} +
+ ); +} + +function AddressForm({ + address, + onSave, + onCancel, +}: { + address: Address | null; + onSave: (data: Partial
) => void; + onCancel: () => void; +}) { + const { t } = useTranslation(); + const [label, setLabel] = useState(address?.label || ''); + const [addressType, setAddressType] = useState(address?.address_type || 'headquarters'); + const [street, setStreet] = useState(address?.street || ''); + const [streetNumber, setStreetNumber] = useState(address?.street_number || ''); + const [city, setCity] = useState(address?.city || ''); + const [zip, setZip] = useState(address?.zip || ''); + const [state, setState] = useState(address?.state || ''); + const [country, setCountry] = useState(address?.country || ''); + const [isDefault, setIsDefault] = useState(address?.is_default || false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave({ label, address_type: addressType, street, street_number: streetNumber, city, zip, state, country, is_default: isDefault }); + }; + + const addressTypes = ['billing', 'shipping', 'headquarters', 'branch', 'private', 'other']; + + return ( +
+
+
+ + setLabel(e.target.value)} + required + className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" + /> +
+
+ + +
+
+
+
+ + setStreet(e.target.value)} className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" /> +
+
+ + setStreetNumber(e.target.value)} className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" /> +
+
+
+
+ + setZip(e.target.value)} className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" /> +
+
+ + setCity(e.target.value)} className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" /> +
+
+ + setState(e.target.value)} className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" /> +
+
+
+
+ + setCountry(e.target.value)} maxLength={2} placeholder="DE" className="mt-1 block w-full rounded-md border border-secondary-300 px-3 py-1.5 text-sm" /> +
+
+ +
+
+
+ + +
+
+ ); +}