add: new button in admin & cabine
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
|
ScrollView,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
@@ -17,6 +18,10 @@ import {
|
|||||||
getAvailableDeliveryPersons,
|
getAvailableDeliveryPersons,
|
||||||
assignDeliveryPerson,
|
assignDeliveryPerson,
|
||||||
updateCommandStatus,
|
updateCommandStatus,
|
||||||
|
getCommandItems,
|
||||||
|
notifyClientToDescend,
|
||||||
|
confirmReceptionAdmin,
|
||||||
|
deleteCommand,
|
||||||
} from "../../api/api_admin";
|
} from "../../api/api_admin";
|
||||||
import type { CommandResponse } from "../../api/types";
|
import type { CommandResponse } from "../../api/types";
|
||||||
import type { AdminStackParamList } from "../../navigation/types";
|
import type { AdminStackParamList } from "../../navigation/types";
|
||||||
@@ -24,6 +29,7 @@ import StatusBadge from "../../components/StatusBadge";
|
|||||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
import Modal from "../../components/ui/Modal";
|
import Modal from "../../components/ui/Modal";
|
||||||
import Button from "../../components/ui/Button";
|
import Button from "../../components/ui/Button";
|
||||||
|
import Card from "../../components/ui/Card";
|
||||||
import AlertModal from "../../components/ui/AlertModal";
|
import AlertModal from "../../components/ui/AlertModal";
|
||||||
import { useAlert } from "../../hooks/useAlert";
|
import { useAlert } from "../../hooks/useAlert";
|
||||||
|
|
||||||
@@ -47,12 +53,27 @@ export default function OrdersScreen() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [filter, setFilter] = useState("all");
|
const [filter, setFilter] = useState("all");
|
||||||
|
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
|
||||||
|
|
||||||
const [assignModal, setAssignModal] = useState<{
|
const [assignModal, setAssignModal] = useState<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
commandId: number | null;
|
commandId: number | null;
|
||||||
}>({ visible: false, commandId: null });
|
}>({ visible: false, commandId: null });
|
||||||
const [livreurs, setLivreurs] = useState<any[]>([]);
|
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 () => {
|
const loadData = useCallback(async () => {
|
||||||
try {
|
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(
|
const styles = useMemo(
|
||||||
() =>
|
() =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
@@ -132,25 +222,6 @@ export default function OrdersScreen() {
|
|||||||
fontSize: fontSize.sm,
|
fontSize: fontSize.sm,
|
||||||
},
|
},
|
||||||
filterTextActive: { color: colors.textWhite },
|
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: {
|
cardText: {
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
fontSize: fontSize.sm,
|
fontSize: fontSize.sm,
|
||||||
@@ -161,8 +232,20 @@ export default function OrdersScreen() {
|
|||||||
fontSize: fontSize.xs,
|
fontSize: fontSize.xs,
|
||||||
marginTop: spacing.s,
|
marginTop: spacing.s,
|
||||||
},
|
},
|
||||||
|
orderId: {
|
||||||
|
fontSize: fontSize.lg,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: colors.textWhite,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: spacing.s,
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
gap: spacing.s,
|
gap: spacing.s,
|
||||||
marginTop: spacing.m,
|
marginTop: spacing.m,
|
||||||
},
|
},
|
||||||
@@ -182,19 +265,63 @@ export default function OrdersScreen() {
|
|||||||
gap: spacing.m,
|
gap: spacing.m,
|
||||||
},
|
},
|
||||||
livreurName: { color: colors.textWhite, fontSize: fontSize.md },
|
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],
|
[colors],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderOrder = ({ item }: { item: CommandResponse }) => (
|
const renderOrder = ({ item }: { item: CommandResponse }) => (
|
||||||
|
<Card style={{ marginBottom: spacing.m }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.card}
|
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("OrderDetail", { orderId: item.id })
|
navigation.navigate("OrderDetail", { orderId: item.id })
|
||||||
}
|
}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.row}>
|
||||||
<Text style={styles.orderId}>#{item.id}</Text>
|
<Text style={styles.orderId}>#{item.id}</Text>
|
||||||
<StatusBadge status={item.status} />
|
<StatusBadge status={item.status} />
|
||||||
</View>
|
</View>
|
||||||
@@ -211,13 +338,28 @@ export default function OrdersScreen() {
|
|||||||
<Text style={styles.cardDate}>
|
<Text style={styles.cardDate}>
|
||||||
{new Date(item.created_at).toLocaleString("fr-FR")}
|
{new Date(item.created_at).toLocaleString("fr-FR")}
|
||||||
</Text>
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
<View style={styles.actions}>
|
<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) && (
|
{!["approved", "cancelled"].includes(item.status) && (
|
||||||
<Button
|
<Button
|
||||||
title="Assigner"
|
title="Assigner"
|
||||||
onPress={() => openAssignModal(item.id)}
|
onPress={() => openAssignModal(item.id)}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="secondary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.status === "assigned" && (
|
{item.status === "assigned" && (
|
||||||
@@ -228,8 +370,22 @@ export default function OrdersScreen() {
|
|||||||
variant="secondary"
|
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>
|
</View>
|
||||||
</TouchableOpacity>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner message="Chargement commandes..." />;
|
if (loading) return <LoadingSpinner message="Chargement commandes..." />;
|
||||||
@@ -277,12 +433,81 @@ export default function OrdersScreen() {
|
|||||||
<Text style={styles.empty}>Aucune commande</Text>
|
<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
|
<Modal
|
||||||
visible={assignModal.visible}
|
visible={assignModal.visible}
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
setAssignModal({ visible: false, commandId: null })
|
setAssignModal({ visible: false, commandId: null })
|
||||||
}
|
}
|
||||||
title="Assigner un livreur"
|
title={`Assigner commande #${assignModal.commandId}`}
|
||||||
icon="bicycle-outline"
|
icon="bicycle-outline"
|
||||||
>
|
>
|
||||||
{livreurs.map((l) => (
|
{livreurs.map((l) => (
|
||||||
@@ -303,12 +528,16 @@ export default function OrdersScreen() {
|
|||||||
<Text style={styles.empty}>Aucun livreur disponible</Text>
|
<Text style={styles.empty}>Aucun livreur disponible</Text>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<AlertModal
|
<AlertModal
|
||||||
visible={alert.visible}
|
visible={alert.visible}
|
||||||
type={alert.type}
|
type={alert.type}
|
||||||
title={alert.title}
|
title={alert.title}
|
||||||
message={alert.message}
|
message={alert.message}
|
||||||
onClose={hideAlert}
|
onClose={hideAlert}
|
||||||
|
onConfirm={alert.onConfirm}
|
||||||
|
confirmText={alert.confirmText}
|
||||||
|
cancelText={alert.cancelText}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user