fix: repair Rentman equipment sync with real API field mapping

- Map real Rentman fields: price -> rental_price, folder -> category/subcategory
  (resolved via folder hierarchy to top-level categories), code -> number,
  shop_description_long (fallback short/external_remark) -> description (HTML stripped)
- Sync only in_shop items; exclude archived/temporary from public catalog
- Extract known brands from equipment names (d&b, Shure, Pioneer, ...)
- Build specifications from physical fields (power, weight, dimensions)
- Persist equipment images in docker volume (survive redeploys)
- Router: filter available items, add price_asc/price_desc sorting
- Frontend: show prices in catalog card, detail page and cart with
  per-day subtotal; price sorting options
- Tests: use real Rentman field names in fixtures, mock folder_map
This commit is contained in:
Agent Zero
2026-09-24 21:26:18 +02:00
parent 654ec09ce1
commit df50ea8b4b
10 changed files with 233 additions and 39 deletions
+4 -2
View File
@@ -20,8 +20,10 @@ export default function EquipmentCard({ item, onClick, onAddToCart }: Props) {
<div className="p-4">
<h3 className="font-semibold text-sm mb-1 truncate" style={{ color: 'var(--text)' }}>{item.name}</h3>
<p className="text-xs mb-3 line-clamp-2" style={{ color: 'var(--secondary)' }}>{item.description}</p>
<div className="flex items-center justify-between">
<span className="text-xs" style={{ color: 'var(--secondary)' }}>{item.code}</span>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium" style={{ color: 'var(--text)' }}>
{item.rental_price != null ? `${Number(item.rental_price).toFixed(2).replace('.', ',')} € / Tag` : item.code}
</span>
<button onClick={e => { e.stopPropagation(); onAddToCart(item); }} className="hms-btn hms-btn-ghost text-xs px-3 py-1.5" style={{ color: 'var(--color-accent)' }} aria-label={item.name + ' zum Warenkorb hinzufügen'}>+ Hinzufügen</button>
</div>
</div>
+8 -2
View File
@@ -41,7 +41,7 @@ export default function EquipmentDetail() {
addItemByFields({
equipment_id: item.id,
name: item.name,
rental_price: null,
rental_price: item.rental_price ?? null,
image_url: item.image_url || null,
});
navigateTo('/warenkorb');
@@ -99,6 +99,12 @@ export default function EquipmentDetail() {
)}
<div className="hms-card p-6">
<h2 className="text-sm font-semibold mb-4" style={{ color: 'var(--text)' }}>Mietanfrage</h2>
{item.rental_price != null && (
<div className="flex items-baseline justify-between mb-4 pb-4 border-b" style={{ borderColor: 'var(--border)' }}>
<span className="text-sm" style={{ color: 'var(--secondary)' }}>Mietpreis pro Tag</span>
<span className="text-2xl font-bold" style={{ color: 'var(--color-accent)' }}>{Number(item.rental_price).toFixed(2).replace('.', ',')} €</span>
</div>
)}
<div className="grid grid-cols-2 gap-3 mb-4">
<div>
<label htmlFor="rental-start" className="block text-xs font-medium mb-1" style={{ color: 'var(--secondary)' }}>Mietbeginn</label>
@@ -121,7 +127,7 @@ export default function EquipmentDetail() {
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Zur Mietanfrage hinzufügen
</button>
<p className="text-xs mt-3 text-center" style={{ color: 'var(--secondary)' }}>Preise auf Anfrage – unverbindliche Mietanfrage</p>
<p className="text-xs mt-3 text-center" style={{ color: 'var(--secondary)' }}>{item.rental_price != null ? 'Unverbindliche Mietanfrage – Endpreis inkl. Rabatt nach Prüfung' : 'Preise auf Anfrage – unverbindliche Mietanfrage'}</p>
</div>
</div>
</div>
+3 -1
View File
@@ -85,7 +85,7 @@ export default function Mietkatalog() {
addItemByFields({
equipment_id: item.id,
name: item.name,
rental_price: null,
rental_price: item.rental_price ?? null,
image_url: item.image_url || null,
});
}
@@ -120,6 +120,8 @@ export default function Mietkatalog() {
<select value={sortBy} onChange={e => { setSortBy(e.target.value as SortOption); setCurrentPage(1); reload(); }} className="hms-input lg:w-48" aria-label="Sortieren nach">
<option value="name_asc">Sortieren: Name (A-Z)</option>
<option value="name_desc">Sortieren: Name (Z-A)</option>
<option value="price_asc">Sortieren: Preis (aufsteigend)</option>
<option value="price_desc">Sortieren: Preis (absteigend)</option>
</select>
</div>
{categories.length > 1 && (
+11
View File
@@ -74,6 +74,11 @@ export default function Warenkorb() {
</button>
</div>
<div className="flex flex-wrap items-center gap-3 mt-3">
{item.rental_price != null && (
<span className="text-xs font-medium" style={{ color: 'var(--color-accent)' }}>
{Number(item.rental_price).toFixed(2).replace('.', ',')} € / Tag × {item.quantity}
</span>
)}
<div className="flex items-center gap-2">
<button onClick={() => decrementQuantity(item.equipment_id)} className="w-7 h-7 rounded border flex items-center justify-center" style={{ borderColor: 'var(--border-strong)', color: 'var(--text-muted)' }} aria-label="Anzahl verringern">−</button>
<span className="w-8 text-center text-sm font-medium" style={{ color: 'var(--text)' }}>{item.quantity}</span>
@@ -92,6 +97,12 @@ export default function Warenkorb() {
<div className="text-sm mb-4 pb-4 border-b" style={{ borderColor: 'var(--border)', color: 'var(--secondary)' }}>
<div className="flex justify-between mb-1"><span>Geräte gesamt:</span><span className="font-medium" style={{ color: 'var(--text)' }}>{totalCount}</span></div>
<div className="flex justify-between"><span>Positionen:</span><span className="font-medium" style={{ color: 'var(--text)' }}>{cartItems.length}</span></div>
{(() => {
const known = cartItems.filter(i => i.rental_price != null);
if (known.length === 0) return null;
const sum = known.reduce((acc, i) => acc + Number(i.rental_price) * i.quantity, 0);
return <div className="flex justify-between mt-1"><span>Zwischensumme (pro Tag, ohne MwSt.):</span><span className="font-medium" style={{ color: 'var(--text)' }}>{sum.toFixed(2).replace('.', ',')} €</span></div>;
})()}
</div>
<form onSubmit={submitRequest} noValidate>
<div className="space-y-3">
+1 -1
View File
@@ -33,7 +33,7 @@ export interface PaginatedEquipment {
total_pages: number;
}
export type SortOption = "name_asc" | "name_desc";
export type SortOption = "name_asc" | "name_desc" | "price_asc" | "price_desc";
export interface CartItem {
equipment_id: number;