add: new button in admin & cabine

This commit is contained in:
2026-03-06 13:04:34 +01:00
parent 0930950f5f
commit 35059ff79a
+274 -45
View File
@@ -6,6 +6,7 @@ import {
FlatList,
TouchableOpacity,
RefreshControl,
ScrollView,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
@@ -17,6 +18,10 @@ import {
getAvailableDeliveryPersons,
assignDeliveryPerson,
updateCommandStatus,
getCommandItems,
notifyClientToDescend,
confirmReceptionAdmin,
deleteCommand,
} from "../../api/api_admin";
import type { CommandResponse } from "../../api/types";
import type { AdminStackParamList } from "../../navigation/types";
@@ -24,6 +29,7 @@ import StatusBadge from "../../components/StatusBadge";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Modal from "../../components/ui/Modal";
import Button from "../../components/ui/Button";
import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
@@ -47,12 +53,27 @@ export default function OrdersScreen() {
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [filter, setFilter] = useState("all");
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
const [assignModal, setAssignModal] = useState<{
visible: boolean;
commandId: number | null;
}>({ visible: false, commandId: null });
const [livreurs, setLivreurs] = useState<any[]>([]);
const { alert, showError, hideAlert } = useAlert();
const [itemsModal, setItemsModal] = useState<{
visible: boolean;
commandId: number | null;
items: any[];
commandInfo: any;
clientInfo: any;
}>({
visible: false,
commandId: null,
items: [],
commandInfo: null,
clientInfo: null,
});
const loadData = useCallback(async () => {
try {
@@ -105,6 +126,75 @@ export default function OrdersScreen() {
}
};
const openItems = async (commandId: number) => {
try {
const result = await getCommandItems(commandId);
setItemsModal({
visible: true,
commandId,
items: result.items,
commandInfo: result.command_info,
clientInfo: result.client_info,
});
} catch (e: any) {
showError("Erreur", e.message);
}
};
const closeItemsModal = () =>
setItemsModal({
visible: false,
commandId: null,
items: [],
commandInfo: null,
clientInfo: null,
});
const handleNotifyClient = async (commandId: number) => {
try {
await notifyClientToDescend(commandId);
showSuccess("Notification envoyée", "Le client a été prévenu de descendre");
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleConfirmReception = (commandId: number) => {
showConfirm(
"Confirmer la commande",
`Confirmer la réception de la commande #${commandId} ?`,
async () => {
try {
const res = await confirmReceptionAdmin(commandId);
showSuccess(
"Réception confirmée",
`${res.points_earned} point(s) attribués au client ${res.client_username}`,
);
await loadData();
} catch (e: any) {
showError("Erreur", e.message);
}
},
"Confirmer",
);
};
const handleDelete = (commandId: number) => {
showConfirm(
"Supprimer",
`Supprimer la commande #${commandId} ?`,
async () => {
try {
await deleteCommand(commandId);
await loadData();
} catch (e: any) {
showError("Erreur", e.message);
}
},
"Supprimer",
);
};
const styles = useMemo(
() =>
StyleSheet.create({
@@ -132,25 +222,6 @@ export default function OrdersScreen() {
fontSize: fontSize.sm,
},
filterTextActive: { color: colors.textWhite },
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.s,
},
orderId: {
fontSize: fontSize.lg,
fontWeight: "bold",
color: colors.textWhite,
},
cardText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
@@ -161,8 +232,20 @@ export default function OrdersScreen() {
fontSize: fontSize.xs,
marginTop: spacing.s,
},
orderId: {
fontSize: fontSize.lg,
fontWeight: "bold",
color: colors.textWhite,
},
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.s,
},
actions: {
flexDirection: "row",
flexWrap: "wrap",
gap: spacing.s,
marginTop: spacing.m,
},
@@ -182,42 +265,101 @@ export default function OrdersScreen() {
gap: spacing.m,
},
livreurName: { color: colors.textWhite, fontSize: fontSize.md },
// Modal items
modalSummary: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
padding: spacing.m,
marginBottom: spacing.m,
gap: spacing.xs,
},
modalSummaryRow: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
},
modalSummaryText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
flex: 1,
},
modalTotal: {
color: colors.accent,
fontSize: fontSize.md,
fontWeight: "700",
marginTop: spacing.xs,
},
itemCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
marginBottom: spacing.s,
overflow: "hidden",
},
itemCardBody: {
padding: spacing.m,
},
itemName: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: "600",
marginBottom: 2,
},
itemMeta: {
color: colors.textMuted,
fontSize: fontSize.xs,
},
}),
[colors],
);
const renderOrder = ({ item }: { item: CommandResponse }) => (
<TouchableOpacity
style={styles.card}
onPress={() =>
navigation.navigate("OrderDetail", { orderId: item.id })
}
activeOpacity={0.7}
>
<View style={styles.cardHeader}>
<Text style={styles.orderId}>#{item.id}</Text>
<StatusBadge status={item.status} />
</View>
<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)}
</Text>
{item.livreur_assign && (
<Card style={{ marginBottom: spacing.m }}>
<TouchableOpacity
onPress={() =>
navigation.navigate("OrderDetail", { orderId: item.id })
}
activeOpacity={0.7}
>
<View style={styles.row}>
<Text style={styles.orderId}>#{item.id}</Text>
<StatusBadge status={item.status} />
</View>
<Text style={styles.cardText}>Client: {item.username}</Text>
<Text style={styles.cardText}>Adresse: {item.adresse}</Text>
<Text style={styles.cardText}>
Livreur: {item.livreur_assign}
Total: {item.total_prix.toFixed(2)}
</Text>
)}
<Text style={styles.cardDate}>
{new Date(item.created_at).toLocaleString("fr-FR")}
</Text>
{item.livreur_assign && (
<Text style={styles.cardText}>
Livreur: {item.livreur_assign}
</Text>
)}
<Text style={styles.cardDate}>
{new Date(item.created_at).toLocaleString("fr-FR")}
</Text>
</TouchableOpacity>
<View style={styles.actions}>
<Button
title="Voir items"
onPress={() => openItems(item.id)}
size="sm"
variant="primary"
/>
{!["approved", "cancelled"].includes(item.status) && (
<Button
title="Le client est là"
onPress={() => handleNotifyClient(item.id)}
size="sm"
variant="warning"
/>
)}
{!["approved", "cancelled"].includes(item.status) && (
<Button
title="Assigner"
onPress={() => openAssignModal(item.id)}
size="sm"
variant="primary"
variant="secondary"
/>
)}
{item.status === "assigned" && (
@@ -228,8 +370,22 @@ export default function OrdersScreen() {
variant="secondary"
/>
)}
{item.status === "livre" && (
<Button
title="Confirmer"
onPress={() => handleConfirmReception(item.id)}
size="sm"
variant="success"
/>
)}
<Button
title="Supprimer"
onPress={() => handleDelete(item.id)}
size="sm"
variant="danger"
/>
</View>
</TouchableOpacity>
</Card>
);
if (loading) return <LoadingSpinner message="Chargement commandes..." />;
@@ -277,12 +433,81 @@ export default function OrdersScreen() {
<Text style={styles.empty}>Aucune commande</Text>
}
/>
{/* Modal items */}
<Modal
visible={itemsModal.visible}
onClose={closeItemsModal}
title={`Commande #${itemsModal.commandId}`}
icon="receipt-outline"
>
<ScrollView showsVerticalScrollIndicator={false} bounces={false}>
{(itemsModal.commandInfo || itemsModal.clientInfo) && (
<View style={styles.modalSummary}>
{itemsModal.clientInfo?.username && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="person-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.modalSummaryText}>
{itemsModal.clientInfo.prenom}{" "}
{itemsModal.clientInfo.nom} ·{" "}
{itemsModal.clientInfo.username}
</Text>
</View>
)}
{(itemsModal.commandInfo?.address ||
itemsModal.commandInfo?.adresse) && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="location-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.modalSummaryText}>
{itemsModal.commandInfo.address ||
itemsModal.commandInfo.adresse}
</Text>
</View>
)}
{itemsModal.commandInfo?.total_prix != null && (
<Text style={styles.modalTotal}>
{Number(
itemsModal.commandInfo.total_prix,
).toFixed(2)}{" "}
</Text>
)}
</View>
)}
{itemsModal.items.map((item: any) => (
<View key={item.id} style={styles.itemCard}>
<View style={styles.itemCardBody}>
<Text style={styles.itemName}>
{item.produit ?? item.product_name}
</Text>
<Text style={styles.itemMeta}>
Qté: {item.quantite ?? item.quantity} ·{" "}
{(item.prix ?? item.price)?.toFixed(2)}
</Text>
</View>
</View>
))}
{itemsModal.items.length === 0 && (
<Text style={styles.empty}>Aucun item</Text>
)}
</ScrollView>
</Modal>
{/* Modal assignation */}
<Modal
visible={assignModal.visible}
onClose={() =>
setAssignModal({ visible: false, commandId: null })
}
title="Assigner un livreur"
title={`Assigner commande #${assignModal.commandId}`}
icon="bicycle-outline"
>
{livreurs.map((l) => (
@@ -303,12 +528,16 @@ export default function OrdersScreen() {
<Text style={styles.empty}>Aucun livreur disponible</Text>
)}
</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>
);