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 };
|
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
|
// STATS HELPERS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import UsersScreen from "../screens/admin/UsersScreen";
|
|||||||
import ProductsScreen from "../screens/admin/ProductsScreen";
|
import ProductsScreen from "../screens/admin/ProductsScreen";
|
||||||
import DeliveryScreen from "../screens/admin/DeliveryScreen";
|
import DeliveryScreen from "../screens/admin/DeliveryScreen";
|
||||||
import AlertsScreen from "../screens/admin/AlertsScreen";
|
import AlertsScreen from "../screens/admin/AlertsScreen";
|
||||||
|
import AddressScreen from "../screens/admin/AddressScreen";
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator<AdminTabParamList>();
|
const Tab = createBottomTabNavigator<AdminTabParamList>();
|
||||||
const Stack = createNativeStackNavigator<AdminStackParamList>();
|
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>
|
</Tab.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type AdminTabParamList = {
|
|||||||
Products: undefined;
|
Products: undefined;
|
||||||
Delivery: undefined;
|
Delivery: undefined;
|
||||||
Alerts: undefined;
|
Alerts: undefined;
|
||||||
|
Addresses: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminStackParamList = {
|
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,
|
queue_info: data.queue_info,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} 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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: error.response?.data?.error || "Erreur serveur",
|
message: errMsg,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -728,6 +728,8 @@ export interface CheckoutCartData {
|
|||||||
export interface CheckoutCartResponse {
|
export interface CheckoutCartResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
|
invalid_address?: boolean;
|
||||||
|
suggested_address?: string;
|
||||||
command_id?: number;
|
command_id?: number;
|
||||||
delivery_address?: string;
|
delivery_address?: string;
|
||||||
command?: {
|
command?: {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export default function CheckoutScreen() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||||
const [confirmationData, setConfirmationData] = useState<any>(null);
|
const [confirmationData, setConfirmationData] = useState<any>(null);
|
||||||
|
const [invalidAddressModal, setInvalidAddressModal] = useState(false);
|
||||||
|
const [suggestedAddress, setSuggestedAddress] = useState("");
|
||||||
|
|
||||||
const handleCheckout = async () => {
|
const handleCheckout = async () => {
|
||||||
if (!nom.trim()) {
|
if (!nom.trim()) {
|
||||||
@@ -70,6 +72,9 @@ export default function CheckoutScreen() {
|
|||||||
setConfirmationData(res);
|
setConfirmationData(res);
|
||||||
setShowConfirmation(true);
|
setShowConfirmation(true);
|
||||||
await refreshCart();
|
await refreshCart();
|
||||||
|
} else if (res.invalid_address && res.suggested_address) {
|
||||||
|
setSuggestedAddress(res.suggested_address);
|
||||||
|
setInvalidAddressModal(true);
|
||||||
} else {
|
} else {
|
||||||
setError(res.message || "Erreur lors de la commande");
|
setError(res.message || "Erreur lors de la commande");
|
||||||
}
|
}
|
||||||
@@ -153,6 +158,39 @@ export default function CheckoutScreen() {
|
|||||||
fontSize: fontSize.sm,
|
fontSize: fontSize.sm,
|
||||||
textAlign: "center",
|
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 },
|
confirmContent: { gap: spacing.m },
|
||||||
confirmIconContainer: { alignItems: "center" },
|
confirmIconContainer: { alignItems: "center" },
|
||||||
confirmIconCircle: {
|
confirmIconCircle: {
|
||||||
@@ -354,6 +392,53 @@ export default function CheckoutScreen() {
|
|||||||
/>
|
/>
|
||||||
</ScrollView>
|
</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
|
<Modal
|
||||||
visible={showConfirmation}
|
visible={showConfirmation}
|
||||||
onClose={handleConfirmClose}
|
onClose={handleConfirmClose}
|
||||||
|
|||||||
Reference in New Issue
Block a user