chore: add new design for new routes
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user