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) => { export const validateCommand = async (commandId: number) => {
const { data } = await apiClient.post( const { data } = await apiClient.post(
`${V2}/admin/protected/orders/${commandId}/force-validate`, `${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 ( export const proposeAddressChangeAdmin = async (
@@ -937,6 +940,8 @@ export interface PointsReward {
type: "free_product" | "half_price_product" | "custom"; type: "free_product" | "half_price_product" | "custom";
description: string; description: string;
category_configs: RewardCategoryConfig[]; category_configs: RewardCategoryConfig[];
reward_product_id?: number;
reward_quantity?: number;
} }
export interface PointsPool { export interface PointsPool {
@@ -72,7 +72,7 @@ export default function OrderDetailScreen() {
getCommandItems(orderId), getCommandItems(orderId),
]); ]);
setCommand(cmdRes.command); setCommand(cmdRes.command);
setItems(itemsRes.items); setItems(itemsRes.items ?? []);
// If livreur assigned, fetch their location and calc route // If livreur assigned, fetch their location and calc route
const cmd = cmdRes.command; const cmd = cmdRes.command;
@@ -205,7 +205,7 @@ export default function OrderDetailScreen() {
try { try {
await deleteCommandItem(orderId, itemId); await deleteCommandItem(orderId, itemId);
const itemsRes = await getCommandItems(orderId); const itemsRes = await getCommandItems(orderId);
setItems(itemsRes.items); setItems(itemsRes.items ?? []);
const cmdRes = await getCommandByID(orderId); const cmdRes = await getCommandByID(orderId);
setCommand(cmdRes.command); setCommand(cmdRes.command);
} catch (e: any) { } catch (e: any) {
@@ -552,7 +552,7 @@ export default function OrderDetailScreen() {
); );
// Groupement des items par product_id // 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); const key = String(item.product_id || item.produit);
if (!acc[key]) acc[key] = []; if (!acc[key]) acc[key] = [];
acc[key].push(item); acc[key].push(item);
@@ -561,7 +561,7 @@ export default function OrderDetailScreen() {
const groupedList = Object.values(productGroups); const groupedList = Object.values(productGroups);
// Récapitulatif par catégorie // 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"; const cat = item.category || "Autre";
if (!acc[cat]) acc[cat] = { qty: 0, total: 0 }; if (!acc[cat]) acc[cat] = { qty: 0, total: 0 };
acc[cat].qty += item.quantite ?? 0; acc[cat].qty += item.quantite ?? 0;
@@ -24,6 +24,7 @@ import {
getCommandItems, getCommandItems,
notifyClientToDescend, notifyClientToDescend,
confirmReceptionAdmin, confirmReceptionAdmin,
validateCommand,
deleteCommand, deleteCommand,
deleteCommandItem, deleteCommandItem,
proposeAddressChangeAdmin, 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 () => { const handleProposeAddress = async () => {
if (!addressModal.commandId || !addressModal.input.trim()) return; if (!addressModal.commandId || !addressModal.input.trim()) return;
try { try {
@@ -647,14 +668,21 @@ export default function OrdersScreen() {
{/* Total + parrainage */} {/* Total + parrainage */}
<Text style={styles.cardText}> <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)} {item.total_prix.toFixed(2)}
{(item.referral_used ?? 0) > 0 && (
<Text style={{ color: colors.accent }}>
{" "}(parrainage -{item.referral_used!.toFixed(2)} )
</Text>
)}
</Text> </Text>
{(item.referral_used ?? 0) > 0 && (
<>
<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>
</>
)}
{/* Livreur */} {/* Livreur */}
{!!item.livreur_assign && ( {!!item.livreur_assign && (
@@ -775,6 +803,15 @@ export default function OrdersScreen() {
}, },
condition: item.status === "livre", 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", label: "Supprimer",
icon: "trash-outline" as keyof typeof Ionicons.glyphMap, 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}>Client: {item.username}</Text>
<Text style={styles.cardText}>Adresse: {item.adresse}</Text> <Text style={styles.cardText}>Adresse: {item.adresse}</Text>
<Text style={styles.cardText}> <Text style={styles.cardText}>
Total: {item.total_prix.toFixed(2)} {(item.referral_used ?? 0) > 0 ? "Total brut" : "Total"}: {item.total_prix.toFixed(2)}
</Text> </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 && ( {item.livreur_assign && (
<Text style={styles.cardText}> <Text style={styles.cardText}>
Livreur: {item.livreur_assign} Livreur: {item.livreur_assign}
@@ -1111,12 +1158,22 @@ export default function OrdersScreen() {
</View> </View>
)} )}
{itemsModal.commandInfo?.total_prix != null && ( {itemsModal.commandInfo?.total_prix != null && (
<Text style={styles.modalTotal}> <>
{Number( <Text style={styles.modalTotal}>
itemsModal.commandInfo.total_prix, {(itemsModal.commandInfo.referral_used ?? 0) > 0 ? "Brut : " : ""}
).toFixed(2)}{" "} {Number(itemsModal.commandInfo.total_prix).toFixed(2)}
</Text>
</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> </View>
)} )}
@@ -657,6 +657,8 @@ const EMPTY_REWARD: PointsReward = {
type: "free_product", type: "free_product",
description: "", description: "",
category_configs: [], category_configs: [],
reward_product_id: 0,
reward_quantity: 0,
}; };
// ────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────
@@ -918,6 +920,76 @@ function CentralRewardSection({
/> />
</View> </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 */} {/* Catégories éligibles */}
<View> <View>
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text> <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) && ( {(!item.items || item.items.length === 0) && (
<> <>
<Text style={styles.priceOnly}> <Text style={styles.priceOnly}>
{item.total_prix ?? 0} {(item.referral_used ?? 0) > 0 ? "Brut : " : ""}
{(item.total_prix ?? 0)}
</Text> </Text>
{(item.referral_used ?? 0) > 0 && ( {(item.referral_used ?? 0) > 0 && (
<Text <>
style={[ <Text
styles.priceOnly, style={[
{ color: colors.success, marginTop: 2 }, styles.priceOnly,
]} { color: colors.success, marginTop: 2 },
> ]}
Parrainage: - >
{(item.referral_used ?? 0).toFixed(2)} Parrainage: -{(item.referral_used ?? 0).toFixed(2)}
</Text> </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 pending = deliveries.filter((d) => d.status === "assigned").length;
const totalRevenue = deliveries const totalRevenue = deliveries
.filter((d) => d.status === "livre" || d.status === "approved") .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; 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; success: boolean;
description?: string; description?: string;
remaining_rewards?: number; remaining_rewards?: number;
product_added?: boolean;
product_name?: string;
error?: string; error?: string;
}> => { }> => {
try { try {
@@ -973,6 +975,8 @@ export const claimMyReward = async (poolKey: string): Promise<{
success: true, success: true,
description: data.description, description: data.description,
remaining_rewards: data.remaining_rewards, remaining_rewards: data.remaining_rewards,
product_added: data.product_added,
product_name: data.product_name,
}; };
} catch (e: any) { } catch (e: any) {
return { success: false, error: e?.response?.data?.error || "Erreur" }; 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 => { export const calculateOrderTotal = (order: any): number => {
let gross = 0; if (typeof order.total_prix === "number" && order.total_prix > 0) return order.total_prix;
if (typeof order.total === "number" && order.total > 0) gross = order.total; if (typeof order.total === "number" && order.total > 0) return order.total;
else if (typeof order.total_prix === "number" && order.total_prix > 0) gross = order.total_prix; if (Array.isArray(order.items) && order.items.length > 0) {
else if (Array.isArray(order.items) && order.items.length > 0) { return order.items.reduce((sum: number, item: any) => {
gross = order.items.reduce((sum: number, item: any) => {
return sum + (item.prix || item.price || 0) * (item.quantite || item.quantity || 1); return sum + (item.prix || item.price || 0) * (item.quantite || item.quantity || 1);
}, 0); }, 0);
} }
const referralUsed = typeof order.referral_used === "number" ? order.referral_used : 0; return 0;
return Math.max(0, gross - referralUsed);
}; };
@@ -96,7 +96,10 @@ export default function OrderHistoryScreen() {
const res = await claimMyReward(poolKey); const res = await claimMyReward(poolKey);
setClaimingPool(null); setClaimingPool(null);
if (res.success) { 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); }); getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); });
} else { } else {
setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" }); setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" });
@@ -385,7 +385,8 @@ export default function OrderTrackingScreen() {
contentContainerStyle={styles.list} contentContainerStyle={styles.list}
renderItem={({ item: order }) => { renderItem={({ item: order }) => {
const expanded = expandedId === order.id; 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 progress = STATUS_PROGRESS[order.status] || 0;
const track = tracking[order.id]; const track = tracking[order.id];
const eta = etas[order.id]; const eta = etas[order.id];
@@ -444,9 +445,16 @@ export default function OrderTrackingScreen() {
</Text> </Text>
</View> </View>
<View style={styles.cardFooter}> <View style={styles.cardFooter}>
<Text style={styles.cardTotal}> <View>
{formatPrice(total)} <Text style={styles.cardTotal}>
</Text> {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 <Ionicons
name={ name={
expanded expanded