Compare commits
2 Commits
3660f80277
...
cbb8dc463f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbb8dc463f | ||
|
|
479e328e7c |
@@ -1,12 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
class ExtrasSelector extends Component {
|
||||
formatPrice(price) {
|
||||
@@ -16,124 +13,159 @@ class ExtrasSelector extends Component {
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
// Render product image using working code from GrowTentKonfigurator
|
||||
renderProductImage(product) {
|
||||
if (!window.smallPicCache) {
|
||||
window.smallPicCache = {};
|
||||
}
|
||||
|
||||
const pictureList = product.pictureList;
|
||||
|
||||
if (!pictureList || pictureList.length === 0 || !pictureList.split(',').length) {
|
||||
return (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="160"
|
||||
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="160"
|
||||
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 (
|
||||
<Box sx={{ height: '160px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CircularProgress sx={{ color: '#90ffc0' }} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderExtraCard(extra) {
|
||||
const { selectedExtras, onExtraToggle, showImage = true } = this.props;
|
||||
const isSelected = selectedExtras.includes(extra.id);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={extra.id}
|
||||
sx={{
|
||||
height: '100%',
|
||||
border: '2px solid',
|
||||
borderColor: isSelected ? '#2e7d32' : '#e0e0e0',
|
||||
backgroundColor: isSelected ? '#f1f8e9' : '#ffffff',
|
||||
'&:hover': {
|
||||
boxShadow: 5,
|
||||
borderColor: isSelected ? '#2e7d32' : '#90caf9'
|
||||
},
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => onExtraToggle(extra.id)}
|
||||
>
|
||||
<Box sx={{
|
||||
width: { xs: '100%', sm: '250px' },
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
border: '2px solid',
|
||||
borderColor: isSelected ? '#2e7d32' : '#e0e0e0',
|
||||
backgroundColor: isSelected ? '#f1f8e9' : '#ffffff',
|
||||
'&:hover': {
|
||||
boxShadow: 6,
|
||||
borderColor: isSelected ? '#2e7d32' : '#90caf9'
|
||||
},
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
onClick={() => onExtraToggle(extra.id)}>
|
||||
{/* Image */}
|
||||
{showImage && (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="160"
|
||||
image={extra.image}
|
||||
alt={extra.name}
|
||||
sx={{ objectFit: 'cover' }}
|
||||
/>
|
||||
)}
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onExtraToggle(extra.id);
|
||||
}}
|
||||
sx={{
|
||||
color: '#2e7d32',
|
||||
'&.Mui-checked': { color: '#2e7d32' },
|
||||
padding: 0
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label=""
|
||||
sx={{ margin: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<Typography variant="h6" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{this.formatPrice(extra.price)}
|
||||
</Typography>
|
||||
<Box sx={{
|
||||
height: { xs: '240px', sm: '180px' },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#ffffff'
|
||||
}}>
|
||||
{this.renderProductImage(extra)}
|
||||
</Box>
|
||||
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Name */}
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
{extra.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{extra.description}
|
||||
|
||||
{/* Price with VAT - Same as other sections */}
|
||||
<Typography variant="h6" sx={{
|
||||
color: '#2e7d32',
|
||||
fontWeight: 'bold',
|
||||
mt: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<span>{extra.price ? this.formatPrice(extra.price) : 'Kein Preis'}</span>
|
||||
{extra.vat && (
|
||||
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
||||
(incl. {extra.vat}% MwSt.,*)
|
||||
</small>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
|
||||
{/* Selection Indicator - Separate line */}
|
||||
{isSelected && (
|
||||
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
||||
<Typography variant="body2" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
✓ Hinzugefügt
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{
|
||||
color: '#2e7d32',
|
||||
fontWeight: 'bold',
|
||||
mt: 1,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
✓ Ausgewählt
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { extras, title, subtitle, groupByCategory = true, gridSize = { xs: 12, sm: 6, md: 4 } } = this.props;
|
||||
|
||||
if (groupByCategory) {
|
||||
// Group extras by category
|
||||
const groupedExtras = extras.reduce((acc, extra) => {
|
||||
if (!acc[extra.category]) {
|
||||
acc[extra.category] = [];
|
||||
}
|
||||
acc[extra.category].push(extra);
|
||||
return acc;
|
||||
}, {});
|
||||
const { extras, title, subtitle, gridSize = { xs: 12, sm: 6, md: 4 } } = this.props;
|
||||
|
||||
if (!extras || !Array.isArray(extras)) {
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{Object.entries(groupedExtras).map(([category, categoryExtras]) => (
|
||||
<Box key={category} sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ color: '#1976d2' }}>
|
||||
{category}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{categoryExtras.map(extra => (
|
||||
<Grid item {...gridSize} key={extra.id}>
|
||||
{this.renderExtraCard(extra)}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Keine Extras verfügbar
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render without category grouping
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
|
||||
@@ -41,54 +41,3 @@ export const tentShapes = [
|
||||
visualDepth: 60
|
||||
}
|
||||
];
|
||||
|
||||
export const extras = [
|
||||
{
|
||||
id: 'ph_tester',
|
||||
name: 'pH-Messgerät',
|
||||
description: 'Digitales pH-Meter',
|
||||
price: 29.99,
|
||||
image: '/assets/images/nopicture.jpg',
|
||||
category: 'Messung'
|
||||
},
|
||||
{
|
||||
id: 'nutrients_starter',
|
||||
name: 'Dünger Starter-Set',
|
||||
description: 'Komplettes Nährstoff-Set',
|
||||
price: 39.99,
|
||||
image: '/assets/images/nopicture.jpg',
|
||||
category: 'Nährstoffe'
|
||||
},
|
||||
{
|
||||
id: 'grow_pots',
|
||||
name: 'Grow-Töpfe Set (5x)',
|
||||
description: '5x Stofftöpfe 11L',
|
||||
price: 24.99,
|
||||
image: '/assets/images/nopicture.jpg',
|
||||
category: 'Töpfe'
|
||||
},
|
||||
{
|
||||
id: 'timer_socket',
|
||||
name: 'Zeitschaltuhr',
|
||||
description: 'Digitale Zeitschaltuhr',
|
||||
price: 19.99,
|
||||
image: '/assets/images/nopicture.jpg',
|
||||
category: 'Steuerung'
|
||||
},
|
||||
{
|
||||
id: 'thermometer',
|
||||
name: 'Thermo-Hygrometer',
|
||||
description: 'Min/Max Temperatur & Luftfeuchtigkeit',
|
||||
price: 14.99,
|
||||
image: '/assets/images/nopicture.jpg',
|
||||
category: 'Messung'
|
||||
},
|
||||
{
|
||||
id: 'pruning_shears',
|
||||
name: 'Gartenschere',
|
||||
description: 'Präzisions-Gartenschere',
|
||||
price: 16.99,
|
||||
image: '/assets/images/nopicture.jpg',
|
||||
category: 'Werkzeug'
|
||||
}
|
||||
];
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||
import { TentShapeSelector, ExtrasSelector } from '../components/configurator/index.js';
|
||||
import { tentShapes, extras } from '../data/configuratorData.js';
|
||||
import { tentShapes } from '../data/configuratorData.js';
|
||||
|
||||
function setCachedCategoryData(categoryId, data) {
|
||||
if (!window.productCache) {
|
||||
@@ -109,6 +109,7 @@ class GrowTentKonfigurator extends Component {
|
||||
this.fetchCategoryData("Zelte");
|
||||
this.fetchCategoryData("Lampen");
|
||||
this.fetchCategoryData("Abluft-sets");
|
||||
this.fetchCategoryData("Set-zubehoer");
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -497,14 +498,19 @@ class GrowTentKonfigurator extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Add extras prices
|
||||
selectedExtras.forEach(extraId => {
|
||||
const extra = extras.find(e => e.id === extraId);
|
||||
if (extra) {
|
||||
total += extra.price;
|
||||
itemCount++;
|
||||
}
|
||||
});
|
||||
const extrasData = getCachedCategoryData('Set-zubehoer');
|
||||
if (!extrasData || !Array.isArray(extrasData.products)) {
|
||||
console.warn('Extras data not available; skipping extras price in total calculation');
|
||||
} else {
|
||||
// Add extras prices
|
||||
selectedExtras.forEach(extraId => {
|
||||
const extra = extrasData.products.find(e => e.id === extraId);
|
||||
if (extra) {
|
||||
total += extra.price;
|
||||
itemCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply bundle discount
|
||||
let discountPercentage = 0;
|
||||
@@ -570,13 +576,18 @@ class GrowTentKonfigurator extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
selectedExtras.forEach(extraId => {
|
||||
const extra = extras.find(e => e.id === extraId);
|
||||
if (extra) {
|
||||
originalTotal += extra.price;
|
||||
itemCount++;
|
||||
}
|
||||
});
|
||||
const extrasData = getCachedCategoryData('Set-zubehoer');
|
||||
if (!extrasData || !Array.isArray(extrasData.products)) {
|
||||
console.warn('Extras data not available; skipping extras in savings calculation');
|
||||
} else {
|
||||
selectedExtras.forEach(extraId => {
|
||||
const extra = extrasData.products.find(e => e.id === extraId);
|
||||
if (extra) {
|
||||
originalTotal += extra.price;
|
||||
itemCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Progressive discount based on number of selected items
|
||||
let discountPercentage = 0;
|
||||
@@ -1034,14 +1045,42 @@ class GrowTentKonfigurator extends Component {
|
||||
|
||||
renderExtrasSection() {
|
||||
const { selectedExtras } = this.state;
|
||||
|
||||
const extrasData = getCachedCategoryData('Set-zubehoer');
|
||||
|
||||
if (!extrasData) {
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
5. Extras hinzufügen (optional)
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Lade Extras...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const extras = Array.isArray(extrasData.products) ? extrasData.products : [];
|
||||
|
||||
if (extras.length === 0) {
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h2" component="h2" gutterBottom sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
5. Extras hinzufügen (optional)
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Keine Extras verfügbar
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExtrasSelector
|
||||
extras={extras}
|
||||
selectedExtras={selectedExtras}
|
||||
onExtraToggle={this.handleExtraToggle}
|
||||
title="5. Extras hinzufügen (optional)"
|
||||
groupByCategory={true}
|
||||
gridSize={{ xs: 12, sm: 6, md: 4 }}
|
||||
/>
|
||||
);
|
||||
@@ -1062,7 +1101,10 @@ class GrowTentKonfigurator extends Component {
|
||||
const selectedLight = availableLamps.find(l => l.id === selectedLightType);
|
||||
const availableVentilation = this.state.selectedTentShape ? this.getVentilationForTentShape(this.state.selectedTentShape) : [];
|
||||
const selectedVentilation = availableVentilation.find(v => v.id === selectedVentilationType && v.isDeliverable);
|
||||
const selectedExtrasItems = extras.filter(e => selectedExtras.includes(e.id));
|
||||
const extrasData = getCachedCategoryData('Set-zubehoer');
|
||||
const selectedExtrasItems = Array.isArray(extrasData?.products)
|
||||
? extrasData.products.filter(e => selectedExtras.includes(e.id))
|
||||
: [];
|
||||
const savingsInfo = this.calculateSavings();
|
||||
|
||||
return (
|
||||
@@ -1086,11 +1128,15 @@ class GrowTentKonfigurator extends Component {
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={`Growbox: ${selectedTent.name}`}
|
||||
secondary={selectedTent.description}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
||||
{this.formatPrice(selectedTent.price)}
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
||||
<span>{this.formatPrice(selectedTent.price)}</span>
|
||||
{selectedTent.vat && (
|
||||
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
||||
(incl. {selectedTent.vat}% MwSt.,*)
|
||||
</small>
|
||||
)}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
@@ -1100,11 +1146,15 @@ class GrowTentKonfigurator extends Component {
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={`Beleuchtung: ${selectedLight.name}`}
|
||||
secondary={selectedLight.description}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
||||
{this.formatPrice(selectedLight.price)}
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
||||
<span>{this.formatPrice(selectedLight.price)}</span>
|
||||
{selectedLight.vat && (
|
||||
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
||||
(incl. {selectedLight.vat}% MwSt.,*)
|
||||
</small>
|
||||
)}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
@@ -1114,11 +1164,15 @@ class GrowTentKonfigurator extends Component {
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={`Belüftung: ${selectedVentilation.name}`}
|
||||
secondary={selectedVentilation.description}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
||||
{this.formatPrice(selectedVentilation.price)}
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
||||
<span>{this.formatPrice(selectedVentilation.price)}</span>
|
||||
{selectedVentilation.vat && (
|
||||
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
||||
(incl. {selectedVentilation.vat}% MwSt.,*)
|
||||
</small>
|
||||
)}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
@@ -1128,11 +1182,15 @@ class GrowTentKonfigurator extends Component {
|
||||
<ListItem key={extra.id}>
|
||||
<ListItemText
|
||||
primary={`Extra: ${extra.name}`}
|
||||
secondary={extra.description}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
||||
{this.formatPrice(extra.price)}
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
||||
<span>{this.formatPrice(extra.price)}</span>
|
||||
{extra.vat && (
|
||||
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>
|
||||
(incl. {extra.vat}% MwSt.,*)
|
||||
</small>
|
||||
)}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
@@ -1146,6 +1204,9 @@ class GrowTentKonfigurator extends Component {
|
||||
<Typography variant="h6" sx={{ color: '#d32f2f', fontWeight: 'bold' }}>
|
||||
Sie sparen: {this.formatPrice(savingsInfo.savings)} ({savingsInfo.discountPercentage}% Bundle-Rabatt)
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#77aa77' }}>
|
||||
(incl. 19% MwSt.,*)
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -1153,9 +1214,14 @@ class GrowTentKonfigurator extends Component {
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>
|
||||
Gesamtpreis:
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{this.formatPrice(totalPrice)}
|
||||
</Typography>
|
||||
<Box sx={{ textAlign: 'right' }}>
|
||||
<Typography variant="h5" sx={{ color: '#2e7d32', fontWeight: 'bold' }}>
|
||||
{this.formatPrice(totalPrice)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#77aa77' }}>
|
||||
(incl. 19% MwSt.,*)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||
|
||||
Reference in New Issue
Block a user