feat: enhance GrowTentKonfigurator with tent filtering and improved rendering

- Added helper functions to filter tent products by shape and generate coverage descriptions based on dimensions.
- Implemented logic to handle product image rendering with caching and loading states.
- Updated tent selection process to dynamically find and display products based on selected tent shape.
- Enhanced user interface with loading indicators and improved layout for product selection.
This commit is contained in:
sebseb7
2025-09-03 11:31:24 +02:00
parent 1a5143a55d
commit ead44afb69

View File

@@ -10,10 +10,13 @@ import {
ListItem,
ListItemText,
ListItemSecondaryAction,
Grid,
CardMedia,
CircularProgress,
} from '@mui/material';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { TentShapeSelector, ProductSelector, ExtrasSelector } from '../components/configurator/index.js';
import { tentShapes, tentSizes, lightTypes, ventilationTypes, extras } from '../data/configuratorData.js';
import { tentShapes, lightTypes, ventilationTypes, extras } from '../data/configuratorData.js';
function setCachedCategoryData(categoryId, data) {
if (!window.productCache) {
@@ -143,12 +146,19 @@ class GrowTentKonfigurator extends Component {
return;
}
console.log(`productList:${categoryId}`);
window.socketManager.off(`productList:${categoryId}`);
// Track if we've received the full response to ignore stub response if needed
let receivedFullResponse = false;
window.socketManager.on(`productList:${categoryId}`,(response) => {
console.log("getCategoryProducts full response", response);
receivedFullResponse = true;
setCachedCategoryData(categoryId, response);
// Force re-render when data arrives
if (categoryId === 'Zelte') {
this.forceUpdate();
}
});
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
@@ -156,7 +166,13 @@ class GrowTentKonfigurator extends Component {
"getCategoryProducts",
{ categoryId: categoryId, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
(response) => {
console.log("getCategoryProducts stub response", response);
// Only process stub response if we haven't received the full response yet
if (!receivedFullResponse) {
setCachedCategoryData(categoryId, response);
if (categoryId === 'Zelte') {
this.forceUpdate();
}
}
}
);
}
@@ -188,6 +204,133 @@ class GrowTentKonfigurator extends Component {
// Helper function to filter real tent products by shape
filterTentsByShape(tentShape, products, attributes) {
if (!products || !attributes || !tentShape) {
return [];
}
// Parse the shape dimensions (e.g., '80x80' -> width: 80, depth: 80)
const [widthStr, depthStr] = tentShape.split('x');
const targetWidth = parseInt(widthStr);
const targetDepth = parseInt(depthStr);
// Group attributes by product for efficient lookup
const productAttributes = {};
attributes.forEach(attr => {
if (!productAttributes[attr.kArtikel]) {
productAttributes[attr.kArtikel] = {};
}
productAttributes[attr.kArtikel][attr.cName] = attr.cWert;
});
// Filter products that match the target dimensions
const matchingProducts = products.filter(product => {
const attrs = productAttributes[product.id];
if (!attrs) return false;
// Check width (Breite)
const widthAttr = attrs['Breite'];
if (!widthAttr) return false;
const widthMatch = widthAttr.match(/(\d+)cm breit/i);
if (!widthMatch) return false;
const productWidth = parseInt(widthMatch[1]);
// Check depth (Tiefe)
const depthAttr = attrs['Tiefe'];
if (!depthAttr) return false;
const depthMatch = depthAttr.match(/(\d+)cm tief/i);
if (!depthMatch) return false;
const productDepth = parseInt(depthMatch[1]);
// Check if dimensions match
return productWidth === targetWidth && productDepth === targetDepth;
});
return matchingProducts.map(product => {
// Convert to the format expected by the configurator
console.log('Raw product from backend:', product);
return product;
}); // No sorting needed
}
// Helper function to generate coverage descriptions
getCoverageDescription(width, depth) {
const area = width * depth;
if (area <= 3600) return '1-2 Pflanzen'; // 60x60
if (area <= 6400) return '2-4 Pflanzen'; // 80x80
if (area <= 10000) return '4-6 Pflanzen'; // 100x100
return '3-6 Pflanzen'; // 120x60 and larger
}
// Render tent image using working code from Product component
renderTentImage(product) {
if (!window.smallPicCache) {
window.smallPicCache = {};
}
const pictureList = product.pictureList;
if (!pictureList || pictureList.length === 0 || !pictureList.split(',').length) {
return (
<CardMedia
component="img"
height={window.innerWidth < 600 ? "240" : "180"}
image="/assets/images/nopicture.jpg"
alt={product.name || 'Produktbild'}
sx={{
objectFit: 'contain',
width: '100%'
}}
/>
);
}
const bildId = pictureList.split(',')[0];
if (window.smallPicCache[bildId]) {
return (
<CardMedia
component="img"
height={window.innerWidth < 600 ? "240" : "180"}
image={window.smallPicCache[bildId]}
alt={product.name || 'Produktbild'}
sx={{
objectFit: 'contain',
width: '100%'
}}
/>
);
}
// Load image if not cached
if (!this.loadingImages) this.loadingImages = new Set();
if (!this.loadingImages.has(bildId)) {
this.loadingImages.add(bildId);
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
if (res.success) {
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
this.forceUpdate();
}
this.loadingImages.delete(bildId);
});
}
return (
<CircularProgress sx={{ color: '#90ffc0' }} />
);
}
// Get real tent products for the selected shape
getTentsForShape(shapeId) {
const cachedData = getCachedCategoryData('Zelte');
if (!cachedData || !cachedData.products || !cachedData.attributes) {
return [];
}
return this.filterTentsByShape(shapeId, cachedData.products, cachedData.attributes);
}
calculateTotalPrice() {
let total = 0;
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras } = this.state;
@@ -195,9 +338,17 @@ class GrowTentKonfigurator extends Component {
// Add tent price
if (selectedTentSize) {
const tent = tentSizes.find(t => t.id === selectedTentSize);
if (tent) {
total += tent.price;
// Find the selected tent from all available shapes
let selectedTent = null;
const allShapes = ['60x60', '80x80', '100x100', '120x60'];
for (const shape of allShapes) {
const tents = this.getTentsForShape(shape);
selectedTent = tents.find(t => t.id === selectedTentSize);
if (selectedTent) break;
}
if (selectedTent) {
total += selectedTent.price;
itemCount++;
}
}
@@ -260,9 +411,17 @@ class GrowTentKonfigurator extends Component {
// Calculate original total without discount
if (selectedTentSize) {
const tent = tentSizes.find(t => t.id === selectedTentSize);
if (tent) {
originalTotal += tent.price;
// Find the selected tent from all available shapes
let selectedTent = null;
const allShapes = ['60x60', '80x80', '100x100', '120x60'];
for (const shape of allShapes) {
const tents = this.getTentsForShape(shape);
selectedTent = tents.find(t => t.id === selectedTentSize);
if (selectedTent) break;
}
if (selectedTent) {
originalTotal += selectedTent.price;
itemCount++;
}
}
@@ -321,25 +480,143 @@ class GrowTentKonfigurator extends Component {
}
renderTentSizeSection() {
const { selectedTentSize, selectedTentShape } = this.state;
// Filter tents by selected shape
const filteredTents = tentSizes.filter(tent => tent.shapeId === selectedTentShape);
const { selectedTentShape } = this.state;
if (!selectedTentShape) {
return null; // Don't show tent sizes until shape is selected
}
// Get real filtered tent products for the selected shape
const filteredTents = this.getTentsForShape(selectedTentShape);
// Show loading state if data is not yet available
if (filteredTents.length === 0) {
const cachedData = getCachedCategoryData('Zelte');
if (!cachedData) {
return (
<ProductSelector
products={filteredTents}
selectedValue={selectedTentSize}
onSelect={this.handleTentSizeSelect}
productType="tent"
title="2. Growbox Produkt auswählen"
subtitle={`Wähle das passende Produkt für deine ${selectedTentShape} Growbox`}
gridSize={{ xs: 12, sm: 6, md: 3 }}
/>
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h6" color="text.secondary">
Lade Growbox-Produkte...
</Typography>
</Box>
);
}
// If we have cached data but no filtered tents, show empty state
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h6" color="text.secondary">
Keine Produkte für diese Größe verfügbar
</Typography>
</Box>
);
}
console.log('Product display:', {
productsCount: filteredTents.length,
firstProduct: filteredTents[0] || null
});
if (filteredTents.length === 0) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h6" color="text.secondary">
Keine Produkte verfügbar
</Typography>
</Box>
);
}
return (
<Box sx={{ mb: 4 }}>
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
2. Growbox Produkt auswählen
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Wähle das passende Produkt für deine {selectedTentShape} Growbox
</Typography>
<Grid container spacing={2}>
{filteredTents.map((product, _index) => (
<Grid item xs={12} sm={6} md={4} key={product.id}>
<Box sx={{
width: { xs: '100%', sm: '250px' },
height: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: '8px',
boxShadow: '0px 2px 8px rgba(0,0,0,0.1)',
overflow: 'hidden',
backgroundColor: '#ffffff',
cursor: 'pointer',
border: '2px solid',
borderColor: this.state.selectedTentSize === product.id ? '#2e7d32' : '#e0e0e0',
'&:hover': {
borderColor: '#90caf9'
}
}}
onClick={() => this.handleTentSizeSelect(product.id)}>
{/* Image */}
<Box sx={{
height: { xs: '240px', sm: '180px' },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff'
}}>
{this.renderTentImage(product)}
</Box>
{/* Content */}
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
{/* Name */}
<Typography
variant="h6"
sx={{
fontWeight: 600,
mb: 1,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{product.name}
</Typography>
{/* Price */}
<Typography
variant="h6"
sx={{
color: '#2e7d32',
fontWeight: 'bold',
mt: 'auto'
}}
>
{product.price ? new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(product.price) : 'Kein Preis'}
</Typography>
{/* VAT */}
<Typography variant="body2" sx={{ color: '#77aa77', fontSize: '0.75em' }}>
(incl. 19% MwSt.,*)
</Typography>
{/* Selection Indicator */}
{this.state.selectedTentSize === product.id && (
<Box sx={{ mt: 1, textAlign: 'center' }}>
<Typography variant="body2" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
Ausgewählt
</Typography>
</Box>
)}
</Box>
</Box>
</Grid>
))}
</Grid>
</Box>
);
}
@@ -391,7 +668,14 @@ class GrowTentKonfigurator extends Component {
renderInlineSummary() {
const { selectedTentSize, selectedLightType, selectedVentilationType, selectedExtras, totalPrice } = this.state;
const selectedTent = tentSizes.find(t => t.id === selectedTentSize);
// Find the selected tent from all available shapes
let selectedTent = null;
const allShapes = ['60x60', '80x80', '100x100', '120x60'];
for (const shape of allShapes) {
const tents = this.getTentsForShape(shape);
selectedTent = tents.find(t => t.id === selectedTentSize);
if (selectedTent) break;
}
const selectedLight = lightTypes.find(l => l.id === selectedLightType);
const selectedVentilation = ventilationTypes.find(v => v.id === selectedVentilationType);
const selectedExtrasItems = extras.filter(e => selectedExtras.includes(e.id));