452 lines
16 KiB
TypeScript
452 lines
16 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
FlatList,
|
|
RefreshControl,
|
|
Modal,
|
|
TouchableOpacity,
|
|
Animated,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
|
import { useTheme } from "../../context/ThemeContext";
|
|
import {
|
|
getMyAlerts,
|
|
triggerPoliceAlert,
|
|
endAlert,
|
|
} from "../../api/api_delivery";
|
|
import type { Alert as AlertType } from "../../api/types";
|
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
|
import Card from "../../components/ui/Card";
|
|
import Badge from "../../components/ui/Badge";
|
|
import Button from "../../components/ui/Button";
|
|
import AlertModal from "../../components/ui/AlertModal";
|
|
import { useAlert } from "../../hooks/useAlert";
|
|
|
|
export default function AlertsScreen() {
|
|
const { colors } = useTheme();
|
|
const [alerts, setAlerts] = useState<AlertType[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [triggering, setTriggering] = useState(false);
|
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
|
const [successMessage, setSuccessMessage] = useState("");
|
|
const [pulseAnim] = useState(() => new Animated.Value(1));
|
|
const { alert: alertModal, showError, hideAlert } = useAlert();
|
|
|
|
const loadData = useCallback(async () => {
|
|
try {
|
|
const res = await getMyAlerts();
|
|
if (res.success && res.alerts) setAlerts(res.alerts);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
// Pulse animation when modal is open
|
|
useEffect(() => {
|
|
if (!showConfirmModal) return;
|
|
const anim = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1.15,
|
|
duration: 800,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1,
|
|
duration: 800,
|
|
useNativeDriver: true,
|
|
}),
|
|
]),
|
|
);
|
|
anim.start();
|
|
return () => anim.stop();
|
|
}, [showConfirmModal, pulseAnim]);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await loadData();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const handleConfirmTrigger = async () => {
|
|
setShowConfirmModal(false);
|
|
setTriggering(true);
|
|
const res = await triggerPoliceAlert();
|
|
setTriggering(false);
|
|
if (res.success) {
|
|
setSuccessMessage(res.message || "Alerte déclenchée avec succès");
|
|
setShowSuccessModal(true);
|
|
loadData();
|
|
} else {
|
|
showError("Erreur", res.error || "Erreur");
|
|
}
|
|
};
|
|
|
|
const handleEnd = async (alertId: number) => {
|
|
const res = await endAlert(alertId);
|
|
if (res.success) {
|
|
loadData();
|
|
} else {
|
|
showError("Erreur", res.error || "Erreur");
|
|
}
|
|
};
|
|
|
|
const renderAlert = ({ item }: { item: AlertType }) => (
|
|
<Card style={{ marginBottom: spacing.m }}>
|
|
<View style={styles.row}>
|
|
<Ionicons
|
|
name="alert-circle"
|
|
size={24}
|
|
color={
|
|
item.status === "true"
|
|
? colors.danger
|
|
: colors.textMuted
|
|
}
|
|
/>
|
|
<View style={{ flex: 1, marginLeft: spacing.m }}>
|
|
<Text style={styles.date}>
|
|
{new Date(item.created_at).toLocaleString("fr-FR")}
|
|
</Text>
|
|
</View>
|
|
<Badge
|
|
label={item.status === "true" ? "Active" : "Terminée"}
|
|
color={
|
|
item.status === "true" ? colors.danger : colors.success
|
|
}
|
|
/>
|
|
</View>
|
|
{item.status === "true" && (
|
|
<Button
|
|
title="Terminer l'alerte"
|
|
onPress={() => handleEnd(item.id)}
|
|
style={{
|
|
marginTop: spacing.s,
|
|
backgroundColor: colors.success,
|
|
}}
|
|
/>
|
|
)}
|
|
</Card>
|
|
);
|
|
|
|
const styles = useMemo(
|
|
() =>
|
|
StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
|
row: { flexDirection: "row", alignItems: "center" },
|
|
date: { color: colors.textMuted, fontSize: fontSize.sm },
|
|
empty: {
|
|
color: colors.textMuted,
|
|
textAlign: "center",
|
|
marginTop: spacing.xxl,
|
|
fontSize: fontSize.md,
|
|
},
|
|
triggerSection: {
|
|
padding: spacing.l,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.border,
|
|
},
|
|
|
|
// Modal
|
|
modalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
padding: spacing.xl,
|
|
},
|
|
modalContent: {
|
|
width: "100%",
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.lg,
|
|
padding: spacing.xl,
|
|
alignItems: "center",
|
|
borderWidth: 1,
|
|
borderColor: colors.danger + "40",
|
|
},
|
|
modalIconCircle: {
|
|
width: 90,
|
|
height: 90,
|
|
borderRadius: 45,
|
|
backgroundColor: colors.danger + "20",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
marginBottom: spacing.l,
|
|
},
|
|
modalIconInner: {
|
|
width: 68,
|
|
height: 68,
|
|
borderRadius: 34,
|
|
backgroundColor: colors.danger,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
modalTitle: {
|
|
fontSize: fontSize.xl,
|
|
fontWeight: "700",
|
|
color: colors.danger,
|
|
marginBottom: spacing.s,
|
|
},
|
|
modalMessage: {
|
|
fontSize: fontSize.sm,
|
|
color: colors.textSecondary,
|
|
textAlign: "center",
|
|
lineHeight: 20,
|
|
marginBottom: spacing.m,
|
|
},
|
|
modalWarning: {
|
|
fontSize: fontSize.md,
|
|
fontWeight: "600",
|
|
color: colors.textWhite,
|
|
textAlign: "center",
|
|
marginBottom: spacing.xl,
|
|
},
|
|
modalButtons: {
|
|
flexDirection: "row",
|
|
gap: spacing.m,
|
|
width: "100%",
|
|
},
|
|
modalCancelBtn: {
|
|
flex: 1,
|
|
paddingVertical: 14,
|
|
borderRadius: borderRadius.sm,
|
|
backgroundColor: colors.bgInput,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
modalCancelText: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.md,
|
|
fontWeight: "600",
|
|
},
|
|
modalConfirmBtn: {
|
|
flex: 1,
|
|
flexDirection: "row",
|
|
paddingVertical: 14,
|
|
borderRadius: borderRadius.sm,
|
|
backgroundColor: colors.danger,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: spacing.xs,
|
|
},
|
|
modalConfirmText: {
|
|
color: colors.white,
|
|
fontSize: fontSize.md,
|
|
fontWeight: "700",
|
|
},
|
|
|
|
// Success Modal
|
|
successModalContent: {
|
|
width: "100%",
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.lg,
|
|
padding: spacing.xl,
|
|
alignItems: "center",
|
|
borderWidth: 1,
|
|
borderColor: colors.success + "40",
|
|
},
|
|
successIconCircle: {
|
|
width: 90,
|
|
height: 90,
|
|
borderRadius: 45,
|
|
backgroundColor: colors.success + "20",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
marginBottom: spacing.l,
|
|
},
|
|
successIconInner: {
|
|
width: 68,
|
|
height: 68,
|
|
borderRadius: 34,
|
|
backgroundColor: colors.success,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
successTitle: {
|
|
fontSize: fontSize.xl,
|
|
fontWeight: "700",
|
|
color: colors.success,
|
|
marginBottom: spacing.s,
|
|
},
|
|
successMessage: {
|
|
fontSize: fontSize.sm,
|
|
color: colors.textSecondary,
|
|
textAlign: "center",
|
|
lineHeight: 20,
|
|
marginBottom: spacing.s,
|
|
},
|
|
successHint: {
|
|
fontSize: fontSize.xs,
|
|
color: colors.textMuted,
|
|
textAlign: "center",
|
|
lineHeight: 18,
|
|
marginBottom: spacing.xl,
|
|
},
|
|
successBtn: {
|
|
width: "100%",
|
|
paddingVertical: 14,
|
|
borderRadius: borderRadius.sm,
|
|
backgroundColor: colors.success,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
successBtnText: {
|
|
color: colors.white,
|
|
fontSize: fontSize.md,
|
|
fontWeight: "700",
|
|
},
|
|
}),
|
|
[colors],
|
|
);
|
|
|
|
if (loading) return <LoadingSpinner message="Chargement alertes..." />;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* Confirmation Modal */}
|
|
<Modal
|
|
visible={showConfirmModal}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={() => setShowConfirmModal(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.modalContent}>
|
|
{/* Animated alert icon */}
|
|
<Animated.View
|
|
style={[
|
|
styles.modalIconCircle,
|
|
{ transform: [{ scale: pulseAnim }] },
|
|
]}
|
|
>
|
|
<View style={styles.modalIconInner}>
|
|
<Ionicons
|
|
name="warning"
|
|
size={40}
|
|
color={colors.white}
|
|
/>
|
|
</View>
|
|
</Animated.View>
|
|
|
|
<Text style={styles.modalTitle}>Alerte Police</Text>
|
|
<Text style={styles.modalMessage}>
|
|
Vous êtes sur le point de déclencher une alerte
|
|
police. Cette action notifiera immédiatement
|
|
l'administration.
|
|
</Text>
|
|
<Text style={styles.modalWarning}>
|
|
Confirmez-vous le déclenchement ?
|
|
</Text>
|
|
|
|
<View style={styles.modalButtons}>
|
|
<TouchableOpacity
|
|
style={styles.modalCancelBtn}
|
|
onPress={() => setShowConfirmModal(false)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={styles.modalCancelText}>
|
|
Annuler
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.modalConfirmBtn}
|
|
onPress={handleConfirmTrigger}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons
|
|
name="alert-circle"
|
|
size={18}
|
|
color={colors.white}
|
|
/>
|
|
<Text style={styles.modalConfirmText}>
|
|
Déclencher
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
{/* Success Modal */}
|
|
<Modal
|
|
visible={showSuccessModal}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={() => setShowSuccessModal(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.successModalContent}>
|
|
<View style={styles.successIconCircle}>
|
|
<View style={styles.successIconInner}>
|
|
<Ionicons
|
|
name="checkmark-sharp"
|
|
size={40}
|
|
color={colors.white}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.successTitle}>Alerte envoyée</Text>
|
|
<Text style={styles.successMessage}>
|
|
{successMessage}
|
|
</Text>
|
|
<Text style={styles.successHint}>
|
|
L'administration a été notifiée. Vous pourrez
|
|
terminer l'alerte quand la situation sera résolue.
|
|
</Text>
|
|
|
|
<TouchableOpacity
|
|
style={styles.successBtn}
|
|
onPress={() => setShowSuccessModal(false)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={styles.successBtnText}>Compris</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
<View style={styles.triggerSection}>
|
|
<Button
|
|
title={triggering ? "Envoi..." : "Déclencher alerte police"}
|
|
onPress={() => setShowConfirmModal(true)}
|
|
disabled={triggering}
|
|
style={{ backgroundColor: colors.danger }}
|
|
/>
|
|
</View>
|
|
<FlatList
|
|
data={alerts}
|
|
keyExtractor={(item) => item.id.toString()}
|
|
renderItem={renderAlert}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={colors.success}
|
|
/>
|
|
}
|
|
contentContainerStyle={{ padding: spacing.l }}
|
|
ListEmptyComponent={
|
|
<Text style={styles.empty}>Aucune alerte</Text>
|
|
}
|
|
/>
|
|
<AlertModal
|
|
visible={alertModal.visible}
|
|
type={alertModal.type}
|
|
title={alertModal.title}
|
|
message={alertModal.message}
|
|
onClose={hideAlert}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|