chore: build
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user