chore:build
This commit is contained in:
@@ -45,7 +45,6 @@ func GetMyPointsRewards(c *gin.Context) {
|
|||||||
AllProducts bool `json:"all_products"`
|
AllProducts bool `json:"all_products"`
|
||||||
ProductIDs []int `json:"product_ids"`
|
ProductIDs []int `json:"product_ids"`
|
||||||
ProductNames []string `json:"product_names"`
|
ProductNames []string `json:"product_names"`
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PoolInfo struct {
|
type PoolInfo struct {
|
||||||
@@ -66,6 +65,11 @@ func GetMyPointsRewards(c *gin.Context) {
|
|||||||
allProductIDs = append(allProductIDs, cfg.ProductIDs...)
|
allProductIDs = append(allProductIDs, cfg.ProductIDs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, item := range reward.RewardItems {
|
||||||
|
if item.ProductID > 0 {
|
||||||
|
allProductIDs = append(allProductIDs, item.ProductID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
productNames, _ := database.GetProductNamesByIDs(allProductIDs)
|
productNames, _ := database.GetProductNamesByIDs(allProductIDs)
|
||||||
|
|
||||||
@@ -105,7 +109,6 @@ func GetMyPointsRewards(c *gin.Context) {
|
|||||||
AllProducts: cfg.AllProducts,
|
AllProducts: cfg.AllProducts,
|
||||||
ProductIDs: cfg.ProductIDs,
|
ProductIDs: cfg.ProductIDs,
|
||||||
ProductNames: names,
|
ProductNames: names,
|
||||||
Amount: cfg.Amount,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,13 +124,33 @@ func GetMyPointsRewards(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retourner la récompense sans category_configs (les configs sont par pool)
|
// Construire la liste des produits récompense avec leurs noms
|
||||||
|
type RewardItemResponse struct {
|
||||||
|
ProductID int `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
}
|
||||||
var rewardMeta gin.H
|
var rewardMeta gin.H
|
||||||
if reward != nil {
|
if reward != nil {
|
||||||
|
rewardItems := make([]RewardItemResponse, 0, len(reward.RewardItems))
|
||||||
|
for _, item := range reward.RewardItems {
|
||||||
|
if item.ProductID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := productNames[item.ProductID]
|
||||||
|
rewardItems = append(rewardItems, RewardItemResponse{
|
||||||
|
ProductID: item.ProductID,
|
||||||
|
ProductName: name,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
Price: item.Price,
|
||||||
|
})
|
||||||
|
}
|
||||||
rewardMeta = gin.H{
|
rewardMeta = gin.H{
|
||||||
"threshold": reward.Threshold,
|
"threshold": reward.Threshold,
|
||||||
"type": reward.Type,
|
"type": reward.Type,
|
||||||
"description": reward.Description,
|
"description": reward.Description,
|
||||||
|
"reward_items": rewardItems,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ type PointsTier struct {
|
|||||||
|
|
||||||
// RewardCategoryConfig définit les produits éligibles dans une catégorie pour une récompense
|
// RewardCategoryConfig définit les produits éligibles dans une catégorie pour une récompense
|
||||||
type RewardCategoryConfig struct {
|
type RewardCategoryConfig struct {
|
||||||
Category string `json:"category"` // nom de la catégorie
|
Category string `json:"category"` // nom de la catégorie
|
||||||
AllProducts bool `json:"all_products"` // true = tous les produits de la catégorie
|
AllProducts bool `json:"all_products"` // true = tous les produits de la catégorie
|
||||||
ProductIDs []int `json:"product_ids"` // IDs des produits éligibles si AllProducts = false
|
ProductIDs []int `json:"product_ids"` // IDs des produits éligibles si AllProducts = false
|
||||||
Amount float64 `json:"amount"` // valeur monétaire de la récompense pour cette catégorie (ex: 30.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RewardItem représente un produit offert lors d'une récompense, avec sa quantité et son prix associé
|
// RewardItem représente un produit offert lors d'une récompense, avec sa quantité et son prix associé
|
||||||
|
|||||||
@@ -921,7 +921,6 @@ export interface RewardCategoryConfig {
|
|||||||
category: string;
|
category: string;
|
||||||
all_products: boolean;
|
all_products: boolean;
|
||||||
product_ids: number[];
|
product_ids: number[];
|
||||||
amount: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RewardItem {
|
export interface RewardItem {
|
||||||
|
|||||||
@@ -688,7 +688,6 @@ const REWARD_ACCENT = "#f59e0b";
|
|||||||
const REWARD_TYPES: { value: PointsReward["type"]; label: string; icon: string }[] = [
|
const REWARD_TYPES: { value: PointsReward["type"]; label: string; icon: string }[] = [
|
||||||
{ value: "free_product", label: "Produit offert", icon: "gift-outline" },
|
{ value: "free_product", label: "Produit offert", icon: "gift-outline" },
|
||||||
{ value: "half_price_product", label: "Produit à -50%", icon: "pricetag-outline" },
|
{ value: "half_price_product", label: "Produit à -50%", icon: "pricetag-outline" },
|
||||||
{ value: "custom", label: "Personnalisé", icon: "star-outline" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const EMPTY_REWARD: PointsReward = {
|
const EMPTY_REWARD: PointsReward = {
|
||||||
@@ -726,27 +725,6 @@ function CategoryProductPicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
||||||
{/* Montant pour cette catégorie */}
|
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
|
||||||
<Text style={{ fontSize: 12, color: s.thresholdSep.color ?? "#888", flex: 1 }}>
|
|
||||||
Valeur du produit offert
|
|
||||||
</Text>
|
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
|
||||||
<TextInput
|
|
||||||
style={[s.thresholdInput, { width: 56, fontSize: 13 }]}
|
|
||||||
keyboardType="decimal-pad"
|
|
||||||
value={catConfig.amount > 0 ? String(catConfig.amount) : ""}
|
|
||||||
onChangeText={(v) => {
|
|
||||||
const n = parseFloat(v);
|
|
||||||
onChange({ ...catConfig, amount: isNaN(n) ? 0 : n });
|
|
||||||
}}
|
|
||||||
placeholder="0"
|
|
||||||
placeholderTextColor={colors.textMuted}
|
|
||||||
/>
|
|
||||||
<Text style={{ fontSize: 12, color: colors.textMuted }}>€</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Toggle tous / sélection */}
|
{/* Toggle tous / sélection */}
|
||||||
<View style={{ flexDirection: "row", gap: spacing.s }}>
|
<View style={{ flexDirection: "row", gap: spacing.s }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -843,7 +821,7 @@ function CentralRewardSection({
|
|||||||
|
|
||||||
const getCatConfig = (catName: string): RewardCategoryConfig =>
|
const getCatConfig = (catName: string): RewardCategoryConfig =>
|
||||||
r.category_configs.find((c) => c.category === catName) ??
|
r.category_configs.find((c) => c.category === catName) ??
|
||||||
{ category: catName, all_products: true, product_ids: [], amount: 0 };
|
{ category: catName, all_products: true, product_ids: [] };
|
||||||
|
|
||||||
const isCatSelected = (catName: string) =>
|
const isCatSelected = (catName: string) =>
|
||||||
r.category_configs.some((c) => c.category === catName);
|
r.category_configs.some((c) => c.category === catName);
|
||||||
@@ -852,7 +830,7 @@ function CentralRewardSection({
|
|||||||
if (isCatSelected(catName)) {
|
if (isCatSelected(catName)) {
|
||||||
update({ category_configs: r.category_configs.filter((c) => c.category !== catName) });
|
update({ category_configs: r.category_configs.filter((c) => c.category !== catName) });
|
||||||
} else {
|
} else {
|
||||||
update({ category_configs: [...r.category_configs, { category: catName, all_products: true, product_ids: [], amount: 0 }] });
|
update({ category_configs: [...r.category_configs, { category: catName, all_products: true, product_ids: [] }] });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -956,7 +934,8 @@ function CentralRewardSection({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Produits récompense */}
|
{/* Produits récompense — uniquement pour le type "Produit offert" */}
|
||||||
|
{r.type === "free_product" && (
|
||||||
<View>
|
<View>
|
||||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Produits ajoutés au panier</Text>
|
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Produits ajoutés au panier</Text>
|
||||||
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
||||||
@@ -1082,10 +1061,12 @@ function CentralRewardSection({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Catégories éligibles */}
|
{/* Catégories éligibles — uniquement pour le type "Produit à -50%" */}
|
||||||
|
{r.type === "half_price_product" && (
|
||||||
<View>
|
<View>
|
||||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text>
|
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles à -50%</Text>
|
||||||
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
<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, puis pour chacune choisissez tous les produits ou une sélection.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -1137,9 +1118,10 @@ function CentralRewardSection({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Récapitulatif */}
|
{/* Récapitulatif */}
|
||||||
{(r.category_configs.length > 0 || r.reward_items.length > 0) && (
|
{(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 }}>
|
<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, fontWeight: "700", color: REWARD_ACCENT }}>Récapitulatif</Text>
|
||||||
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
|
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
|
||||||
@@ -1149,17 +1131,20 @@ function CentralRewardSection({
|
|||||||
{r.description !== "" && (
|
{r.description !== "" && (
|
||||||
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>"{r.description}"</Text>
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>"{r.description}"</Text>
|
||||||
)}
|
)}
|
||||||
{r.category_configs.map((cfg) => (
|
{r.type === "half_price_product" && r.category_configs.map((cfg) => (
|
||||||
<Text key={cfg.category} style={{ fontSize: 12, color: colors.textSecondary }}>
|
<Text key={cfg.category} style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||||
• Éligible : {cfg.category}{cfg.amount > 0 ? ` (${cfg.amount}€)` : ""} — {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s)`}
|
• Éligible à -50% : {cfg.category} — {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s)`}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
{r.reward_items.filter((it) => it.product_id > 0).map((it, idx) => {
|
{r.type === "free_product" && 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);
|
const prod = Object.values(productsByCategory).flat().find((p) => p.id === it.product_id);
|
||||||
return (
|
return (
|
||||||
<Text key={idx} style={{ fontSize: 12, color: colors.textSecondary }}>
|
<View key={idx} style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||||||
🎁 {prod?.name ?? `Produit #${it.product_id}`}{it.quantity > 0 ? ` · x${it.quantity}` : ""}{it.price > 0 ? ` · ${it.price}€` : ""}
|
<Ionicons name="gift-outline" size={11} color={REWARD_ACCENT} />
|
||||||
</Text>
|
<Text style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||||
|
{prod?.name ?? `Produit #${it.product_id}`}{it.quantity > 0 ? ` · x${it.quantity}` : ""}{it.price > 0 ? ` · ${it.price}€` : ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -2162,10 +2162,18 @@ export type PointsPoolInfo = {
|
|||||||
eligible_configs: RewardCategoryConfig[];
|
eligible_configs: RewardCategoryConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RewardItemConfig = {
|
||||||
|
product_id: number;
|
||||||
|
product_name: string;
|
||||||
|
quantity: number;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type PointsRewardConfig = {
|
export type PointsRewardConfig = {
|
||||||
threshold: number;
|
threshold: number;
|
||||||
type: string;
|
type: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
reward_items: RewardItemConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMyPointsRewards = async (): Promise<{
|
export const getMyPointsRewards = async (): Promise<{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
getMyPointsRewards,
|
getMyPointsRewards,
|
||||||
claimMyReward,
|
claimMyReward,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig } from "../../api/api";
|
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig } from "../../api/api";
|
||||||
import type {
|
import type {
|
||||||
CompletedOrder,
|
CompletedOrder,
|
||||||
ClientStats,
|
ClientStats,
|
||||||
@@ -304,7 +304,19 @@ function ConsultationHistorique() {
|
|||||||
<FontAwesomeIcon icon={faTrophy} className="rewards-section-icon" />
|
<FontAwesomeIcon icon={faTrophy} className="rewards-section-icon" />
|
||||||
Récompenses
|
Récompenses
|
||||||
</p>
|
</p>
|
||||||
<p className="rewards-section-desc">{pointsRewards.reward.description}</p>
|
{pointsRewards.reward.description && (
|
||||||
|
<p className="rewards-section-desc">{pointsRewards.reward.description}</p>
|
||||||
|
)}
|
||||||
|
{(pointsRewards.reward.reward_items ?? []).filter((it: RewardItemConfig) => it.product_name).length > 0 && (
|
||||||
|
<div className="reward-eligible-cats">
|
||||||
|
{(pointsRewards.reward.reward_items ?? []).map((it: RewardItemConfig, idx: number) => (
|
||||||
|
<span key={idx} className="reward-eligible-cat">
|
||||||
|
<FontAwesomeIcon icon={faGift} style={{ marginRight: 4 }} />
|
||||||
|
{it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? ` — ${it.price}€` : ""}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="rewards-pools">
|
<div className="rewards-pools">
|
||||||
{pointsRewards.pools.map((pool) => {
|
{pointsRewards.pools.map((pool) => {
|
||||||
const threshold = pointsRewards.reward!.threshold;
|
const threshold = pointsRewards.reward!.threshold;
|
||||||
@@ -323,7 +335,7 @@ function ConsultationHistorique() {
|
|||||||
{pool.eligible_configs.flatMap((cfg) =>
|
{pool.eligible_configs.flatMap((cfg) =>
|
||||||
cfg.all_products
|
cfg.all_products
|
||||||
? [<span key={cfg.category} className="reward-eligible-cat">
|
? [<span key={cfg.category} className="reward-eligible-cat">
|
||||||
{cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""}
|
{cfg.category}
|
||||||
</span>]
|
</span>]
|
||||||
: (cfg.product_names ?? []).map((name) => (
|
: (cfg.product_names ?? []).map((name) => (
|
||||||
<span key={`${cfg.category}-${name}`} className="reward-eligible-cat">
|
<span key={`${cfg.category}-${name}`} className="reward-eligible-cat">
|
||||||
|
|||||||
@@ -936,10 +936,18 @@ export type PointsPoolInfo = {
|
|||||||
eligible_configs: RewardCategoryConfig[];
|
eligible_configs: RewardCategoryConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RewardItemConfig = {
|
||||||
|
product_id: number;
|
||||||
|
product_name: string;
|
||||||
|
quantity: number;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type PointsRewardConfig = {
|
export type PointsRewardConfig = {
|
||||||
threshold: number;
|
threshold: number;
|
||||||
type: string;
|
type: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
reward_items: RewardItemConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMyPointsRewards = async (): Promise<{
|
export const getMyPointsRewards = async (): Promise<{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
formatOrderDate,
|
formatOrderDate,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig } from "../../api/api";
|
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig } from "../../api/api";
|
||||||
import type {
|
import type {
|
||||||
CompletedOrder,
|
CompletedOrder,
|
||||||
ClientStats,
|
ClientStats,
|
||||||
@@ -482,6 +482,18 @@ export default function OrderHistoryScreen() {
|
|||||||
{pointsRewards.reward.description !== "" && (
|
{pointsRewards.reward.description !== "" && (
|
||||||
<Text style={styles.rewardsSectionDesc}>{pointsRewards.reward.description}</Text>
|
<Text style={styles.rewardsSectionDesc}>{pointsRewards.reward.description}</Text>
|
||||||
)}
|
)}
|
||||||
|
{(pointsRewards.reward.reward_items ?? []).filter((it) => it.price > 0 || it.product_name).length > 0 && (
|
||||||
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginBottom: spacing.s }}>
|
||||||
|
{(pointsRewards.reward.reward_items ?? []).map((it: RewardItemConfig, idx: number) => (
|
||||||
|
<View key={idx} style={[styles.rewardAmountBadge, { flexDirection: "row", alignItems: "center", gap: 4 }]}>
|
||||||
|
<Ionicons name="gift-outline" size={11} color="#f59e0b" />
|
||||||
|
<Text style={styles.rewardAmountText}>
|
||||||
|
{it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? ` — ${it.price}€` : ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
{pointsRewards.pools.map((pool) => {
|
{pointsRewards.pools.map((pool) => {
|
||||||
const threshold = pointsRewards.reward!.threshold;
|
const threshold = pointsRewards.reward!.threshold;
|
||||||
const progress = Math.min(1, (pool.points % threshold) / threshold);
|
const progress = Math.min(1, (pool.points % threshold) / threshold);
|
||||||
@@ -499,9 +511,7 @@ export default function OrderHistoryScreen() {
|
|||||||
{pool.eligible_configs.flatMap((cfg) =>
|
{pool.eligible_configs.flatMap((cfg) =>
|
||||||
cfg.all_products
|
cfg.all_products
|
||||||
? [<View key={cfg.category} style={styles.rewardAmountBadge}>
|
? [<View key={cfg.category} style={styles.rewardAmountBadge}>
|
||||||
<Text style={styles.rewardAmountText}>
|
<Text style={styles.rewardAmountText}>{cfg.category}</Text>
|
||||||
{cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""}
|
|
||||||
</Text>
|
|
||||||
</View>]
|
</View>]
|
||||||
: (cfg.product_names ?? []).map((name) => (
|
: (cfg.product_names ?? []).map((name) => (
|
||||||
<View key={`${cfg.category}-${name}`} style={styles.rewardAmountBadge}>
|
<View key={`${cfg.category}-${name}`} style={styles.rewardAmountBadge}>
|
||||||
|
|||||||
Reference in New Issue
Block a user