chore: build

This commit is contained in:
Xor290
2026-09-13 16:11:22 +02:00
parent 181147e3bf
commit 91745636ec
4 changed files with 148 additions and 35 deletions
@@ -1161,13 +1161,22 @@ function PromotionProductPicker({
}); });
}; };
const updateProductQuantity = (id: number, quantity: number) => { // Chaque produit peut être en promo sur plusieurs paliers de quantité en
onChange({ // même temps (ex: 1g ET 3g) — on ajoute/retire l'entrée {product_id,
...catConfig, // quantity} correspondante plutôt que de remplacer une quantité unique.
products: catConfig.products.map((pq) => const toggleProductQuantity = (id: number, quantity: number) => {
pq.product_id === id ? { ...pq, quantity } : pq const exists = catConfig.products.some((pq) => pq.product_id === id && pq.quantity === quantity);
), if (exists) {
}); onChange({
...catConfig,
products: catConfig.products.filter((pq) => !(pq.product_id === id && pq.quantity === quantity)),
});
} else {
onChange({
...catConfig,
products: [...catConfig.products, { product_id: id, quantity }],
});
}
}; };
return ( return (
@@ -1227,8 +1236,9 @@ function PromotionProductPicker({
</View> </View>
)} )}
{/* Mode "Sélection" : chaque produit choisi a sa propre quantité, {/* Mode "Sélection" : chaque produit choisi peut être en promo sur
via les paliers de prix réels du produit (pas de saisie libre) */} plusieurs paliers de quantité à la fois, via les paliers de
prix réels du produit (pas de saisie libre) */}
{!catConfig.all_products && ( {!catConfig.all_products && (
<View style={{ gap: spacing.xs }}> <View style={{ gap: spacing.xs }}>
{catProducts.length === 0 ? ( {catProducts.length === 0 ? (
@@ -1263,13 +1273,16 @@ function PromotionProductPicker({
{catConfig.products.length > 0 && ( {catConfig.products.length > 0 && (
<View style={{ gap: spacing.s, marginTop: spacing.xs }}> <View style={{ gap: spacing.s, marginTop: spacing.xs }}>
{catConfig.products.map((pq) => { {Array.from(new Set(catConfig.products.map((pq) => pq.product_id))).map((productId) => {
const prod = catProducts.find((p) => p.id === pq.product_id); const prod = catProducts.find((p) => p.id === productId);
const tiers = (prod?.prices ?? []).filter((pr) => pr.active_price !== false); const tiers = (prod?.prices ?? []).filter((pr) => pr.active_price !== false);
const selectedQuantities = catConfig.products
.filter((pq) => pq.product_id === productId)
.map((pq) => pq.quantity);
return ( return (
<View key={pq.product_id} style={{ gap: 4 }}> <View key={productId} style={{ gap: 4 }}>
<Text style={{ fontSize: 12, color: colors.textMuted }} numberOfLines={1}> <Text style={{ fontSize: 12, color: colors.textMuted }} numberOfLines={1}>
{prod?.name ?? `Produit #${pq.product_id}`} {prod?.name ?? `Produit #${productId}`}
</Text> </Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4 }}> <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4 }}>
{tiers.length === 0 ? ( {tiers.length === 0 ? (
@@ -1277,11 +1290,11 @@ function PromotionProductPicker({
Aucun palier de prix actif pour ce produit Aucun palier de prix actif pour ce produit
</Text> </Text>
) : tiers.map((tier) => { ) : tiers.map((tier) => {
const isSel = pq.quantity === tier.quantity; const isSel = selectedQuantities.includes(tier.quantity);
return ( return (
<TouchableOpacity <TouchableOpacity
key={tier.quantity} key={tier.quantity}
onPress={() => updateProductQuantity(pq.product_id, tier.quantity)} onPress={() => toggleProductQuantity(productId, tier.quantity)}
style={{ style={{
paddingHorizontal: spacing.s, paddingVertical: 3, paddingHorizontal: spacing.s, paddingVertical: 3,
borderRadius: borderRadius.sm, borderWidth: 1.5, borderRadius: borderRadius.sm, borderWidth: 1.5,
@@ -122,6 +122,13 @@
text-align: center; text-align: center;
} }
.product-price-strike {
color: var(--text-muted);
text-decoration: line-through;
font-size: 0.75em;
font-weight: 500;
}
.product-stock { .product-stock {
color: var(--text-muted); color: var(--text-muted);
font-size: clamp(0.85rem, 2.5vw, 1rem); font-size: clamp(0.85rem, 2.5vw, 1rem);
+31 -7
View File
@@ -28,7 +28,13 @@ interface ProductCardProps {
image: string; image: string;
stock: number; stock: number;
category: string; category: string;
prices?: Array<{ quantity: number; price: number; active_price?: boolean }>; prices?: Array<{
quantity: number;
price: number;
active_price?: boolean;
promo_price?: number | null;
promo_percent?: number;
}>;
hasVideo?: boolean; hasVideo?: boolean;
videoUrl?: string; // ✨ Nouveau prop pour l'URL de la vidéo videoUrl?: string; // ✨ Nouveau prop pour l'URL de la vidéo
categoryColor?: string; categoryColor?: string;
@@ -63,6 +69,11 @@ function ProductCard({
const isOutOfStock = stock === 0; const isOutOfStock = stock === 0;
const isComingSoon = coming_soon === true; const isComingSoon = coming_soon === true;
const normalizedCategory = (category || "autre").toLowerCase().trim(); const normalizedCategory = (category || "autre").toLowerCase().trim();
const firstPromoPrice =
prices?.[0]?.promo_price != null &&
prices[0].promo_price < prices[0].price
? prices[0].promo_price
: null;
const handleDetailsClick = (e: React.MouseEvent) => { const handleDetailsClick = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
@@ -171,9 +182,20 @@ function ProductCard({
<div className="product-info"> <div className="product-info">
<h3 className="product-name">{name}</h3> <h3 className="product-name">{name}</h3>
<p className="product-price"> <p className="product-price">
{price > 0 {price > 0 ? (
? `${price.toFixed(2)}` firstPromoPrice !== null ? (
: "Prix non disponible"} <>
<span className="product-price-strike">
{price.toFixed(2)}
</span>{" "}
{firstPromoPrice.toFixed(2)}
</>
) : (
`${price.toFixed(2)}`
)
) : (
"Prix non disponible"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -224,9 +246,11 @@ function ProductCard({
key={priceOption.quantity} key={priceOption.quantity}
value={priceOption.quantity} value={priceOption.quantity}
> >
{priceOption.quantity} {priceOption.promo_price != null &&
{unit} - {priceOption.price.toFixed(2)}{" "} priceOption.promo_price <
priceOption.price
? `${priceOption.quantity}${unit} - ${priceOption.promo_price.toFixed(2)} € (au lieu de ${priceOption.price.toFixed(2)} €, -${priceOption.promo_percent}%)`
: `${priceOption.quantity}${unit} - ${priceOption.price.toFixed(2)}`}
</option> </option>
))} ))}
</select> </select>
+82 -13
View File
@@ -47,6 +47,8 @@ interface ProductCardProps {
quantity: number; quantity: number;
price: number; price: number;
active_price?: boolean; active_price?: boolean;
promo_price?: number | null;
promo_percent?: number;
}>; }>;
media?: Array<{ url: string; type: string }>; media?: Array<{ url: string; type: string }>;
}; };
@@ -67,6 +69,11 @@ export default function ProductCard({
const activePrices = const activePrices =
product.prices?.filter((p) => p.active_price !== false) ?? []; product.prices?.filter((p) => p.active_price !== false) ?? [];
const firstPrice = activePrices[0]?.price ?? null; const firstPrice = activePrices[0]?.price ?? null;
const firstPromoPrice =
activePrices[0]?.promo_price != null &&
activePrices[0].promo_price < activePrices[0].price
? activePrices[0].promo_price
: null;
const imageMedia = product.media?.find((m) => m.type === "image"); const imageMedia = product.media?.find((m) => m.type === "image");
const videoMedia = product.media?.find((m) => m.type === "video"); const videoMedia = product.media?.find((m) => m.type === "video");
@@ -198,11 +205,33 @@ export default function ProductCard({
> >
{product.name} {product.name}
</Text> </Text>
<Text style={[styles.price, { color: colors.success }]}> {firstPrice !== null ? (
{firstPrice !== null firstPromoPrice !== null ? (
? `${firstPrice.toFixed(2)}` <View style={styles.priceRow}>
: "Prix non disponible"} <Text
</Text> style={[
styles.priceStrike,
{ color: colors.textMuted },
]}
>
{firstPrice.toFixed(2)}
</Text>
<Text
style={[styles.price, { color: colors.success }]}
>
{firstPromoPrice.toFixed(2)}
</Text>
</View>
) : (
<Text style={[styles.price, { color: colors.success }]}>
{firstPrice.toFixed(2)}
</Text>
)
) : (
<Text style={[styles.price, { color: colors.success }]}>
Prix non disponible
</Text>
)}
</View> </View>
<View <View
@@ -317,14 +346,39 @@ export default function ProductCard({
{p.quantity} {p.quantity}
{product.unit || "g"} {product.unit || "g"}
</Text> </Text>
<Text {p.promo_price != null &&
style={[ p.promo_price < p.price ? (
styles.pickerOptionPrice, <View style={styles.pickerPriceRow}>
{ color: catColor }, <Text
]} style={[
> styles.priceStrike,
{p.price.toFixed(2)} { color: colors.textMuted },
</Text> ]}
>
{p.price.toFixed(2)}
</Text>
<Text
style={[
styles.pickerOptionPrice,
{ color: catColor },
]}
>
{p.promo_price.toFixed(2)}
{p.promo_percent
? ` (-${p.promo_percent}%)`
: ""}
</Text>
</View>
) : (
<Text
style={[
styles.pickerOptionPrice,
{ color: catColor },
]}
>
{p.price.toFixed(2)}
</Text>
)}
</View> </View>
<Ionicons <Ionicons
name="add-circle" name="add-circle"
@@ -515,6 +569,21 @@ const styles = StyleSheet.create({
fontWeight: fontWeight.bold, fontWeight: fontWeight.bold,
textAlign: "center", textAlign: "center",
}, },
priceRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: spacing.xs,
},
pickerPriceRow: {
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
},
priceStrike: {
fontSize: fontSize.md,
textDecorationLine: "line-through",
},
quickAddSection: { padding: spacing.m, borderTopWidth: 1 }, quickAddSection: { padding: spacing.m, borderTopWidth: 1 },
quickAddBtn: { quickAddBtn: {
width: "100%", width: "100%",