chore: add new design for new routes

This commit is contained in:
2026-02-25 08:45:59 +01:00
parent a6a70bab0b
commit 03cb05f3a5
7 changed files with 358 additions and 1 deletions
+27
View File
@@ -476,6 +476,33 @@ export const deleteProductMediaAdmin = async (
return { success: true, message: data.message };
};
// ============================================
// ============================================
// ADDRESSES
// ============================================
export const addAddress = async (
invalidAddress: string,
correctAddress: string,
): Promise<{ success: boolean; message: string }> => {
const { data } = await apiClient.post(`${V2}/admin/protected/add/address`, {
invalid_address: invalidAddress,
correct_address: correctAddress,
});
return { success: true, message: data.message };
};
export const deleteAddress = async (
invalidAddress: string,
correctAddress: string,
): Promise<{ success: boolean; message: string }> => {
const { data } = await apiClient.delete(
`${V2}/admin/protected/delete/address`,
{ data: { invalid_address: invalidAddress, correct_address: correctAddress } },
);
return { success: true, message: data.message };
};
// ============================================
// STATS HELPERS
// ============================================
@@ -16,6 +16,7 @@ import UsersScreen from "../screens/admin/UsersScreen";
import ProductsScreen from "../screens/admin/ProductsScreen";
import DeliveryScreen from "../screens/admin/DeliveryScreen";
import AlertsScreen from "../screens/admin/AlertsScreen";
import AddressScreen from "../screens/admin/AddressScreen";
const Tab = createBottomTabNavigator<AdminTabParamList>();
const Stack = createNativeStackNavigator<AdminStackParamList>();
@@ -155,6 +156,20 @@ function AdminTabs() {
),
}}
/>
<Tab.Screen
name="Addresses"
component={AddressScreen}
options={{
title: "Adresses",
tabBarIcon: ({ color, size }) => (
<Ionicons
name="map-outline"
size={size}
color={color}
/>
),
}}
/>
</Tab.Navigator>
);
}
+1
View File
@@ -12,6 +12,7 @@ export type AdminTabParamList = {
Products: undefined;
Delivery: undefined;
Alerts: undefined;
Addresses: undefined;
};
export type AdminStackParamList = {
@@ -0,0 +1,217 @@
import React, { useState, useCallback, useMemo } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { addAddress, deleteAddress } from "../../api/api_admin";
import { useAlert } from "../../hooks/useAlert";
import Card from "../../components/ui/Card";
import Modal from "../../components/ui/Modal";
import TextInput from "../../components/ui/TextInput";
import Button from "../../components/ui/Button";
import AlertModal from "../../components/ui/AlertModal";
type AddressEntry = {
invalid_address: string;
correct_address: string;
};
export default function AddressScreen() {
const { colors } = useTheme();
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
const [addresses, setAddresses] = useState<AddressEntry[]>([]);
const [addModal, setAddModal] = useState(false);
const [invalidInput, setInvalidInput] = useState("");
const [correctInput, setCorrectInput] = useState("");
const [saving, setSaving] = useState(false);
const openAddModal = () => {
setInvalidInput("");
setCorrectInput("");
setAddModal(true);
};
const handleAdd = useCallback(async () => {
const inv = invalidInput.trim();
const cor = correctInput.trim();
if (!inv || !cor) {
showError("Erreur", "Les deux champs sont obligatoires.");
return;
}
setSaving(true);
try {
await addAddress(inv, cor);
setAddresses((prev) => [
...prev,
{ invalid_address: inv, correct_address: cor },
]);
setAddModal(false);
showSuccess("Succès", "Adresse ajoutée avec succès.");
} catch (e: any) {
showError("Erreur", e.response?.data?.error ?? e.message);
} finally {
setSaving(false);
}
}, [invalidInput, correctInput, showError, showSuccess]);
const handleDelete = useCallback(
(item: AddressEntry) => {
showConfirm(
"Supprimer",
`Supprimer la correction :\n"${item.invalid_address}" → "${item.correct_address}" ?`,
async () => {
try {
await deleteAddress(item.invalid_address, item.correct_address);
setAddresses((prev) =>
prev.filter(
(a) =>
a.invalid_address !== item.invalid_address ||
a.correct_address !== item.correct_address,
),
);
showSuccess("Supprimé", "Adresse supprimée.");
} catch (e: any) {
showError("Erreur", e.response?.data?.error ?? e.message);
}
},
"Supprimer",
);
},
[showConfirm, showError, showSuccess],
);
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: spacing.l,
paddingBottom: spacing.s,
},
title: {
color: colors.textWhite,
fontSize: fontSize.lg,
fontWeight: "700",
},
addBtn: {
backgroundColor: colors.accent,
borderRadius: 20,
padding: spacing.s,
},
invalid: {
color: colors.danger,
fontSize: fontSize.sm,
fontWeight: "600",
},
correct: {
color: colors.success,
fontSize: fontSize.sm,
marginTop: 4,
},
arrow: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginVertical: 2,
},
row: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
empty: {
color: colors.textMuted,
textAlign: "center",
marginTop: spacing.xxl,
fontSize: fontSize.md,
},
}),
[colors],
);
const renderItem = ({ item }: { item: AddressEntry }) => (
<Card style={{ marginBottom: spacing.m }}>
<View style={styles.row}>
<View style={{ flex: 1, marginRight: spacing.m }}>
<Text style={styles.invalid}>{item.invalid_address}</Text>
<Text style={styles.arrow}></Text>
<Text style={styles.correct}>{item.correct_address}</Text>
</View>
<TouchableOpacity onPress={() => handleDelete(item)}>
<Ionicons name="trash-outline" size={22} color={colors.danger} />
</TouchableOpacity>
</View>
</Card>
);
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Corrections d'adresses</Text>
<TouchableOpacity style={styles.addBtn} onPress={openAddModal}>
<Ionicons name="add" size={22} color={colors.textWhite} />
</TouchableOpacity>
</View>
<FlatList
data={addresses}
keyExtractor={(item, i) => `${item.invalid_address}-${i}`}
renderItem={renderItem}
contentContainerStyle={{ padding: spacing.l, paddingTop: spacing.s }}
ListEmptyComponent={
<Text style={styles.empty}>
Aucune correction.{"\n"}Appuyez sur + pour en ajouter une.
</Text>
}
/>
<Modal
visible={addModal}
onClose={() => setAddModal(false)}
title="Ajouter une correction"
icon="map-outline"
>
<TextInput
label="Adresse invalide"
value={invalidInput}
onChangeText={setInvalidInput}
placeholder="Ex: 10 rue de la paix"
autoCapitalize="none"
/>
<TextInput
label="Adresse correcte"
value={correctInput}
onChangeText={setCorrectInput}
placeholder="Ex: 10 Rue de la Paix, 75001 Paris"
autoCapitalize="none"
/>
<Button
title="Ajouter"
onPress={handleAdd}
loading={saving}
style={{ marginTop: spacing.m }}
/>
</Modal>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
onConfirm={alert.onConfirm}
confirmText={alert.confirmText}
cancelText={alert.cancelText}
/>
</View>
);
}
+11 -1
View File
@@ -306,9 +306,19 @@ export const checkoutCart = async (
queue_info: data.queue_info,
};
} catch (error: any) {
const errMsg: string = error.response?.data?.error || "Erreur serveur";
if (errMsg.startsWith("Adresse invalide ")) {
const suggested = errMsg.replace("Adresse invalide ", "").trim();
return {
success: false,
invalid_address: true,
suggested_address: suggested,
message: errMsg,
};
}
return {
success: false,
message: error.response?.data?.error || "Erreur serveur",
message: errMsg,
};
}
};
+2
View File
@@ -728,6 +728,8 @@ export interface CheckoutCartData {
export interface CheckoutCartResponse {
success: boolean;
message: string;
invalid_address?: boolean;
suggested_address?: string;
command_id?: number;
delivery_address?: string;
command?: {
@@ -34,6 +34,8 @@ export default function CheckoutScreen() {
const [error, setError] = useState<string | null>(null);
const [showConfirmation, setShowConfirmation] = useState(false);
const [confirmationData, setConfirmationData] = useState<any>(null);
const [invalidAddressModal, setInvalidAddressModal] = useState(false);
const [suggestedAddress, setSuggestedAddress] = useState("");
const handleCheckout = async () => {
if (!nom.trim()) {
@@ -70,6 +72,9 @@ export default function CheckoutScreen() {
setConfirmationData(res);
setShowConfirmation(true);
await refreshCart();
} else if (res.invalid_address && res.suggested_address) {
setSuggestedAddress(res.suggested_address);
setInvalidAddressModal(true);
} else {
setError(res.message || "Erreur lors de la commande");
}
@@ -153,6 +158,39 @@ export default function CheckoutScreen() {
fontSize: fontSize.sm,
textAlign: "center",
},
invalidAddrContent: { gap: spacing.m },
invalidAddrIconContainer: { alignItems: "center" },
invalidAddrIconCircle: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: "rgba(251,191,36,0.12)",
justifyContent: "center",
alignItems: "center",
},
invalidAddrLabel: {
color: colors.textSecondary,
fontSize: fontSize.sm,
textAlign: "center",
},
invalidAddrSuggestion: {
backgroundColor: colors.bgInput,
borderRadius: 10,
padding: spacing.m,
borderWidth: 1,
borderColor: colors.borderLight,
},
invalidAddrSuggestionText: {
color: colors.textPrimary,
fontSize: fontSize.md,
fontWeight: fontWeight.semibold,
textAlign: "center",
},
invalidAddrActions: {
flexDirection: "row",
gap: spacing.m,
marginTop: spacing.s,
},
confirmContent: { gap: spacing.m },
confirmIconContainer: { alignItems: "center" },
confirmIconCircle: {
@@ -354,6 +392,53 @@ export default function CheckoutScreen() {
/>
</ScrollView>
<Modal
visible={invalidAddressModal}
onClose={() => setInvalidAddressModal(false)}
title="Adresse invalide"
icon="warning-outline"
iconColor={colors.warning}
>
<View style={styles.invalidAddrContent}>
<View style={styles.invalidAddrIconContainer}>
<View style={styles.invalidAddrIconCircle}>
<Ionicons
name="location-outline"
size={32}
color={colors.warning}
/>
</View>
</View>
<Text style={styles.invalidAddrLabel}>
L'adresse saisie n'est pas reconnue. Voulez-vous utiliser l'adresse correcte suggérée ?
</Text>
<View style={styles.invalidAddrSuggestion}>
<Text style={styles.invalidAddrSuggestionText}>
{suggestedAddress}
</Text>
</View>
<View style={styles.invalidAddrActions}>
<Button
title="Modifier"
variant="outline"
size="md"
onPress={() => setInvalidAddressModal(false)}
style={{ flex: 1 }}
/>
<Button
title="Utiliser"
variant="success"
size="md"
onPress={() => {
setAddress(suggestedAddress);
setInvalidAddressModal(false);
}}
style={{ flex: 1 }}
/>
</View>
</View>
</Modal>
<Modal
visible={showConfirmation}
onClose={handleConfirmClose}