Files
projet_gestion_commande/frontend-admin/src/screens/admin/OrderDetailScreen.tsx
T
2026-06-14 18:09:44 +02:00

1046 lines
42 KiB
TypeScript

import React, { useState, useEffect, useRef, useMemo } from "react";
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Modal,
Alert,
useWindowDimensions,
} from "react-native";
import {
useRoute,
useNavigation,
type RouteProp,
} from "@react-navigation/native";
import { Ionicons } from "@expo/vector-icons";
import TomTomMap, { type TomTomMapRef, type TomTomMarker } from "../../components/TomTomMap";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import {
getCommandByID,
getCommandItems,
updateCommandStatus,
validateCommand,
notifyClientToDescend,
getDeliveryPersonDetails,
deleteCommandItem,
deleteCommand,
} from "../../api/api_admin";
import { calculateRoute, geocodeAddress } from "../../api/tomtom";
import type { RouteInfo, LatLng } from "../../api/tomtom";
import type { AdminStackParamList } from "../../navigation/types";
import StatusBadge from "../../components/StatusBadge";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Button from "../../components/ui/Button";
import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
type Route = RouteProp<AdminStackParamList, "OrderDetail">;
export default function OrderDetailScreen() {
const { colors } = useTheme();
const { height: screenHeight } = useWindowDimensions();
const MAP_HEIGHT = screenHeight < 700 ? 180 : 240;
const route = useRoute<Route>();
const navigation = useNavigation();
const { orderId } = route.params;
const [command, setCommand] = useState<any>(null);
const [items, setItems] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showItemsModal, setShowItemsModal] = useState(false);
// Map / tracking
const mapRef = useRef<TomTomMapRef | null>(null);
const fullscreenMapRef = useRef<TomTomMapRef | null>(null);
const [mapFullscreen, setMapFullscreen] = useState(false);
const [livreurCoords, setLivreurCoords] = useState<LatLng | null>(null);
const [destCoords, setDestCoords] = useState<LatLng | null>(null);
const [livreurMarkers, setLivreurMarkers] = useState<TomTomMarker[]>([]);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [mapLoading, setMapLoading] = useState(false);
const { alert, showError, showSuccess, hideAlert } = useAlert();
useEffect(() => {
const load = async () => {
try {
const [cmdRes, itemsRes] = await Promise.all([
getCommandByID(orderId),
getCommandItems(orderId),
]);
setCommand(cmdRes.command);
setItems(itemsRes.items ?? []);
// If livreur assigned, fetch their location and calc route
const cmd = cmdRes.command;
if (cmd?.livreur_assign && cmd?.adresse) {
loadLivreurRoute(cmd.livreur_assign, cmd.adresse);
}
} catch {
/* ignore */
}
setLoading(false);
};
load();
}, [orderId]);
useEffect(() => {
const interval = setInterval(async () => {
try {
const res = await getCommandByID(orderId);
setCommand(res.command);
} catch {
/* ignore */
}
}, 20000);
return () => clearInterval(interval);
}, [orderId]);
// Rejoue la route sur la carte fullscreen quand elle s'ouvre
useEffect(() => {
if (mapFullscreen && livreurCoords && destCoords) {
setTimeout(() => {
fullscreenMapRef.current?.calcRoute(livreurCoords, destCoords);
}, 600);
}
}, [mapFullscreen]);
const loadLivreurRoute = async (
livreurUsername: string,
deliveryAddress: string,
) => {
setMapLoading(true);
try {
const details = await getDeliveryPersonDetails(livreurUsername);
const loc = details?.location;
if (!loc?.latitude || !loc?.longitude) {
setMapLoading(false);
return;
}
const origin: LatLng = { latitude: loc.latitude, longitude: loc.longitude };
setLivreurCoords(origin);
setLivreurMarkers([{
id: livreurUsername,
latitude: loc.latitude,
longitude: loc.longitude,
color: "#22c55e",
label: livreurUsername,
description: "Livreur",
}]);
// Dessine la route sur la carte et récupère les infos (distance/durée)
const dest = await geocodeAddress(deliveryAddress);
if (dest) {
setDestCoords(dest);
const result = await calculateRoute(origin, dest);
if (result) {
setRouteInfo(result.route);
mapRef.current?.calcRoute(origin, dest);
}
}
} catch {
/* silent */
}
setMapLoading(false);
};
const handleValidate = async () => {
try {
await validateCommand(orderId);
showSuccess("Succès", "Commande validée");
const res = await getCommandByID(orderId);
setCommand(res.command);
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleStatusChange = async (status: string) => {
try {
await updateCommandStatus(orderId, status);
const res = await getCommandByID(orderId);
setCommand(res.command);
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleNotifyClient = async () => {
try {
await notifyClientToDescend(orderId);
showSuccess(
"Notification envoyée",
"Le client a été prévenu de descendre",
);
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleDeleteItem = (itemId: number, itemName: string) => {
Alert.alert(
"Supprimer l'article",
`Supprimer "${itemName}" de la commande ?`,
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: async () => {
try {
await deleteCommandItem(orderId, itemId);
const itemsRes = await getCommandItems(orderId);
setItems(itemsRes.items ?? []);
const cmdRes = await getCommandByID(orderId);
setCommand(cmdRes.command);
} catch (e: any) {
showError("Erreur", e.message);
}
},
},
],
);
};
const handleDeleteCommand = () => {
Alert.alert(
"Supprimer la commande",
`Supprimer définitivement la commande #${orderId} ?`,
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: async () => {
try {
await deleteCommand(orderId);
navigation.goBack();
} catch (e: any) {
showError("Erreur", e.message);
}
},
},
],
);
};
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.s,
},
title: {
fontSize: fontSize.xl,
fontWeight: "bold",
color: colors.textWhite,
},
info: {
color: colors.textSecondary,
fontSize: fontSize.sm,
marginTop: 2,
},
date: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginTop: spacing.s,
},
sectionTitle: {
fontSize: fontSize.lg,
fontWeight: "600",
color: colors.textWhite,
marginTop: spacing.xl,
marginBottom: spacing.m,
},
itemName: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: "600",
marginBottom: spacing.xs,
},
itemDetails: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
itemDeleteBtn: {
padding: spacing.xs,
marginLeft: spacing.s,
},
actions: { marginTop: spacing.m, marginBottom: spacing.xxl },
empty: {
color: colors.textMuted,
textAlign: "center",
marginTop: spacing.xxl,
fontSize: fontSize.md,
},
// Map
mapContainer: {
borderRadius: borderRadius.md,
overflow: "hidden",
position: "relative",
},
map: { width: "100%", height: MAP_HEIGHT },
routeOverlay: {
position: "absolute",
top: spacing.s,
left: spacing.s,
backgroundColor: "rgba(0,0,0,0.75)",
borderRadius: borderRadius.sm,
padding: spacing.s,
paddingHorizontal: spacing.m,
},
routeOverlayUser: {
color: colors.white,
fontSize: fontSize.sm,
fontWeight: "700",
},
routeOverlayInfo: {
color: colors.accent,
fontSize: fontSize.xs,
marginTop: 2,
},
mapLoadingOverlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.3)",
justifyContent: "center",
alignItems: "center",
},
mapLoadingText: {
color: colors.white,
fontSize: fontSize.sm,
fontWeight: "600",
},
expandBtn: {
position: "absolute",
top: spacing.s,
right: spacing.s,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.6)",
justifyContent: "center",
alignItems: "center",
},
noMapBox: {
alignItems: "center",
paddingVertical: spacing.l,
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
},
noMapText: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginTop: spacing.xs,
},
// Grouped items
subItemRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: spacing.xs,
paddingLeft: spacing.m,
borderLeftWidth: 2,
borderLeftColor: colors.border,
marginLeft: spacing.xs,
marginBottom: 2,
},
subItemText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
flex: 1,
},
groupTotalRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: spacing.s,
paddingTop: spacing.s,
borderTopWidth: 1,
borderTopColor: colors.border,
},
groupTotalQty: {
color: colors.textWhite,
fontSize: fontSize.sm,
fontWeight: "700",
},
groupTotalPrice: {
color: colors.accent,
fontSize: fontSize.md,
fontWeight: "700",
},
categoryBadge: {
fontSize: fontSize.xs,
color: colors.accent,
fontWeight: "600",
marginRight: spacing.s,
textTransform: "uppercase",
},
// Category summary
categorySummaryCard: {
marginTop: spacing.s,
},
categoryRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: spacing.s,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
categoryName: {
color: colors.textSecondary,
fontSize: fontSize.sm,
fontWeight: "600",
flex: 1,
},
categoryQty: {
color: colors.textWhite,
fontSize: fontSize.sm,
fontWeight: "700",
marginRight: spacing.l,
},
categoryTotal: {
color: colors.accent,
fontSize: fontSize.sm,
fontWeight: "700",
minWidth: 70,
textAlign: "right",
},
// Items modal
itemsModalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.6)",
justifyContent: "flex-end",
},
itemsModalContainer: {
backgroundColor: colors.bgPrimary,
borderTopLeftRadius: borderRadius.lg,
borderTopRightRadius: borderRadius.lg,
maxHeight: "75%",
padding: spacing.l,
},
itemsModalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.m,
},
itemsModalTitle: {
fontSize: fontSize.lg,
fontWeight: "700",
color: colors.textWhite,
},
itemsModalCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
padding: spacing.m,
marginBottom: spacing.s,
},
// Fullscreen
fullscreenContainer: {
flex: 1,
backgroundColor: colors.bgPrimary,
},
fullscreenMap: { ...StyleSheet.absoluteFillObject },
fullscreenTopBar: {
position: "absolute",
top: 0,
left: 0,
right: 0,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingTop: 50,
paddingHorizontal: spacing.l,
paddingBottom: spacing.m,
backgroundColor: "rgba(0,0,0,0.5)",
},
closeBtn: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "rgba(255,255,255,0.15)",
justifyContent: "center",
alignItems: "center",
},
fullscreenTitle: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
}),
[colors, MAP_HEIGHT],
);
// Groupement des items par product_id
const productGroups = (items ?? []).reduce<Record<string, any[]>>(
(acc, item) => {
const key = String(item.product_id || item.produit);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
},
{},
);
const groupedList = Object.values(productGroups);
// Récapitulatif par catégorie
const categoryTotals = (items ?? []).reduce<
Record<string, { qty: number; total: number }>
>((acc, item) => {
const cat = item.category || "Autre";
if (!acc[cat]) acc[cat] = { qty: 0, total: 0 };
acc[cat].qty += item.quantite ?? 0;
acc[cat].total += item.prix ?? 0;
return acc;
}, {});
const categoryEntries = Object.entries(categoryTotals);
if (loading) return <LoadingSpinner message="Chargement..." />;
if (!command)
return (
<View style={styles.container}>
<Text style={styles.empty}>Commande introuvable</Text>
</View>
);
const hasMap = livreurCoords !== null;
return (
<ScrollView
style={styles.container}
contentContainerStyle={{ padding: spacing.l }}
>
{/* Fullscreen map modal */}
<Modal
visible={mapFullscreen}
animationType="fade"
onRequestClose={() => setMapFullscreen(false)}
>
<View style={styles.fullscreenContainer}>
<TomTomMap
ref={fullscreenMapRef}
style={styles.fullscreenMap}
markers={livreurMarkers}
initialCenter={livreurCoords ?? undefined}
initialZoom={14}
/>
<View style={styles.fullscreenTopBar}>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => setMapFullscreen(false)}
>
<Ionicons name="close" size={24} color={colors.white} />
</TouchableOpacity>
<Text style={styles.fullscreenTitle}>
{routeInfo
? `${routeInfo.distance} · ${routeInfo.duration}`
: `Livreur: ${command.livreur_assign}`}
</Text>
<View style={{ width: 40 }} />
</View>
</View>
</Modal>
{/* Order info */}
<Card>
<View style={styles.row}>
<Text style={styles.title}>Commande #{command.id}</Text>
<StatusBadge status={command.status} />
</View>
<Text style={styles.info}>Client: {command.username}</Text>
<Text style={styles.info}>Adresse: {command.adresse}</Text>
<Text style={styles.info}>
Total brut: {command.total_prix?.toFixed(2)}
</Text>
{command.referral_used > 0 && (
<>
<Text style={[styles.info, { color: colors.success }]}>
Parrainage utilisé: -
{command.referral_used?.toFixed(2)}
</Text>
<Text
style={[
styles.info,
{
fontWeight: "700",
color: colors.textPrimary,
},
]}
>
Net:{" "}
{(
command.total_prix - command.referral_used
).toFixed(2)}{" "}
</Text>
</>
)}
{command.livreur_assign && (
<Text style={styles.info}>
Livreur: {command.livreur_assign}
</Text>
)}
<Text style={styles.date}>
{new Date(command.created_at).toLocaleString("fr-FR")}
</Text>
{command.status === "cancelled" && command.cancel_reason ? (
<View
style={{
marginTop: spacing.m,
padding: spacing.m,
backgroundColor: colors.danger + "18",
borderRadius: borderRadius.sm,
borderLeftWidth: 3,
borderLeftColor: colors.danger,
}}
>
<Text
style={{
color: colors.danger,
fontSize: fontSize.xs,
fontWeight: "700",
marginBottom: 4,
textTransform: "uppercase",
letterSpacing: 0.5,
}}
>
Motif d'annulation
</Text>
<Text
style={{
color: colors.textSecondary,
fontSize: fontSize.sm,
}}
>
{command.cancel_reason}
</Text>
</View>
) : command.status === "cancelled" ? (
<View
style={{
marginTop: spacing.m,
padding: spacing.m,
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.sm,
}}
>
<Text
style={{
color: colors.textMuted,
fontSize: fontSize.sm,
fontStyle: "italic",
}}
>
Aucun motif fourni
</Text>
</View>
) : null}
</Card>
{/* Livreur tracking map */}
{command.livreur_assign && (
<>
<Text style={styles.sectionTitle}>Suivi du livreur</Text>
{hasMap ? (
<View style={styles.mapContainer}>
<TomTomMap
ref={mapRef}
style={styles.map}
markers={livreurMarkers}
initialCenter={livreurCoords ?? undefined}
initialZoom={14}
/>
{routeInfo && (
<View style={styles.routeOverlay}>
<Text style={styles.routeOverlayUser}>
{command.livreur_assign}
</Text>
<Text style={styles.routeOverlayInfo}>
{routeInfo.distance} · {routeInfo.duration}
</Text>
</View>
)}
{mapLoading && (
<View style={styles.mapLoadingOverlay}>
<Text style={styles.mapLoadingText}>
Chargement...
</Text>
</View>
)}
<TouchableOpacity
style={styles.expandBtn}
onPress={() => setMapFullscreen(true)}
>
<Ionicons
name="expand-outline"
size={18}
color={colors.white}
/>
</TouchableOpacity>
</View>
) : mapLoading ? (
<View style={styles.noMapBox}>
<Text style={styles.noMapText}>
Chargement position du livreur...
</Text>
</View>
) : (
<View style={styles.noMapBox}>
<Ionicons
name="location-outline"
size={28}
color={colors.textMuted}
/>
<Text style={styles.noMapText}>
Position GPS du livreur non disponible
</Text>
</View>
)}
</>
)}
{/* Items groupés par produit */}
<Text style={styles.sectionTitle}>Articles ({items.length})</Text>
{groupedList.map((group, gi) => {
const rep = group[0];
const name = rep.produit ?? rep.product_name;
const unit = rep.unit || "";
const totalQty = group.reduce(
(s: number, it: any) => s + (it.quantite ?? 0),
0,
);
const totalPrice = group.reduce(
(s: number, it: any) => s + (it.prix ?? 0),
0,
);
const isMultiple = group.length > 1;
return (
<Card
key={`${rep.product_id}-${gi}`}
style={{ marginBottom: spacing.s }}
>
<View style={styles.row}>
<Text style={[styles.itemName, { flex: 1 }]}>
{name}
</Text>
{rep.category ? (
<Text style={styles.categoryBadge}>
{rep.category}
</Text>
) : null}
<TouchableOpacity
style={styles.itemDeleteBtn}
onPress={() => handleDeleteItem(rep.id, name)}
>
<Ionicons
name="trash-outline"
size={18}
color={colors.danger}
/>
</TouchableOpacity>
</View>
{isMultiple &&
group.map((item: any, i: number) => (
<View key={item.id} style={styles.subItemRow}>
<Text style={styles.subItemText}>
{item.quantite}
{unit} {item.prix?.toFixed(2)}
</Text>
<TouchableOpacity
onPress={() =>
handleDeleteItem(item.id, name)
}
>
<Ionicons
name="remove-circle-outline"
size={16}
color={colors.danger}
/>
</TouchableOpacity>
</View>
))}
<View style={styles.groupTotalRow}>
<Text style={styles.groupTotalQty}>
{isMultiple
? `Total: ${totalQty}${unit}`
: `${totalQty}${unit}`}
</Text>
<Text style={styles.groupTotalPrice}>
{totalPrice.toFixed(2)}
</Text>
</View>
</Card>
);
})}
{/* Récapitulatif par catégorie */}
{categoryEntries.length > 0 && (
<>
<Text style={styles.sectionTitle}>Par catégorie</Text>
<Card style={styles.categorySummaryCard}>
{categoryEntries.map(([cat, data]) => (
<View key={cat} style={styles.categoryRow}>
<Text style={styles.categoryName}>{cat}</Text>
<Text style={styles.categoryQty}>
{data.qty.toFixed(
data.qty % 1 === 0 ? 0 : 2,
)}
</Text>
<Text style={styles.categoryTotal}>
{data.total.toFixed(2)}
</Text>
</View>
))}
</Card>
</>
)}
{/* Actions */}
<Text style={styles.sectionTitle}>Actions</Text>
<View style={styles.actions}>
<Button
title="Voir les items"
onPress={() => setShowItemsModal(true)}
variant="secondary"
fullWidth
/>
{command.status !== "approved" &&
command.status !== "cancelled" && (
<Button
title="Valider commande"
onPress={handleValidate}
variant="success"
fullWidth
style={{ marginTop: spacing.s }}
/>
)}
{command.status === "pending" && (
<Button
title="Passer en assignée"
onPress={() => handleStatusChange("assigned")}
variant="secondary"
fullWidth
style={{ marginTop: spacing.s }}
/>
)}
{command.status === "assigned" && (
<Button
title="Passer en route"
onPress={() => handleStatusChange("en_route")}
variant="secondary"
fullWidth
style={{ marginTop: spacing.s }}
/>
)}
{!["approved", "cancelled"].includes(command.status) && (
<Button
title="Le livreur est là"
onPress={handleNotifyClient}
variant="warning"
fullWidth
style={{ marginTop: spacing.s }}
/>
)}
<Button
title="Supprimer la commande"
onPress={handleDeleteCommand}
variant="danger"
fullWidth
style={{ marginTop: spacing.s }}
/>
</View>
{/* Items modal */}
<Modal
visible={showItemsModal}
animationType="slide"
onRequestClose={() => setShowItemsModal(false)}
transparent
>
<View style={styles.itemsModalOverlay}>
<View style={styles.itemsModalContainer}>
<View style={styles.itemsModalHeader}>
<Text style={styles.itemsModalTitle}>
Articles ({items.length})
</Text>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => setShowItemsModal(false)}
>
<Ionicons
name="close"
size={22}
color={colors.textWhite}
/>
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{groupedList.map((group, gi) => {
const rep = group[0];
const name = rep.produit ?? rep.product_name;
const unit = rep.unit || "";
const totalQty = group.reduce(
(s: number, it: any) =>
s + (it.quantite ?? 0),
0,
);
const totalPrice = group.reduce(
(s: number, it: any) => s + (it.prix ?? 0),
0,
);
const isMultiple = group.length > 1;
return (
<View
key={`modal-${rep.product_id}-${gi}`}
style={styles.itemsModalCard}
>
<View style={styles.row}>
<Text
style={[
styles.itemName,
{ flex: 1 },
]}
>
{name}
</Text>
{rep.category ? (
<Text
style={styles.categoryBadge}
>
{rep.category}
</Text>
) : null}
<TouchableOpacity
style={styles.itemDeleteBtn}
onPress={() => {
setShowItemsModal(false);
handleDeleteItem(
rep.id,
name,
);
}}
>
<Ionicons
name="trash-outline"
size={18}
color={colors.danger}
/>
</TouchableOpacity>
</View>
{isMultiple &&
group.map((item: any) => (
<View
key={item.id}
style={styles.subItemRow}
>
<Text
style={
styles.subItemText
}
>
{item.quantite}
{unit} {" "}
{item.prix?.toFixed(2)}{" "}
</Text>
</View>
))}
<View style={styles.groupTotalRow}>
<Text style={styles.groupTotalQty}>
{isMultiple
? `Total: ${totalQty}${unit}`
: `${totalQty}${unit}`}
</Text>
<Text
style={styles.groupTotalPrice}
>
{totalPrice.toFixed(2)}
</Text>
</View>
</View>
);
})}
{/* Récapitulatif catégories dans le modal */}
{categoryEntries.length > 0 && (
<View
style={{
marginTop: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
paddingTop: spacing.m,
}}
>
<Text
style={[
styles.itemsModalTitle,
{
fontSize: fontSize.md,
marginBottom: spacing.s,
},
]}
>
Par catégorie
</Text>
{categoryEntries.map(([cat, data]) => (
<View
key={cat}
style={styles.categoryRow}
>
<Text style={styles.categoryName}>
{cat}
</Text>
<Text style={styles.categoryQty}>
{data.qty.toFixed(
data.qty % 1 === 0 ? 0 : 2,
)}
</Text>
<Text style={styles.categoryTotal}>
{data.total.toFixed(2)}
</Text>
</View>
))}
</View>
)}
{items.length === 0 && (
<Text style={styles.empty}>Aucun article</Text>
)}
</ScrollView>
</View>
</View>
</Modal>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</ScrollView>
);
}