932 lines
40 KiB
TypeScript
932 lines
40 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
StyleSheet,
|
|
TextInput as RNTextInput,
|
|
} from "react-native";
|
|
import { useNavigation, useFocusEffect } from "@react-navigation/native";
|
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import {
|
|
getMyOrders,
|
|
getOrderTracking,
|
|
getOrderETA,
|
|
confirmReception,
|
|
cancelCommand,
|
|
respondToAddressProposal,
|
|
updateOwnCommandAddress,
|
|
formatOrderDate,
|
|
formatPrice,
|
|
calculateOrderTotal,
|
|
getPublicSettings,
|
|
} from "../../api/api";
|
|
import type {
|
|
OrderDetail,
|
|
TrackingResponse,
|
|
ETAResponse,
|
|
CancelCommandResponse,
|
|
} from "../../api/api_types";
|
|
import type { ClientStackParamList } from "../../navigation/types";
|
|
import StatusBadge from "../../components/StatusBadge";
|
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
|
import Button from "../../components/ui/Button";
|
|
import Modal from "../../components/ui/Modal";
|
|
import Toast from "../../components/ui/Toast";
|
|
import { useTheme } from "../../context/ThemeContext";
|
|
import {
|
|
spacing,
|
|
borderRadius,
|
|
fontSize,
|
|
fontWeight,
|
|
shadows,
|
|
} from "../../theme";
|
|
|
|
type Nav = NativeStackNavigationProp<ClientStackParamList>;
|
|
|
|
const STATUS_PROGRESS: Record<string, number> = {
|
|
pending: 15,
|
|
assigned: 25,
|
|
en_route: 60,
|
|
arrived: 85,
|
|
livre: 95,
|
|
approved: 100,
|
|
cancelled: 0,
|
|
};
|
|
|
|
export default function OrderTrackingScreen() {
|
|
const navigation = useNavigation<Nav>();
|
|
const { colors } = useTheme();
|
|
const [orders, setOrders] = useState<OrderDetail[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
const [tracking, setTracking] = useState<Record<number, TrackingResponse>>(
|
|
{},
|
|
);
|
|
const [etas, setEtas] = useState<Record<number, ETAResponse>>({});
|
|
const [confirmingId, setConfirmingId] = useState<number | null>(null);
|
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
|
const [cancellingId, setCancellingId] = useState<number | null>(null);
|
|
const [cancelReason, setCancelReason] = useState("");
|
|
const [cancelLoading, setCancelLoading] = useState(false);
|
|
const [penaltyWarning, setPenaltyWarning] =
|
|
useState<CancelCommandResponse | null>(null);
|
|
const [penaltyOrderId, setPenaltyOrderId] = useState<number | null>(null);
|
|
const [penaltiesEnabled, setPenaltiesEnabled] = useState(false);
|
|
const [editingAddressId, setEditingAddressId] = useState<number | null>(
|
|
null,
|
|
);
|
|
const [newAddress, setNewAddress] = useState("");
|
|
const [editAddressLoading, setEditAddressLoading] = useState(false);
|
|
const [toastMsg, setToastMsg] = useState("");
|
|
const [toastType, setToastType] = useState<
|
|
"success" | "error" | "warning" | "info"
|
|
>("success");
|
|
const [toastVisible, setToastVisible] = useState(false);
|
|
|
|
const showToast = (
|
|
msg: string,
|
|
type: "success" | "error" | "warning" | "info" = "success",
|
|
) => {
|
|
setToastMsg(msg);
|
|
setToastType(type);
|
|
setToastVisible(true);
|
|
};
|
|
|
|
const fetchOrders = useCallback(async () => {
|
|
try {
|
|
const res = await getMyOrders();
|
|
if (res.success) setOrders(res.commands || []);
|
|
} catch {
|
|
/* ignore */
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
getPublicSettings()
|
|
.then((s) => setPenaltiesEnabled(s.penalties_enabled))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
setLoading(true);
|
|
fetchOrders();
|
|
}, [fetchOrders]),
|
|
);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(fetchOrders, 10000);
|
|
return () => clearInterval(interval);
|
|
}, [fetchOrders]);
|
|
|
|
useEffect(() => {
|
|
if (expandedId === null) return;
|
|
(async () => {
|
|
const [trackRes, etaRes] = await Promise.all([
|
|
getOrderTracking(expandedId),
|
|
getOrderETA(expandedId),
|
|
]);
|
|
if (trackRes.success)
|
|
setTracking((prev) => ({ ...prev, [expandedId]: trackRes }));
|
|
if (etaRes.success)
|
|
setEtas((prev) => ({ ...prev, [expandedId]: etaRes }));
|
|
})();
|
|
}, [expandedId]);
|
|
|
|
const handleConfirm = async (orderId: number) => {
|
|
setConfirmLoading(true);
|
|
try {
|
|
const res = await confirmReception(orderId);
|
|
if (res.success) {
|
|
const cat = res.category || "";
|
|
let catLabel = "";
|
|
if (cat === "total") catLabel = " (Total)";
|
|
else if (cat.includes("zipette")) catLabel = " (Zipette&Co)";
|
|
else if (cat.includes("weed") || cat.includes("hash"))
|
|
catLabel = " (Weed&Hash)";
|
|
showToast(
|
|
`Livraison confirmee ! +${res.points_earned || 0} points${catLabel}`,
|
|
"success",
|
|
);
|
|
fetchOrders();
|
|
} else {
|
|
showToast(res.message || "Erreur", "error");
|
|
}
|
|
} catch {
|
|
showToast("Erreur de confirmation", "error");
|
|
} finally {
|
|
setConfirmLoading(false);
|
|
setConfirmingId(null);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async (orderId: number, force = false) => {
|
|
setCancelLoading(true);
|
|
try {
|
|
const res = await cancelCommand(
|
|
orderId,
|
|
cancelReason || "Annulation client",
|
|
force,
|
|
);
|
|
if (res.success) {
|
|
showToast(res.message || "Commande annulee", "success");
|
|
setCancellingId(null);
|
|
setCancelReason("");
|
|
setPenaltyWarning(null);
|
|
fetchOrders();
|
|
} else if (res.warning) {
|
|
setPenaltyOrderId(orderId);
|
|
setPenaltyWarning(res);
|
|
setCancellingId(null);
|
|
} else {
|
|
showToast(res.message || "Erreur", "error");
|
|
}
|
|
} catch {
|
|
showToast("Erreur annulation", "error");
|
|
} finally {
|
|
setCancelLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleUpdateAddress = async (orderId: number) => {
|
|
if (!newAddress.trim()) return;
|
|
setEditAddressLoading(true);
|
|
try {
|
|
const res = await updateOwnCommandAddress(
|
|
orderId,
|
|
newAddress.trim(),
|
|
);
|
|
if (res.success) {
|
|
showToast("Adresse mise à jour", "success");
|
|
setEditingAddressId(null);
|
|
setNewAddress("");
|
|
fetchOrders();
|
|
} else {
|
|
showToast(res.message || "Erreur", "error");
|
|
}
|
|
} catch {
|
|
showToast("Erreur lors de la mise à jour de l'adresse", "error");
|
|
} finally {
|
|
setEditAddressLoading(false);
|
|
}
|
|
};
|
|
|
|
const styles = useMemo(
|
|
() =>
|
|
StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
|
emptyContainer: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
padding: spacing.xl,
|
|
},
|
|
emptyTitle: {
|
|
color: colors.textMuted,
|
|
fontSize: fontSize.lg,
|
|
marginTop: spacing.l,
|
|
},
|
|
list: { padding: spacing.l, paddingBottom: spacing.xxxl },
|
|
card: {
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.md,
|
|
padding: spacing.l,
|
|
marginBottom: spacing.m,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: spacing.m,
|
|
},
|
|
orderId: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.md,
|
|
fontWeight: fontWeight.semibold,
|
|
},
|
|
progressBarBg: {
|
|
height: 4,
|
|
backgroundColor: colors.bgInput,
|
|
borderRadius: 2,
|
|
marginBottom: spacing.m,
|
|
overflow: "hidden",
|
|
},
|
|
progressBarFill: {
|
|
height: "100%",
|
|
backgroundColor: colors.accent,
|
|
borderRadius: 2,
|
|
},
|
|
cardRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
marginBottom: spacing.xs,
|
|
},
|
|
cardDetail: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.sm,
|
|
marginLeft: spacing.s,
|
|
flex: 1,
|
|
},
|
|
cardFooter: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginTop: spacing.m,
|
|
paddingTop: spacing.m,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
cardTotal: {
|
|
color: colors.success,
|
|
fontSize: fontSize.lg,
|
|
fontWeight: fontWeight.bold,
|
|
},
|
|
expandedSection: {
|
|
marginTop: spacing.l,
|
|
paddingTop: spacing.l,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
trackRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: spacing.s,
|
|
marginBottom: spacing.s,
|
|
},
|
|
trackText: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.sm,
|
|
},
|
|
expandedActions: {
|
|
flexDirection: "row",
|
|
flexWrap: "wrap",
|
|
gap: spacing.s,
|
|
marginTop: spacing.l,
|
|
},
|
|
proposalBox: {
|
|
backgroundColor: colors.warning + "18",
|
|
borderWidth: 1,
|
|
borderColor: colors.warning,
|
|
borderRadius: borderRadius.md,
|
|
padding: spacing.m,
|
|
marginTop: spacing.m,
|
|
},
|
|
proposalTitle: {
|
|
color: colors.warning,
|
|
fontSize: fontSize.sm,
|
|
fontWeight: fontWeight.semibold,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
proposalAddress: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.md,
|
|
marginBottom: spacing.m,
|
|
},
|
|
proposalActions: {
|
|
flexDirection: "row",
|
|
gap: spacing.s,
|
|
},
|
|
modalBody: { gap: spacing.m },
|
|
modalText: {
|
|
color: colors.textPrimary,
|
|
fontSize: fontSize.md,
|
|
lineHeight: 22,
|
|
},
|
|
modalActions: {
|
|
flexDirection: "row",
|
|
justifyContent: "flex-end",
|
|
gap: spacing.m,
|
|
marginTop: spacing.m,
|
|
},
|
|
cancelInput: {
|
|
backgroundColor: colors.bgInput,
|
|
color: colors.textPrimary,
|
|
borderRadius: 12,
|
|
padding: spacing.m,
|
|
fontSize: fontSize.md,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
minHeight: 80,
|
|
textAlignVertical: "top",
|
|
},
|
|
penaltyContent: { gap: spacing.l },
|
|
penaltyIconContainer: { alignItems: "center" },
|
|
penaltyIconCircle: {
|
|
width: 60,
|
|
height: 60,
|
|
borderRadius: 30,
|
|
backgroundColor: "rgba(245,158,11,0.12)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
penaltyText: {
|
|
color: colors.textPrimary,
|
|
fontSize: fontSize.md,
|
|
textAlign: "center",
|
|
lineHeight: 22,
|
|
},
|
|
penaltyBadge: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: spacing.s,
|
|
backgroundColor: "rgba(245,158,11,0.1)",
|
|
paddingVertical: spacing.s,
|
|
paddingHorizontal: spacing.m,
|
|
borderRadius: 10,
|
|
alignSelf: "center",
|
|
},
|
|
penaltyDetail: {
|
|
color: colors.warning,
|
|
fontSize: fontSize.sm,
|
|
fontWeight: fontWeight.semibold,
|
|
},
|
|
}),
|
|
[colors],
|
|
);
|
|
|
|
if (loading)
|
|
return <LoadingSpinner message="Chargement des commandes..." />;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Toast
|
|
message={toastMsg}
|
|
type={toastType}
|
|
visible={toastVisible}
|
|
onHide={() => setToastVisible(false)}
|
|
/>
|
|
|
|
{orders.length === 0 ? (
|
|
<View style={styles.emptyContainer}>
|
|
<Ionicons
|
|
name="receipt-outline"
|
|
size={80}
|
|
color={colors.textMuted}
|
|
/>
|
|
<Text style={styles.emptyTitle}>
|
|
Aucune commande active
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={orders}
|
|
keyExtractor={(item) => String(item.id)}
|
|
contentContainerStyle={styles.list}
|
|
renderItem={({ item: order }) => {
|
|
const expanded = expandedId === order.id;
|
|
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];
|
|
const canConfirm = order.status === "livre";
|
|
const canCancel = [
|
|
"pending",
|
|
"assigned",
|
|
"en_route",
|
|
].includes(order.status);
|
|
const canEditAddress = [
|
|
"pending",
|
|
"assigned",
|
|
].includes(order.status);
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
activeOpacity={0.8}
|
|
onPress={() =>
|
|
setExpandedId(expanded ? null : order.id)
|
|
}
|
|
style={[styles.card, shadows.sm]}
|
|
>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.orderId}>
|
|
Commande #{order.client_order_number}
|
|
</Text>
|
|
<StatusBadge status={order.status} />
|
|
</View>
|
|
<View style={styles.progressBarBg}>
|
|
<View
|
|
style={[
|
|
styles.progressBarFill,
|
|
{ width: `${progress}%` },
|
|
]}
|
|
/>
|
|
</View>
|
|
<View style={styles.cardRow}>
|
|
<Ionicons
|
|
name="location-outline"
|
|
size={14}
|
|
color={colors.textMuted}
|
|
/>
|
|
<Text
|
|
style={styles.cardDetail}
|
|
numberOfLines={1}
|
|
>
|
|
{order.delivery_address ||
|
|
order.adresse ||
|
|
"N/A"}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.cardRow}>
|
|
<Ionicons
|
|
name="time-outline"
|
|
size={14}
|
|
color={colors.textMuted}
|
|
/>
|
|
<Text style={styles.cardDetail}>
|
|
{formatOrderDate(order.created_at)}
|
|
</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
|
|
? "chevron-up"
|
|
: "chevron-down"
|
|
}
|
|
size={18}
|
|
color={colors.textMuted}
|
|
/>
|
|
</View>
|
|
{order.address_proposal_status === "pending" &&
|
|
order.proposed_address && (
|
|
<View style={styles.proposalBox}>
|
|
<Text style={styles.proposalTitle}>
|
|
📍 Nouvelle adresse proposée
|
|
</Text>
|
|
<Text
|
|
style={styles.proposalAddress}
|
|
>
|
|
{order.proposed_address}
|
|
</Text>
|
|
<View
|
|
style={styles.proposalActions}
|
|
>
|
|
<Button
|
|
title="Accepter"
|
|
variant="success"
|
|
size="sm"
|
|
onPress={async () => {
|
|
const res =
|
|
await respondToAddressProposal(
|
|
order.id,
|
|
true,
|
|
);
|
|
if (res.success) {
|
|
showToast(
|
|
"Adresse acceptée",
|
|
"success",
|
|
);
|
|
fetchOrders();
|
|
} else {
|
|
showToast(
|
|
res.message,
|
|
"error",
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
<Button
|
|
title="Refuser"
|
|
variant="danger"
|
|
size="sm"
|
|
onPress={async () => {
|
|
const res =
|
|
await respondToAddressProposal(
|
|
order.id,
|
|
false,
|
|
);
|
|
if (res.success) {
|
|
showToast(
|
|
"Adresse refusée",
|
|
"info",
|
|
);
|
|
fetchOrders();
|
|
} else {
|
|
showToast(
|
|
res.message,
|
|
"error",
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
{expanded && (
|
|
<View style={styles.expandedSection}>
|
|
{track?.livreur_username && (
|
|
<View style={styles.trackRow}>
|
|
<Ionicons
|
|
name="bicycle-outline"
|
|
size={16}
|
|
color={colors.info}
|
|
/>
|
|
<Text style={styles.trackText}>
|
|
Livreur:{" "}
|
|
{track.livreur_username}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{order.status === "en_route" && (
|
|
<View style={styles.trackRow}>
|
|
<Ionicons
|
|
name="timer-outline"
|
|
size={16}
|
|
color={colors.warning}
|
|
/>
|
|
<Text style={styles.trackText}>
|
|
{eta?.eta_minutes != null &&
|
|
eta.eta_minutes > 0
|
|
? `Temps de livraison estimé : ~${eta.eta_minutes} min`
|
|
: "Aucune heure disponible"}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{order.status === "arrived" && (
|
|
<View style={styles.trackRow}>
|
|
<Ionicons
|
|
name="timer-outline"
|
|
size={16}
|
|
color={colors.warning}
|
|
/>
|
|
<Text style={styles.trackText}>
|
|
Temps de livraison estimé :
|
|
~
|
|
{eta?.eta_minutes != null &&
|
|
eta.eta_minutes > 0 &&
|
|
eta.eta_minutes < 5
|
|
? eta.eta_minutes
|
|
: 5}{" "}
|
|
min
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{track?.current_step && (
|
|
<View style={styles.trackRow}>
|
|
<Ionicons
|
|
name="footsteps-outline"
|
|
size={16}
|
|
color={colors.accent}
|
|
/>
|
|
<Text style={styles.trackText}>
|
|
{track.current_step}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View style={styles.expandedActions}>
|
|
<Button
|
|
title="Details"
|
|
onPress={() =>
|
|
navigation.navigate(
|
|
"OrderDetails",
|
|
{ orderId: order.id },
|
|
)
|
|
}
|
|
variant="outline"
|
|
size="sm"
|
|
/>
|
|
{canConfirm && (
|
|
<Button
|
|
title="Confirmer reception"
|
|
onPress={() =>
|
|
setConfirmingId(
|
|
order.id,
|
|
)
|
|
}
|
|
variant="success"
|
|
size="sm"
|
|
/>
|
|
)}
|
|
{canEditAddress && (
|
|
<Button
|
|
title="Modifier l'adresse"
|
|
onPress={() => {
|
|
setNewAddress(
|
|
order.delivery_address ||
|
|
order.adresse ||
|
|
"",
|
|
);
|
|
setEditingAddressId(
|
|
order.id,
|
|
);
|
|
}}
|
|
variant="outline"
|
|
size="sm"
|
|
/>
|
|
)}
|
|
{canCancel && (
|
|
<Button
|
|
title="Annuler"
|
|
onPress={() =>
|
|
setCancellingId(
|
|
order.id,
|
|
)
|
|
}
|
|
variant="danger"
|
|
size="sm"
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<Modal
|
|
visible={confirmingId !== null}
|
|
onClose={() => setConfirmingId(null)}
|
|
title="Confirmer la reception"
|
|
icon="checkmark-circle-outline"
|
|
iconColor={colors.success}
|
|
>
|
|
<View style={styles.modalBody}>
|
|
<Text style={styles.modalText}>
|
|
Confirmez-vous avoir recu votre commande #{confirmingId}{" "}
|
|
?
|
|
</Text>
|
|
<View style={styles.modalActions}>
|
|
<Button
|
|
title="Annuler"
|
|
onPress={() => setConfirmingId(null)}
|
|
variant="outline"
|
|
size="md"
|
|
/>
|
|
<Button
|
|
title="Confirmer"
|
|
onPress={() =>
|
|
confirmingId && handleConfirm(confirmingId)
|
|
}
|
|
loading={confirmLoading}
|
|
variant="success"
|
|
size="md"
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
<Modal
|
|
visible={editingAddressId !== null}
|
|
onClose={() => {
|
|
setEditingAddressId(null);
|
|
setNewAddress("");
|
|
}}
|
|
title="Modifier l'adresse de livraison"
|
|
icon="location-outline"
|
|
iconColor={colors.accent}
|
|
>
|
|
<View style={styles.modalBody}>
|
|
<Text style={styles.modalText}>
|
|
Nouvelle adresse de livraison :
|
|
</Text>
|
|
<RNTextInput
|
|
style={styles.cancelInput}
|
|
placeholder="Adresse complète"
|
|
placeholderTextColor={colors.textMuted}
|
|
value={newAddress}
|
|
onChangeText={setNewAddress}
|
|
multiline
|
|
/>
|
|
<View style={styles.modalActions}>
|
|
<Button
|
|
title="Retour"
|
|
onPress={() => {
|
|
setEditingAddressId(null);
|
|
setNewAddress("");
|
|
}}
|
|
variant="outline"
|
|
size="md"
|
|
/>
|
|
<Button
|
|
title="Enregistrer"
|
|
onPress={() =>
|
|
editingAddressId &&
|
|
handleUpdateAddress(editingAddressId)
|
|
}
|
|
loading={editAddressLoading}
|
|
variant="success"
|
|
size="md"
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
<Modal
|
|
visible={cancellingId !== null}
|
|
onClose={() => {
|
|
setCancellingId(null);
|
|
setCancelReason("");
|
|
}}
|
|
title="Annuler la commande"
|
|
icon="close-circle-outline"
|
|
iconColor={colors.danger}
|
|
>
|
|
<View style={styles.modalBody}>
|
|
{penaltiesEnabled && (
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "flex-start",
|
|
gap: 8,
|
|
backgroundColor: colors.danger + "18",
|
|
borderWidth: 1,
|
|
borderColor: colors.danger + "55",
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
marginBottom: 12,
|
|
}}
|
|
>
|
|
<Ionicons
|
|
name="warning-outline"
|
|
size={18}
|
|
color={colors.danger}
|
|
style={{ marginTop: 1 }}
|
|
/>
|
|
<Text
|
|
style={{
|
|
flex: 1,
|
|
color: colors.danger,
|
|
fontSize: 13,
|
|
lineHeight: 18,
|
|
}}
|
|
>
|
|
Attention : en cas d'annulations répétées, une
|
|
amende sera appliquée à votre compte.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<Text style={styles.modalText}>
|
|
Raison de l'annulation :
|
|
</Text>
|
|
<RNTextInput
|
|
style={styles.cancelInput}
|
|
placeholder="Raison (optionnel)"
|
|
placeholderTextColor={colors.textMuted}
|
|
value={cancelReason}
|
|
onChangeText={setCancelReason}
|
|
multiline
|
|
/>
|
|
<View style={styles.modalActions}>
|
|
<Button
|
|
title="Retour"
|
|
onPress={() => {
|
|
setCancellingId(null);
|
|
setCancelReason("");
|
|
}}
|
|
variant="outline"
|
|
size="md"
|
|
/>
|
|
<Button
|
|
title="Annuler la commande"
|
|
onPress={() =>
|
|
cancellingId && handleCancel(cancellingId)
|
|
}
|
|
loading={cancelLoading}
|
|
variant="danger"
|
|
size="md"
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
<Modal
|
|
visible={penaltyWarning !== null}
|
|
onClose={() => {
|
|
setPenaltyWarning(null);
|
|
setPenaltyOrderId(null);
|
|
}}
|
|
title="Attention - Penalite"
|
|
icon="warning-outline"
|
|
iconColor={colors.warning}
|
|
>
|
|
<View style={styles.penaltyContent}>
|
|
<View style={styles.penaltyIconContainer}>
|
|
<View style={styles.penaltyIconCircle}>
|
|
<Ionicons
|
|
name="warning"
|
|
size={32}
|
|
color={colors.warning}
|
|
/>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.penaltyText}>
|
|
{penaltyWarning?.message}
|
|
</Text>
|
|
{penaltyWarning?.penalty_warning && (
|
|
<View style={styles.penaltyBadge}>
|
|
<Ionicons
|
|
name="remove-circle-outline"
|
|
size={16}
|
|
color={colors.danger}
|
|
/>
|
|
<Text
|
|
style={[
|
|
styles.penaltyDetail,
|
|
{ color: colors.danger, fontWeight: "700" },
|
|
]}
|
|
>
|
|
Amende :{" "}
|
|
{penaltyWarning.penalty_warning.penalty_amount}{" "}
|
|
€
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View style={styles.modalActions}>
|
|
<Button
|
|
title="Retour"
|
|
onPress={() => setPenaltyWarning(null)}
|
|
variant="outline"
|
|
size="md"
|
|
/>
|
|
<Button
|
|
title="Confirmer l'annulation"
|
|
onPress={() => {
|
|
if (penaltyOrderId)
|
|
handleCancel(penaltyOrderId, true);
|
|
setPenaltyWarning(null);
|
|
setPenaltyOrderId(null);
|
|
}}
|
|
variant="danger"
|
|
size="md"
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</View>
|
|
);
|
|
}
|