324 lines
9.1 KiB
TypeScript
324 lines
9.1 KiB
TypeScript
import { ThemedText } from "@/components/themed-text";
|
|
import { ThemedView } from "@/components/themed-view";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import dayjs from "dayjs";
|
|
import React, { useCallback, useMemo } from "react";
|
|
import { FlatList, StyleSheet, TouchableOpacity, View } from "react-native";
|
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
import { AlarmData } from ".";
|
|
|
|
// ============ Types ============
|
|
type AlarmType = "approaching" | "entered" | "fishing";
|
|
|
|
interface AlarmCardProps {
|
|
alarm: AlarmData;
|
|
onPress?: () => void;
|
|
}
|
|
|
|
// ============ Config ============
|
|
const ALARM_CONFIG: Record<
|
|
AlarmType,
|
|
{
|
|
icon: keyof typeof Ionicons.glyphMap;
|
|
label: string;
|
|
bgColor: string;
|
|
borderColor: string;
|
|
iconBgColor: string;
|
|
iconColor: string;
|
|
labelColor: string;
|
|
}
|
|
> = {
|
|
entered: {
|
|
icon: "warning",
|
|
label: "Xâm nhập",
|
|
bgColor: "bg-red-50",
|
|
borderColor: "border-red-200",
|
|
iconBgColor: "bg-red-100",
|
|
iconColor: "#DC2626",
|
|
labelColor: "text-red-600",
|
|
},
|
|
approaching: {
|
|
icon: "alert-circle",
|
|
label: "Tiếp cận",
|
|
bgColor: "bg-amber-50",
|
|
borderColor: "border-amber-200",
|
|
iconBgColor: "bg-amber-100",
|
|
iconColor: "#D97706",
|
|
labelColor: "text-amber-600",
|
|
},
|
|
fishing: {
|
|
icon: "fish",
|
|
label: "Đánh bắt",
|
|
bgColor: "bg-orange-50",
|
|
borderColor: "border-orange-200",
|
|
iconBgColor: "bg-orange-100",
|
|
iconColor: "#EA580C",
|
|
labelColor: "text-orange-600",
|
|
},
|
|
};
|
|
|
|
// ============ Helper Functions ============
|
|
const formatTimestamp = (timestamp?: number): string => {
|
|
if (!timestamp) return "N/A";
|
|
return dayjs.unix(timestamp).format("DD/MM/YYYY HH:mm:ss");
|
|
};
|
|
|
|
// ============ AlarmCard Component ============
|
|
const AlarmCard = React.memo(({ alarm, onPress }: AlarmCardProps) => {
|
|
const config = ALARM_CONFIG[alarm.type];
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={onPress}
|
|
activeOpacity={0.7}
|
|
className={`rounded-2xl p-4 ${config.bgColor} ${config.borderColor} border shadow-sm`}
|
|
>
|
|
<View className="flex-row items-start gap-3">
|
|
{/* Icon Container */}
|
|
<View
|
|
className={`w-12 h-12 rounded-xl items-center justify-center ${config.iconBgColor}`}
|
|
>
|
|
<Ionicons name={config.icon} size={24} color={config.iconColor} />
|
|
</View>
|
|
|
|
{/* Content */}
|
|
<View className="flex-1">
|
|
{/* Header: Ship name + Badge */}
|
|
<View className="flex-row items-center justify-between mb-1">
|
|
<ThemedText className="text-base font-bold text-gray-800 flex-1 mr-2">
|
|
{alarm.ship_name || alarm.thing_id}
|
|
</ThemedText>
|
|
<View className={`px-2 py-1 rounded-full ${config.iconBgColor}`}>
|
|
<ThemedText
|
|
className={`text-xs font-semibold ${config.labelColor}`}
|
|
>
|
|
{config.label}
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Zone Info */}
|
|
<ThemedText className="text-sm text-gray-600 mb-2" numberOfLines={2}>
|
|
{alarm.zone.message || alarm.zone.zone_name}
|
|
</ThemedText>
|
|
|
|
{/* Footer: Zone ID + Time */}
|
|
<View className="flex-row items-center justify-between">
|
|
<View className="flex-row items-center gap-1">
|
|
<Ionicons name="time-outline" size={20} color="#6B7280" />
|
|
<ThemedText className="text-xs text-gray-500">
|
|
{formatTimestamp(alarm.zone.gps_time)}
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
});
|
|
|
|
AlarmCard.displayName = "AlarmCard";
|
|
|
|
// ============ Main Component ============
|
|
interface WarningScreenProps {
|
|
alarms?: AlarmData[];
|
|
}
|
|
|
|
export default function WarningScreen({ alarms = [] }: WarningScreenProps) {
|
|
// Mock data for demo - replace with actual props
|
|
const sampleAlarms: AlarmData[] = useMemo(
|
|
() => [
|
|
{
|
|
thing_id: "SHIP-001",
|
|
ship_name: "Ocean Star",
|
|
type: "entered",
|
|
zone: {
|
|
zone_type: 1,
|
|
zone_name: "Khu vực cấm A1",
|
|
zone_id: "A1",
|
|
message: "Tàu đã đi vào vùng cấm A1",
|
|
alarm_type: 1,
|
|
lat: 10.12345,
|
|
lon: 106.12345,
|
|
s: 12,
|
|
h: 180,
|
|
fishing: false,
|
|
gps_time: 1733389200,
|
|
},
|
|
},
|
|
{
|
|
thing_id: "SHIP-002",
|
|
ship_name: "Blue Whale",
|
|
type: "approaching",
|
|
zone: {
|
|
zone_type: 2,
|
|
zone_name: "Vùng cảnh báo B3",
|
|
zone_id: "B3",
|
|
message: "Tàu đang tiếp cận khu vực cấm B3",
|
|
alarm_type: 2,
|
|
lat: 9.87654,
|
|
lon: 105.87654,
|
|
gps_time: 1733389260,
|
|
},
|
|
},
|
|
{
|
|
thing_id: "SHIP-003",
|
|
ship_name: "Sea Dragon",
|
|
type: "fishing",
|
|
zone: {
|
|
zone_type: 3,
|
|
zone_name: "Vùng cấm đánh bắt C2",
|
|
zone_id: "C2",
|
|
message: "Phát hiện hành vi đánh bắt trong vùng cấm C2",
|
|
alarm_type: 3,
|
|
lat: 11.11223,
|
|
lon: 107.44556,
|
|
fishing: true,
|
|
gps_time: 1733389320,
|
|
},
|
|
},
|
|
{
|
|
thing_id: "SHIP-004",
|
|
ship_name: "Red Coral",
|
|
type: "entered",
|
|
zone: {
|
|
zone_type: 1,
|
|
zone_name: "Khu vực A2",
|
|
zone_id: "A2",
|
|
message: "Tàu đã đi sâu vào khu vực A2",
|
|
alarm_type: 1,
|
|
gps_time: 1733389380,
|
|
},
|
|
},
|
|
{
|
|
thing_id: "SHIP-005",
|
|
ship_name: "Silver Wind",
|
|
type: "approaching",
|
|
zone: {
|
|
zone_type: 2,
|
|
zone_name: "Vùng B1",
|
|
zone_id: "B1",
|
|
message: "Tàu đang tiến gần vào vùng B1",
|
|
alarm_type: 2,
|
|
gps_time: 1733389440,
|
|
},
|
|
},
|
|
],
|
|
[]
|
|
);
|
|
|
|
const displayAlarms = alarms.length > 0 ? alarms : sampleAlarms;
|
|
|
|
const handleAlarmPress = useCallback((alarm: AlarmData) => {
|
|
console.log("Alarm pressed:", alarm);
|
|
// TODO: Navigate to alarm detail or show modal
|
|
}, []);
|
|
|
|
const renderAlarmCard = useCallback(
|
|
({ item }: { item: AlarmData }) => (
|
|
<AlarmCard alarm={item} onPress={() => handleAlarmPress(item)} />
|
|
),
|
|
[handleAlarmPress]
|
|
);
|
|
|
|
const keyExtractor = useCallback(
|
|
(item: AlarmData, index: number) => `${item.thing_id}-${index}`,
|
|
[]
|
|
);
|
|
|
|
const ItemSeparator = useCallback(() => <View className="h-3" />, []);
|
|
|
|
// Count alarms by type
|
|
const alarmCounts = useMemo(() => {
|
|
return displayAlarms.reduce((acc, alarm) => {
|
|
acc[alarm.type] = (acc[alarm.type] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<AlarmType, number>);
|
|
}, [displayAlarms]);
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={["top"]}>
|
|
<ThemedView style={styles.content}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View className="flex-row items-center gap-3">
|
|
<View className="w-10 h-10 rounded-xl bg-red-500 items-center justify-center">
|
|
<Ionicons name="warning" size={22} color="#fff" />
|
|
</View>
|
|
<ThemedText style={styles.titleText}>Cảnh báo</ThemedText>
|
|
</View>
|
|
<View className="bg-red-500 px-3 py-1 rounded-full">
|
|
<ThemedText className="text-white text-sm font-semibold">
|
|
{displayAlarms.length}
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Stats Bar */}
|
|
<View className="flex-row px-4 pb-3 gap-2">
|
|
{(["entered", "approaching", "fishing"] as AlarmType[]).map(
|
|
(type) => {
|
|
const config = ALARM_CONFIG[type];
|
|
const count = alarmCounts[type] || 0;
|
|
return (
|
|
<View
|
|
key={type}
|
|
className={`flex-1 flex-row items-center justify-center gap-1 py-2 rounded-lg ${config.iconBgColor}`}
|
|
>
|
|
<Ionicons
|
|
name={config.icon}
|
|
size={14}
|
|
color={config.iconColor}
|
|
/>
|
|
<ThemedText
|
|
className={`text-xs font-medium ${config.labelColor}`}
|
|
>
|
|
{count}
|
|
</ThemedText>
|
|
</View>
|
|
);
|
|
}
|
|
)}
|
|
</View>
|
|
|
|
{/* Alarm List */}
|
|
<FlatList
|
|
data={displayAlarms}
|
|
renderItem={renderAlarmCard}
|
|
keyExtractor={keyExtractor}
|
|
ItemSeparatorComponent={ItemSeparator}
|
|
contentContainerStyle={styles.listContent}
|
|
showsVerticalScrollIndicator={false}
|
|
initialNumToRender={10}
|
|
maxToRenderPerBatch={10}
|
|
windowSize={5}
|
|
/>
|
|
</ThemedView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 16,
|
|
},
|
|
titleText: {
|
|
fontSize: 26,
|
|
fontWeight: "700",
|
|
},
|
|
listContent: {
|
|
paddingHorizontal: 16,
|
|
paddingBottom: 20,
|
|
},
|
|
});
|