diff --git a/omnex-plateform-app/App.tsx b/omnex-plateform-app/App.tsx
index e936e22..7be8b7b 100644
--- a/omnex-plateform-app/App.tsx
+++ b/omnex-plateform-app/App.tsx
@@ -108,8 +108,8 @@ export default function App() {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
- } catch {
- // Silently ignore update errors
+ } catch (e) {
+ console.error("[OTA] Échec de la vérification/application de la mise à jour:", e);
}
};
checkForUpdate();
diff --git a/omnex-plateform-app/src/api/api_admin.ts b/omnex-plateform-app/src/api/api_admin.ts
index c442501..615905c 100644
--- a/omnex-plateform-app/src/api/api_admin.ts
+++ b/omnex-plateform-app/src/api/api_admin.ts
@@ -1107,24 +1107,23 @@ export interface PointsTier {
points: number;
}
+export interface RewardProductQuantity {
+ product_id: number;
+ quantity: number; // quantité individuelle de ce produit (palier de prix catalogue, ex: 1g)
+}
+
export interface RewardCategoryConfig {
category: string;
type: "free_product" | "half_price_product";
all_products: boolean;
- product_ids: number[];
-}
-
-export interface RewardItem {
- product_id: number;
- quantity: number;
- price: number;
+ quantity: number; // quantité uniforme si all_products = true
+ products: RewardProductQuantity[]; // produits + quantité individuelle si all_products = false
}
export interface PointsReward {
threshold: number;
description: string;
category_configs: RewardCategoryConfig[];
- reward_items: RewardItem[];
}
export interface PointsPool {
diff --git a/omnex-plateform-app/src/api/tomtom.ts b/omnex-plateform-app/src/api/tomtom.ts
index 2a3bb8f..56b96e4 100644
--- a/omnex-plateform-app/src/api/tomtom.ts
+++ b/omnex-plateform-app/src/api/tomtom.ts
@@ -123,7 +123,6 @@ export async function calculateRoute(
}
}
- // Fallback: straight line
if (coordinates.length === 0) {
coordinates.push(origin, destination);
}
diff --git a/omnex-plateform-app/src/navigation/DeliveryNavigator.tsx b/omnex-plateform-app/src/navigation/DeliveryNavigator.tsx
index 9888260..a7697cc 100644
--- a/omnex-plateform-app/src/navigation/DeliveryNavigator.tsx
+++ b/omnex-plateform-app/src/navigation/DeliveryNavigator.tsx
@@ -218,6 +218,7 @@ export default function DeliveryNavigator() {
/>
+ {/* Modal notifications */}
setRatingsModal(null)}
>
- setRatingsModal(null)}>
- {}}>
-
-
-
- Avis — {ratingsModal?.username}
-
- setRatingsModal(null)}>
-
-
-
- {ratingsLoading ? (
- Chargement...
- ) : ratingsModal && ratingsModal.count > 0 ? (
- <>
-
- {[1,2,3,4,5].map((s) => (
-
- ))}
-
- {ratingsModal.average.toFixed(1)}
-
-
- ({ratingsModal.count} avis)
-
-
-
- {ratingsModal.ratings.map((r) => (
-
-
- {r.client_username}
-
- {new Date(r.created_at).toLocaleDateString("fr-FR")}
-
-
-
- {[1,2,3,4,5].map((s) => (
-
- ))}
-
- {r.comment !== "" && (
- "{r.comment}"
- )}
-
- ))}
-
- >
- ) : (
- Aucun avis pour ce livreur
- )}
+
+
+
+
+ Avis — {ratingsModal?.username}
+
+ setRatingsModal(null)}>
+
+
-
-
+ {ratingsLoading ? (
+ Chargement...
+ ) : ratingsModal && ratingsModal.count > 0 ? (
+ <>
+
+ {[1,2,3,4,5].map((s) => (
+
+ ))}
+
+ {ratingsModal.average.toFixed(1)}
+
+
+ ({ratingsModal.count} avis)
+
+
+
+ {ratingsModal.ratings.map((r) => (
+
+
+ {r.client_username}
+
+ {new Date(r.created_at).toLocaleDateString("fr-FR")}
+
+
+
+ {[1,2,3,4,5].map((s) => (
+
+ ))}
+
+ {r.comment !== "" && (
+ "{r.comment}"
+ )}
+
+ ))}
+
+ >
+ ) : (
+ Aucun avis pour ce livreur
+ )}
+
+
{/* ── Modal historique de connexion livreur ── */}
@@ -1148,12 +1148,8 @@ export default function DeliveryScreen() {
animationType="slide"
onRequestClose={() => setLoginHistoryModal(null)}
>
- setLoginHistoryModal(null)}
- >
- {}}>
-
+
+
Connexions — {loginHistoryModal?.username}
@@ -1258,7 +1254,7 @@ export default function DeliveryScreen() {
) : loginHistoryModal &&
loginHistoryModal.weeks.length > 0 ? (
-
+
{loginHistoryModal.weeks.map((week) => (
@@ -1310,9 +1306,8 @@ export default function DeliveryScreen() {
Aucune connexion ce mois-ci
)}
-
-
-
+
+
);
diff --git a/omnex-plateform-app/src/screens/admin/SettingsScreen.tsx b/omnex-plateform-app/src/screens/admin/SettingsScreen.tsx
index e58d03a..278fdfb 100644
--- a/omnex-plateform-app/src/screens/admin/SettingsScreen.tsx
+++ b/omnex-plateform-app/src/screens/admin/SettingsScreen.tsx
@@ -17,7 +17,7 @@ import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, getAllProductsAdmin, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
-import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, RewardItem, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
+import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
import type { Product } from "../../api/types";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
@@ -49,7 +49,7 @@ const DAYS: { key: keyof DeliverySchedule; label: string }[] = [
// ──────────────────────────────────────────────────────────────
// Composant section accordéon générique
-// ──────────────────────────────────────────────────────────────
+
function AccordionSection({
title,
badge,
@@ -695,7 +695,6 @@ const EMPTY_REWARD: PointsReward = {
threshold: 20,
description: "",
category_configs: [],
- reward_items: [],
};
// ──────────────────────────────────────────────────────────────
@@ -717,10 +716,29 @@ function CategoryProductPicker({
const catProducts = products.filter((p) => p.category === catConfig.category);
const toggleProduct = (id: number) => {
- const ids = catConfig.product_ids.includes(id)
- ? catConfig.product_ids.filter((x) => x !== id)
- : [...catConfig.product_ids, id];
- onChange({ ...catConfig, product_ids: ids, all_products: false });
+ const exists = catConfig.products.some((pq) => pq.product_id === id);
+ if (exists) {
+ onChange({ ...catConfig, products: catConfig.products.filter((pq) => pq.product_id !== id), all_products: false });
+ return;
+ }
+ // Présélectionne le premier palier de prix actif du produit, plutôt
+ // qu'une valeur arbitraire qui pourrait ne correspondre à aucun palier réel.
+ const prod = catProducts.find((p) => p.id === id);
+ const firstTier = (prod?.prices ?? []).find((pr) => pr.active_price !== false);
+ onChange({
+ ...catConfig,
+ products: [...catConfig.products, { product_id: id, quantity: firstTier?.quantity ?? 0 }],
+ all_products: false,
+ });
+ };
+
+ const updateProductQuantity = (id: number, quantity: number) => {
+ onChange({
+ ...catConfig,
+ products: catConfig.products.map((pq) =>
+ pq.product_id === id ? { ...pq, quantity } : pq
+ ),
+ });
};
return (
@@ -728,7 +746,7 @@ function CategoryProductPicker({
{/* Toggle tous / sélection */}
onChange({ ...catConfig, all_products: true, product_ids: [] })}
+ onPress={() => onChange({ ...catConfig, all_products: true, products: [] })}
style={{
flexDirection: "row", alignItems: "center", gap: spacing.xs,
paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
@@ -759,34 +777,103 @@ function CategoryProductPicker({
- {/* Liste des produits si mode sélection */}
+ {/* Mode "Tous" : une quantité uniforme pour tous les produits de la catégorie */}
+ {catConfig.all_products && (
+
+ Quantité :
+ 0 ? String(catConfig.quantity) : ""}
+ onChangeText={(v) => {
+ const n = parseFloat(v);
+ onChange({ ...catConfig, quantity: isNaN(n) ? 0 : n });
+ }}
+ placeholder="1"
+ placeholderTextColor={colors.textMuted}
+ />
+
+ doit correspondre à un palier de prix existant
+
+
+ )}
+
+ {/* Mode "Sélection" : chaque produit choisi a sa propre quantité
+ (ex: produit A à 2g offerts, produit B à 1g offert) */}
{!catConfig.all_products && (
-
+
{catProducts.length === 0 ? (
Aucun produit dans cette catégorie
- ) : catProducts.map((p) => {
- const sel = catConfig.product_ids.includes(p.id);
- return (
- toggleProduct(p.id)}
- style={{
- paddingHorizontal: spacing.s, paddingVertical: 4,
- borderRadius: borderRadius.sm, borderWidth: 1.5,
- borderColor: sel ? REWARD_ACCENT : colors.border,
- backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
- flexDirection: "row", alignItems: "center", gap: 4,
- }}
- >
- {sel && }
-
- {p.name}
-
-
- );
- })}
+ ) : (
+
+ {catProducts.map((p) => {
+ const sel = catConfig.products.some((pq) => pq.product_id === p.id);
+ return (
+ toggleProduct(p.id)}
+ style={{
+ paddingHorizontal: spacing.s, paddingVertical: 4,
+ borderRadius: borderRadius.sm, borderWidth: 1.5,
+ borderColor: sel ? REWARD_ACCENT : colors.border,
+ backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
+ flexDirection: "row", alignItems: "center", gap: 4,
+ }}
+ >
+ {sel && }
+
+ {p.name}
+
+
+ );
+ })}
+
+ )}
+
+ {catConfig.products.length > 0 && (
+
+ {catConfig.products.map((pq) => {
+ const prod = catProducts.find((p) => p.id === pq.product_id);
+ const tiers = (prod?.prices ?? []).filter((pr) => pr.active_price !== false);
+ return (
+
+
+ {prod?.name ?? `Produit #${pq.product_id}`}
+
+
+ {tiers.length === 0 ? (
+
+ Aucun palier de prix actif pour ce produit
+
+ ) : tiers.map((tier) => {
+ const isSel = pq.quantity === tier.quantity;
+ return (
+ updateProductQuantity(pq.product_id, tier.quantity)}
+ style={{
+ paddingHorizontal: spacing.s, paddingVertical: 3,
+ borderRadius: borderRadius.sm, borderWidth: 1.5,
+ borderColor: isSel ? REWARD_ACCENT : colors.border,
+ backgroundColor: isSel ? REWARD_ACCENT + "22" : "transparent",
+ flexDirection: "row", alignItems: "center", gap: 4,
+ }}
+ >
+ {isSel && }
+
+ {tier.quantity}{prod?.unit ?? ""} · {tier.price}€
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ )}
)}
@@ -819,29 +906,44 @@ function CentralRewardSection({
const update = (patch: Partial) =>
onChange({ ...r, ...patch });
- const getCatConfig = (catName: string): RewardCategoryConfig =>
- r.category_configs.find((c) => c.category === catName) ??
- { category: catName, type: "free_product", all_products: true, product_ids: [] };
+ // Une catégorie peut avoir jusqu'à deux configs actives en parallèle
+ // (une "free_product" et une "half_price_product"), chacune avec sa
+ // propre sélection de produits — d'où la recherche par (catégorie, type).
+ const getCatConfig = (catName: string, type: RewardCategoryConfig["type"]): RewardCategoryConfig =>
+ r.category_configs.find((c) => c.category === catName && c.type === type) ??
+ { category: catName, type, all_products: true, products: [], quantity: 1 };
+
+ const isTypeEnabled = (catName: string, type: RewardCategoryConfig["type"]) =>
+ r.category_configs.some((c) => c.category === catName && c.type === type);
const isCatSelected = (catName: string) =>
r.category_configs.some((c) => c.category === catName);
- const toggleCategory = (catName: string) => {
- if (isCatSelected(catName)) {
- update({ category_configs: r.category_configs.filter((c) => c.category !== catName) });
+ const toggleCategoryType = (catName: string, type: RewardCategoryConfig["type"]) => {
+ if (isTypeEnabled(catName, type)) {
+ update({ category_configs: r.category_configs.filter((c) => !(c.category === catName && c.type === type)) });
} else {
- update({ category_configs: [...r.category_configs, { category: catName, type: "free_product", all_products: true, product_ids: [] }] });
+ update({ category_configs: [...r.category_configs, { category: catName, type, all_products: true, products: [], quantity: 1 }] });
}
};
const updateCatConfig = (cfg: RewardCategoryConfig) => {
update({
category_configs: r.category_configs.map((c) =>
- c.category === cfg.category ? cfg : c
+ c.category === cfg.category && c.type === cfg.type ? cfg : c
),
});
};
+ const [expandedCats, setExpandedCats] = useState>(new Set());
+ const toggleExpanded = (catName: string) => {
+ setExpandedCats((prev) => {
+ const next = new Set(prev);
+ if (next.has(catName)) next.delete(catName); else next.add(catName);
+ return next;
+ });
+ };
+
const rewardBadge = (
- {/* Produits récompense — proposés au client selon le type choisi
- pour la catégorie de chaque produit (voir "Catégories éligibles" ci-dessous) */}
-
- Produits ajoutés au panier
-
- 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.
-
-
- {r.reward_items.map((item, idx) => {
- const allProds = Object.values(productsByCategory).flat();
- const prod = allProds.find((p) => p.id === item.product_id);
- return (
-
- {/* Sélecteur produit */}
-
- {allProds.map((p) => {
- const sel = item.product_id === p.id;
- return (
- {
- const updated = r.reward_items.map((it, i) =>
- i === idx ? { ...it, product_id: p.id } : it
- );
- update({ reward_items: updated });
- }}
- 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",
- }}
- >
- {sel && }
-
- {p.name} ({p.category})
-
-
- );
- })}
-
-
- {/* Quantité + Prix + Supprimer */}
-
-
- Qté :
- 0 ? String(item.quantity) : ""}
- onChangeText={(v) => {
- const n = parseFloat(v);
- const updated = r.reward_items.map((it, i) =>
- i === idx ? { ...it, quantity: isNaN(n) ? 0 : n } : it
- );
- update({ reward_items: updated });
- }}
- placeholder="1"
- placeholderTextColor={colors.textMuted}
- />
-
-
- Prix :
- 0 ? String(item.price) : ""}
- onChangeText={(v) => {
- const n = parseFloat(v);
- const updated = r.reward_items.map((it, i) =>
- i === idx ? { ...it, price: isNaN(n) ? 0 : n } : it
- );
- update({ reward_items: updated });
- }}
- placeholder="0"
- placeholderTextColor={colors.textMuted}
- />
- €
-
- update({ reward_items: r.reward_items.filter((_, i) => i !== idx) })}
- style={{ padding: spacing.xs }}
- >
-
-
-
-
- {prod && (
-
- {prod.name}{item.quantity > 0 ? ` · x${item.quantity}` : ""}{item.price > 0 ? ` · ${item.price}€` : ""}
-
- )}
- {!prod && (
-
- Sélectionnez un produit ci-dessus
-
- )}
-
- );
- })}
-
- {Object.values(productsByCategory).flat().length === 0 ? (
- Aucun produit disponible
- ) : (
- update({ reward_items: [...r.reward_items, { product_id: 0, quantity: 1, price: 0 }] })}
- style={{
- flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.xs,
- paddingHorizontal: spacing.m, paddingVertical: spacing.s,
- borderRadius: borderRadius.sm, borderWidth: 1.5,
- borderColor: REWARD_ACCENT + "66",
- }}
- >
-
- Ajouter un produit récompense
-
- )}
-
-
-
{/* 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 */}
Catégories éligibles
- Sélectionnez les catégories, choisissez le type de récompense pour chacune, puis tous les produits ou une sélection.
+ Sélectionnez une catégorie pour activer, indépendamment, un lot de produits offerts et/ou un lot de produits à -50%, chacun avec sa propre sélection de produits.
{allCategories.length === 0 ? (
Aucune catégorie disponible
@@ -1048,12 +1022,13 @@ function CentralRewardSection({
{allCategories.map((cat) => {
const selected = isCatSelected(cat.name);
+ const expanded = expandedCats.has(cat.name);
const catColor = cat.color || REWARD_ACCENT;
return (
- {/* Chip catégorie */}
+ {/* Chip catégorie (couleur = au moins un type actif, clic = déplier/replier) */}
toggleCategory(cat.name)}
+ onPress={() => toggleExpanded(cat.name)}
style={{
flexDirection: "row", alignItems: "center", gap: spacing.xs,
alignSelf: "flex-start",
@@ -1068,46 +1043,52 @@ function CentralRewardSection({
{cat.name}
- {/* Type + sélecteur produits (visible si catégorie sélectionnée) */}
- {selected && (
-
-
- {REWARD_TYPES.map((rt) => {
- const cfg = getCatConfig(cat.name);
- const sel = (cfg.type || "free_product") === rt.value;
- return (
+ {/* Les deux types de récompense, activables indépendamment */}
+ {expanded && (
+
+ {REWARD_TYPES.map((rt) => {
+ const typeEnabled = isTypeEnabled(cat.name, rt.value);
+ return (
+
updateCatConfig({ ...cfg, type: rt.value })}
+ onPress={() => toggleCategoryType(cat.name, rt.value)}
style={{
flexDirection: "row", alignItems: "center", gap: 4,
+ alignSelf: "flex-start",
paddingHorizontal: spacing.s, paddingVertical: 4,
borderRadius: borderRadius.sm, borderWidth: 1.5,
- borderColor: sel ? REWARD_ACCENT : colors.border,
- backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
+ borderColor: typeEnabled ? REWARD_ACCENT : colors.border,
+ backgroundColor: typeEnabled ? REWARD_ACCENT + "22" : "transparent",
}}
>
-
-
+
+
+
{rt.label}
- );
- })}
-
-
+ {typeEnabled && (
+
+ )}
+
+ );
+ })}
)}
@@ -1118,7 +1099,7 @@ function CentralRewardSection({
{/* Récapitulatif */}
- {(r.category_configs.length > 0 || r.reward_items.filter((it) => it.product_id > 0).length > 0) && (
+ {r.category_configs.length > 0 && (
Récapitulatif
@@ -1127,22 +1108,13 @@ function CentralRewardSection({
{r.description !== "" && (
"{r.description}"
)}
- {r.category_configs.map((cfg) => (
-
- • {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)`}
+ {r.category_configs.map((cfg, idx) => (
+
+ • {REWARD_TYPES.find((x) => x.value === (cfg.type || "free_product"))?.label} : {cfg.category} — {cfg.all_products
+ ? `tous les produits · qté ${cfg.quantity > 0 ? cfg.quantity : 1}`
+ : `${cfg.products.length} produit(s) : ${cfg.products.map((pq) => `#${pq.product_id}×${pq.quantity}`).join(", ")}`}
))}
- {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 (
-
-
-
- {prod?.name ?? `Produit #${it.product_id}`}{it.quantity > 0 ? ` · x${it.quantity}` : ""}{it.price > 0 ? ` · ${it.price}€` : ""}
-
-
- );
- })}
)}
@@ -1277,7 +1249,13 @@ export default function SettingsScreen() {
category_routes: s.delivery_mode?.category_routes ?? [],
},
points_reward: s.points_reward
- ? { ...s.points_reward, category_configs: s.points_reward.category_configs ?? [], reward_items: s.points_reward.reward_items ?? [] }
+ ? {
+ ...s.points_reward,
+ category_configs: (s.points_reward.category_configs ?? []).map((cfg) => ({
+ ...cfg,
+ products: cfg.products ?? [],
+ })),
+ }
: null,
});
}
diff --git a/omnex-plateform-client/src/api/api.ts b/omnex-plateform-client/src/api/api.ts
index 58afaa1..644e33b 100644
--- a/omnex-plateform-client/src/api/api.ts
+++ b/omnex-plateform-client/src/api/api.ts
@@ -961,13 +961,18 @@ export const toggle2FA = async (
// 🏆 POINTS — RÉCOMPENSES
// ============================================
+export type RewardConfigProduct = {
+ product_id: number;
+ product_name: string;
+ quantity: number;
+};
+
export type RewardCategoryConfig = {
category: string;
type: "free_product" | "half_price_product";
all_products: boolean;
- product_ids: number[];
- product_names: string[];
- amount: number;
+ products: RewardConfigProduct[];
+ quantity: number;
};
export type RewardItemConfig = {
diff --git a/omnex-plateform-client/src/screens/client/OrderHistoryScreen.tsx b/omnex-plateform-client/src/screens/client/OrderHistoryScreen.tsx
index f1dc37a..ee4a6bc 100644
--- a/omnex-plateform-client/src/screens/client/OrderHistoryScreen.tsx
+++ b/omnex-plateform-client/src/screens/client/OrderHistoryScreen.tsx
@@ -830,14 +830,14 @@ export default function OrderHistoryScreen() {
,
]
: (
- cfg.product_names ??
+ cfg.products ??
[]
).map(
(
- name,
+ p,
) => (
{
- name
+ p.product_name
}