chore: build

This commit is contained in:
2026-06-13 15:22:51 +02:00
parent dd861fd7be
commit 59d4f89a14
9 changed files with 198 additions and 41 deletions
+6 -1
View File
@@ -235,8 +235,11 @@ export const updateCommandAddress = async (
export const validateCommand = async (commandId: number) => {
const { data } = await apiClient.post(
`${V2}/admin/protected/orders/${commandId}/force-validate`,
{ command_id: commandId },
);
return { success: true, message: data.message };
const validated = (data.validated ?? []) as { command_id: number; points_awarded: number }[];
const points = validated.find((v) => v.command_id === commandId)?.points_awarded ?? 0;
return { success: true, points_awarded: points, validated_count: data.validated_count ?? 0 };
};
export const proposeAddressChangeAdmin = async (
@@ -937,6 +940,8 @@ export interface PointsReward {
type: "free_product" | "half_price_product" | "custom";
description: string;
category_configs: RewardCategoryConfig[];
reward_product_id?: number;
reward_quantity?: number;
}
export interface PointsPool {
@@ -72,7 +72,7 @@ export default function OrderDetailScreen() {
getCommandItems(orderId),
]);
setCommand(cmdRes.command);
setItems(itemsRes.items);
setItems(itemsRes.items ?? []);
// If livreur assigned, fetch their location and calc route
const cmd = cmdRes.command;
@@ -205,7 +205,7 @@ export default function OrderDetailScreen() {
try {
await deleteCommandItem(orderId, itemId);
const itemsRes = await getCommandItems(orderId);
setItems(itemsRes.items);
setItems(itemsRes.items ?? []);
const cmdRes = await getCommandByID(orderId);
setCommand(cmdRes.command);
} catch (e: any) {
@@ -552,7 +552,7 @@ export default function OrderDetailScreen() {
);
// Groupement des items par product_id
const productGroups = items.reduce<Record<string, any[]>>((acc, item) => {
const productGroups = (items ?? []).reduce<Record<string, any[]>>((acc, item) => {
const key = String(item.product_id || item.produit);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
@@ -561,7 +561,7 @@ export default function OrderDetailScreen() {
const groupedList = Object.values(productGroups);
// Récapitulatif par catégorie
const categoryTotals = items.reduce<Record<string, { qty: number; total: number }>>((acc, item) => {
const categoryTotals = (items ?? []).reduce<Record<string, { qty: number; total: number }>>((acc, item) => {
const cat = item.category || "Autre";
if (!acc[cat]) acc[cat] = { qty: 0, total: 0 };
acc[cat].qty += item.quantite ?? 0;
@@ -24,6 +24,7 @@ import {
getCommandItems,
notifyClientToDescend,
confirmReceptionAdmin,
validateCommand,
deleteCommand,
deleteCommandItem,
proposeAddressChangeAdmin,
@@ -240,6 +241,26 @@ export default function OrdersScreen() {
);
};
const handleForceValidate = (commandId: number) => {
showConfirm(
"Finaliser la commande",
`Finaliser définitivement la commande #${commandId} et attribuer les points au client ?`,
async () => {
try {
const res = await validateCommand(commandId);
showSuccess(
"Commande finalisée",
`${res.points_awarded} point(s) attribués au client`,
);
await loadData();
} catch (e: any) {
showError("Erreur", e.message);
}
},
"Finaliser",
);
};
const handleProposeAddress = async () => {
if (!addressModal.commandId || !addressModal.input.trim()) return;
try {
@@ -647,14 +668,21 @@ export default function OrdersScreen() {
{/* Total + parrainage */}
<Text style={styles.cardText}>
<Text style={{ color: colors.textMuted }}>Total : </Text>
<Text style={{ color: colors.textMuted }}>
{(item.referral_used ?? 0) > 0 ? "Total brut : " : "Total : "}
</Text>
{item.total_prix.toFixed(2)}
</Text>
{(item.referral_used ?? 0) > 0 && (
<Text style={{ color: colors.accent }}>
{" "}(parrainage -{item.referral_used!.toFixed(2)} )
<>
<Text style={[styles.cardText, { color: colors.success }]}>
Parrainage : -{item.referral_used!.toFixed(2)}
</Text>
<Text style={[styles.cardText, { fontWeight: "700" }]}>
Net : {(item.total_prix - item.referral_used!).toFixed(2)}
</Text>
</>
)}
</Text>
{/* Livreur */}
{!!item.livreur_assign && (
@@ -775,6 +803,15 @@ export default function OrdersScreen() {
},
condition: item.status === "livre",
},
{
label: "Finaliser commande",
icon: "trophy-outline" as keyof typeof Ionicons.glyphMap,
onPress: () => {
setOpenMenuId(null);
handleForceValidate(item.id);
},
condition: !isDone && item.status !== "livre",
},
{
label: "Supprimer",
icon: "trash-outline" as keyof typeof Ionicons.glyphMap,
@@ -802,8 +839,18 @@ export default function OrdersScreen() {
<Text style={styles.cardText}>Client: {item.username}</Text>
<Text style={styles.cardText}>Adresse: {item.adresse}</Text>
<Text style={styles.cardText}>
Total: {item.total_prix.toFixed(2)}
{(item.referral_used ?? 0) > 0 ? "Total brut" : "Total"}: {item.total_prix.toFixed(2)}
</Text>
{(item.referral_used ?? 0) > 0 && (
<>
<Text style={[styles.cardText, { color: colors.success }]}>
Parrainage: -{(item.referral_used ?? 0).toFixed(2)}
</Text>
<Text style={[styles.cardText, { fontWeight: "700" }]}>
Net: {(item.total_prix - (item.referral_used ?? 0)).toFixed(2)}
</Text>
</>
)}
{item.livreur_assign && (
<Text style={styles.cardText}>
Livreur: {item.livreur_assign}
@@ -1111,12 +1158,22 @@ export default function OrdersScreen() {
</View>
)}
{itemsModal.commandInfo?.total_prix != null && (
<>
<Text style={styles.modalTotal}>
{Number(
itemsModal.commandInfo.total_prix,
).toFixed(2)}{" "}
{(itemsModal.commandInfo.referral_used ?? 0) > 0 ? "Brut : " : ""}
{Number(itemsModal.commandInfo.total_prix).toFixed(2)}
</Text>
{(itemsModal.commandInfo.referral_used ?? 0) > 0 && (
<>
<Text style={[styles.modalTotal, { color: colors.success, fontSize: 13 }]}>
Parrainage : -{Number(itemsModal.commandInfo.referral_used).toFixed(2)}
</Text>
<Text style={[styles.modalTotal, { fontWeight: "700" }]}>
Net : {(Number(itemsModal.commandInfo.total_prix) - Number(itemsModal.commandInfo.referral_used)).toFixed(2)}
</Text>
</>
)}
</>
)}
</View>
)}
@@ -657,6 +657,8 @@ const EMPTY_REWARD: PointsReward = {
type: "free_product",
description: "",
category_configs: [],
reward_product_id: 0,
reward_quantity: 0,
};
// ──────────────────────────────────────────────────────────────
@@ -918,6 +920,76 @@ function CentralRewardSection({
/>
</View>
{/* Produit récompense */}
<View>
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Produit ajouté au panier</Text>
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
Quand le client réclame sa récompense, ce produit est automatiquement ajouté à son panier (gratuit). Il doit commander au moins un produit normal.
</Text>
{/* Sélecteur produit */}
<View style={{ gap: spacing.s }}>
{Object.values(productsByCategory).flat().length === 0 ? (
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucun produit disponible</Text>
) : (
<View style={{ gap: spacing.xs }}>
<TouchableOpacity
onPress={() => update({ reward_product_id: 0, reward_quantity: 0 })}
style={{
flexDirection: "row", alignItems: "center", gap: spacing.xs,
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
borderRadius: borderRadius.sm, borderWidth: 1.5,
borderColor: !r.reward_product_id ? colors.border : "transparent",
backgroundColor: !r.reward_product_id ? colors.border + "30" : "transparent",
}}
>
<Text style={{ fontSize: 13, color: !r.reward_product_id ? colors.textPrimary : colors.textMuted }}>
Aucun (désactivé)
</Text>
</TouchableOpacity>
{Object.values(productsByCategory).flat().map((prod) => {
const sel = r.reward_product_id === prod.id;
return (
<TouchableOpacity
key={prod.id}
onPress={() => update({ reward_product_id: prod.id, reward_quantity: r.reward_quantity && r.reward_quantity > 0 ? r.reward_quantity : 1 })}
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 + "18" : "transparent",
}}
>
<Ionicons name="gift-outline" size={14} color={sel ? REWARD_ACCENT : colors.textMuted} />
<Text style={{ flex: 1, fontSize: 13, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
{prod.name} ({prod.category})
</Text>
{sel && <Ionicons name="checkmark-circle" size={16} color={REWARD_ACCENT} />}
</TouchableOpacity>
);
})}
</View>
)}
{/* Quantité */}
{!!r.reward_product_id && (
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m, marginTop: spacing.s }}>
<Text style={[s.rowLabel, { flex: 1 }]}>Quantité offerte</Text>
<TextInput
style={[s.thresholdInput, { width: 64 }]}
keyboardType="decimal-pad"
value={r.reward_quantity ? String(r.reward_quantity) : ""}
onChangeText={(v) => {
const n = parseFloat(v);
if (!isNaN(n) && n > 0) update({ reward_quantity: n });
}}
placeholder="1"
placeholderTextColor={colors.textMuted}
/>
</View>
)}
</View>
</View>
{/* Catégories éligibles */}
<View>
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text>
@@ -756,18 +756,28 @@ export default function DashboardScreen() {
{(!item.items || item.items.length === 0) && (
<>
<Text style={styles.priceOnly}>
{item.total_prix ?? 0}
{(item.referral_used ?? 0) > 0 ? "Brut : " : ""}
{(item.total_prix ?? 0)}
</Text>
{(item.referral_used ?? 0) > 0 && (
<>
<Text
style={[
styles.priceOnly,
{ color: colors.success, marginTop: 2 },
]}
>
Parrainage: -
{(item.referral_used ?? 0).toFixed(2)}
Parrainage: -{(item.referral_used ?? 0).toFixed(2)}
</Text>
<Text
style={[
styles.priceOnly,
{ fontWeight: "700", marginTop: 2 },
]}
>
Net à encaisser: {((item.total_prix ?? 0) - (item.referral_used ?? 0)).toFixed(2)}
</Text>
</>
)}
</>
)}
@@ -139,7 +139,7 @@ export default function StatsScreen() {
const pending = deliveries.filter((d) => d.status === "assigned").length;
const totalRevenue = deliveries
.filter((d) => d.status === "livre" || d.status === "approved")
.reduce((s, d) => s + d.total_prix, 0);
.reduce((s, d) => s + (d.total_prix - (d.referral_used ?? 0)), 0);
const chartData = period === "day" ? byDay : period === "week" ? byWeek : byMonth;
+9 -7
View File
@@ -965,6 +965,8 @@ export const claimMyReward = async (poolKey: string): Promise<{
success: boolean;
description?: string;
remaining_rewards?: number;
product_added?: boolean;
product_name?: string;
error?: string;
}> => {
try {
@@ -973,6 +975,8 @@ export const claimMyReward = async (poolKey: string): Promise<{
success: true,
description: data.description,
remaining_rewards: data.remaining_rewards,
product_added: data.product_added,
product_name: data.product_name,
};
} catch (e: any) {
return { success: false, error: e?.response?.data?.error || "Erreur" };
@@ -980,14 +984,12 @@ export const claimMyReward = async (poolKey: string): Promise<{
};
export const calculateOrderTotal = (order: any): number => {
let gross = 0;
if (typeof order.total === "number" && order.total > 0) gross = order.total;
else if (typeof order.total_prix === "number" && order.total_prix > 0) gross = order.total_prix;
else if (Array.isArray(order.items) && order.items.length > 0) {
gross = order.items.reduce((sum: number, item: any) => {
if (typeof order.total_prix === "number" && order.total_prix > 0) return order.total_prix;
if (typeof order.total === "number" && order.total > 0) return order.total;
if (Array.isArray(order.items) && order.items.length > 0) {
return order.items.reduce((sum: number, item: any) => {
return sum + (item.prix || item.price || 0) * (item.quantite || item.quantity || 1);
}, 0);
}
const referralUsed = typeof order.referral_used === "number" ? order.referral_used : 0;
return Math.max(0, gross - referralUsed);
return 0;
};
@@ -96,7 +96,10 @@ export default function OrderHistoryScreen() {
const res = await claimMyReward(poolKey);
setClaimingPool(null);
if (res.success) {
setClaimFeedback({ pool: poolKey, type: "success", text: res.description || "Récompense réclamée !" });
const text = res.product_added && res.product_name
? `🎁 ${res.product_name} ajouté à votre panier ! Commandez au moins un produit pour en profiter.`
: res.description || "Récompense réclamée !";
setClaimFeedback({ pool: poolKey, type: "success", text });
getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); });
} else {
setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" });
@@ -385,7 +385,8 @@ export default function OrderTrackingScreen() {
contentContainerStyle={styles.list}
renderItem={({ item: order }) => {
const expanded = expandedId === order.id;
const total = calculateOrderTotal(order);
const gross = calculateOrderTotal(order);
const total = Math.max(0, gross - (order.referral_used ?? 0));
const progress = STATUS_PROGRESS[order.status] || 0;
const track = tracking[order.id];
const eta = etas[order.id];
@@ -444,9 +445,16 @@ export default function OrderTrackingScreen() {
</Text>
</View>
<View style={styles.cardFooter}>
<View>
<Text style={styles.cardTotal}>
{formatPrice(total)}
</Text>
{(order.referral_used ?? 0) > 0 && (
<Text style={{ fontSize: 11, color: colors.success, marginTop: 2 }}>
dont -{(order.referral_used as number).toFixed(2)} parrainage
</Text>
)}
</View>
<Ionicons
name={
expanded