chore: build
Frontend Admin - EAS Build / build (push) Successful in 1h28m12s
Frontend Client - EAS Build / build (push) Successful in 1h24m10s
Frontend Web - Build & Lint / build (push) Failing after 6m2s

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