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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user