chore: build
Backend - Build & Lint / build (push) Failing after 28m7s
Frontend Admin - EAS Build / build (push) Failing after 1h39m13s
Frontend Client - EAS Build / build (push) Failing after 1h38m12s
Frontend Web - Build & Lint / build (push) Failing after 10m56s

This commit is contained in:
Xor290
2026-08-19 17:49:04 +02:00
parent 1623a9eafd
commit 901d013830
33 changed files with 1459 additions and 263 deletions
+1 -1
View File
@@ -1109,6 +1109,7 @@ export interface PointsTier {
export interface RewardCategoryConfig {
category: string;
type: "free_product" | "half_price_product";
all_products: boolean;
product_ids: number[];
}
@@ -1121,7 +1122,6 @@ export interface RewardItem {
export interface PointsReward {
threshold: number;
type: "free_product" | "half_price_product" | "custom";
description: string;
category_configs: RewardCategoryConfig[];
reward_items: RewardItem[];
@@ -686,14 +686,13 @@ function TiersSection({
const REWARD_ACCENT = "#f59e0b";
const REWARD_TYPES: { value: PointsReward["type"]; label: string; icon: string }[] = [
const REWARD_TYPES: { value: RewardCategoryConfig["type"]; label: string; icon: string }[] = [
{ value: "free_product", label: "Produit offert", icon: "gift-outline" },
{ value: "half_price_product", label: "Produit à -50%", icon: "pricetag-outline" },
];
const EMPTY_REWARD: PointsReward = {
threshold: 20,
type: "free_product",
description: "",
category_configs: [],
reward_items: [],
@@ -822,7 +821,7 @@ function CentralRewardSection({
const getCatConfig = (catName: string): RewardCategoryConfig =>
r.category_configs.find((c) => c.category === catName) ??
{ category: catName, all_products: true, product_ids: [] };
{ category: catName, type: "free_product", all_products: true, product_ids: [] };
const isCatSelected = (catName: string) =>
r.category_configs.some((c) => c.category === catName);
@@ -831,7 +830,7 @@ function CentralRewardSection({
if (isCatSelected(catName)) {
update({ category_configs: r.category_configs.filter((c) => c.category !== catName) });
} else {
update({ category_configs: [...r.category_configs, { category: catName, all_products: true, product_ids: [] }] });
update({ category_configs: [...r.category_configs, { category: catName, type: "free_product", all_products: true, product_ids: [] }] });
}
};
@@ -894,34 +893,6 @@ function CentralRewardSection({
</View>
</View>
{/* Type */}
<View>
<Text style={[s.rowLabel, { marginBottom: spacing.s }]}>Type de récompense</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.s }}>
{REWARD_TYPES.map((rt) => {
const sel = r.type === rt.value;
return (
<TouchableOpacity
key={rt.value}
onPress={() => update({ type: rt.value })}
style={{
flexDirection: "row", alignItems: "center", gap: spacing.xs,
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
borderRadius: borderRadius.sm, borderWidth: 1.5,
borderColor: sel ? REWARD_ACCENT : colors.border,
backgroundColor: sel ? REWARD_ACCENT + "20" : "transparent",
}}
>
<Ionicons name={rt.icon as any} size={14} color={sel ? REWARD_ACCENT : colors.textMuted} />
<Text style={{ fontSize: 13, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
{rt.label}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
{/* Description */}
<View>
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Description affichée au client</Text>
@@ -935,12 +906,12 @@ function CentralRewardSection({
/>
</View>
{/* Produits récompense — uniquement pour le type "Produit offert" */}
{r.type === "free_product" && (
{/* Produits récompense — proposés au client selon le type choisi
pour la catégorie de chaque produit (voir "Catégories éligibles" ci-dessous) */}
<View>
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Produits ajoutés au panier</Text>
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
Quand le client réclame sa récompense, ces produits sont automatiquement ajoutés à son panier (gratuits). Il doit commander au moins un produit normal.
Quand le client réclame sa récompense, ces produits sont automatiquement ajoutés à son panier gratuits ou à -50% selon le type configuré pour la catégorie du produit. Il doit commander au moins un produit normal.
</Text>
<View style={{ gap: spacing.s }}>
{r.reward_items.map((item, idx) => {
@@ -1062,14 +1033,14 @@ function CentralRewardSection({
)}
</View>
</View>
)}
{/* Catégories éligibles — uniquement pour le type "Produit à -50%" */}
{r.type === "half_price_product" && (
{/* Catégories éligibles — chaque catégorie choisit son propre type
(produit offert ou -50%), qui s'applique aux produits récompense
de cette catégorie configurés ci-dessus */}
<View>
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles à -50%</Text>
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text>
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
Sélectionnez les catégories, puis pour chacune choisissez tous les produits ou une sélection.
Sélectionnez les catégories, choisissez le type de récompense pour chacune, puis tous les produits ou une sélection.
</Text>
{allCategories.length === 0 ? (
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucune catégorie disponible</Text>
@@ -1103,15 +1074,41 @@ function CentralRewardSection({
/>
</TouchableOpacity>
{/* Sélecteur produits (visible si catégorie sélectionnée) */}
{/* Type + sélecteur produits (visible si catégorie sélectionnée) */}
{selected && (
<CategoryProductPicker
catConfig={getCatConfig(cat.name)}
products={productsByCategory[cat.name] ?? []}
onChange={updateCatConfig}
colors={colors}
s={s}
/>
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
{REWARD_TYPES.map((rt) => {
const cfg = getCatConfig(cat.name);
const sel = (cfg.type || "free_product") === rt.value;
return (
<TouchableOpacity
key={rt.value}
onPress={() => updateCatConfig({ ...cfg, type: rt.value })}
style={{
flexDirection: "row", alignItems: "center", gap: 4,
paddingHorizontal: spacing.s, paddingVertical: 4,
borderRadius: borderRadius.sm, borderWidth: 1.5,
borderColor: sel ? REWARD_ACCENT : colors.border,
backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
}}
>
<Ionicons name={rt.icon as any} size={12} color={sel ? REWARD_ACCENT : colors.textMuted} />
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
{rt.label}
</Text>
</TouchableOpacity>
);
})}
</View>
<CategoryProductPicker
catConfig={getCatConfig(cat.name)}
products={productsByCategory[cat.name] ?? []}
onChange={updateCatConfig}
colors={colors}
s={s}
/>
</View>
)}
</View>
);
@@ -1119,25 +1116,23 @@ function CentralRewardSection({
</View>
)}
</View>
)}
{/* Récapitulatif */}
{(r.category_configs.length > 0 || r.reward_items.filter((it) => it.product_id > 0).length > 0) && (
<View style={{ backgroundColor: REWARD_ACCENT + "12", borderRadius: borderRadius.sm, borderLeftWidth: 3, borderLeftColor: REWARD_ACCENT, padding: spacing.m, gap: 4 }}>
<Text style={{ fontSize: 13, fontWeight: "700", color: REWARD_ACCENT }}>Récapitulatif</Text>
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
Dès <Text style={{ fontWeight: "700" }}>{r.threshold} pts</Text> par type {" "}
{REWARD_TYPES.find((x) => x.value === r.type)?.label}
Dès <Text style={{ fontWeight: "700" }}>{r.threshold} pts</Text> par type de points récompense débloquée
</Text>
{r.description !== "" && (
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>"{r.description}"</Text>
)}
{r.type === "half_price_product" && r.category_configs.map((cfg) => (
{r.category_configs.map((cfg) => (
<Text key={cfg.category} style={{ fontSize: 12, color: colors.textSecondary }}>
Éligible à -50% : {cfg.category} {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s)`}
{REWARD_TYPES.find((x) => x.value === (cfg.type || "free_product"))?.label} : {cfg.category} {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s)`}
</Text>
))}
{r.type === "free_product" && r.reward_items.filter((it) => it.product_id > 0).map((it, idx) => {
{r.reward_items.filter((it) => it.product_id > 0).map((it, idx) => {
const prod = Object.values(productsByCategory).flat().find((p) => p.id === it.product_id);
return (
<View key={idx} style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
@@ -715,7 +715,7 @@ export default function DashboardScreen() {
{prod.is_reward && (
<View style={{ flexDirection: "row", alignItems: "center", gap: 2, backgroundColor: "rgba(245,158,11,0.15)", borderRadius: 4, paddingHorizontal: 4, paddingVertical: 1 }}>
<Ionicons name="gift-outline" size={10} color="#f59e0b" />
<Text style={{ fontSize: 10, color: "#f59e0b", fontWeight: "700" }}>Offert</Text>
<Text style={{ fontSize: 10, color: "#f59e0b", fontWeight: "700" }}>Récompense</Text>
</View>
)}
</View>
@@ -724,7 +724,7 @@ export default function DashboardScreen() {
</Text>
</View>
<Text style={[styles.itemPrice, prod.is_reward ? { color: "#10b981" } : {}]}>
{prod.is_reward ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}`}
{prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}`}
</Text>
</View>
))}
@@ -2018,7 +2018,7 @@ export default function DashboardScreen() {
{prod.is_reward && (
<View style={{ flexDirection: "row", alignItems: "center", gap: 2, backgroundColor: "rgba(245,158,11,0.15)", borderRadius: 4, paddingHorizontal: 4, paddingVertical: 1 }}>
<Ionicons name="gift-outline" size={11} color="#f59e0b" />
<Text style={{ fontSize: 11, color: "#f59e0b", fontWeight: "700" }}>Offert</Text>
<Text style={{ fontSize: 11, color: "#f59e0b", fontWeight: "700" }}>Récompense</Text>
</View>
)}
</View>
@@ -2027,7 +2027,7 @@ export default function DashboardScreen() {
</Text>
</View>
<Text style={[styles.detailProductPrice, prod.is_reward ? { color: "#10b981" } : {}]}>
{prod.is_reward ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}`}
{prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}`}
</Text>
</View>
))}