Cập nhật theme cho cảnh báo, giám sát

This commit is contained in:
2025-12-29 17:05:05 +07:00
parent 871360af49
commit bf261e70e4
6 changed files with 234 additions and 115 deletions

View File

@@ -10,6 +10,7 @@ import ShipSearchForm, {
import { ThemedText } from "@/components/themed-text"; import { ThemedText } from "@/components/themed-text";
import { EVENT_SEARCH_THINGS, IOS_PLATFORM, LIGHT_THEME } from "@/constants"; import { EVENT_SEARCH_THINGS, IOS_PLATFORM, LIGHT_THEME } from "@/constants";
import { queryBanzoneById } from "@/controller/MapController"; import { queryBanzoneById } from "@/controller/MapController";
import { useAppTheme } from "@/hooks/use-app-theme";
import { usePlatform } from "@/hooks/use-platform"; import { usePlatform } from "@/hooks/use-platform";
import { useThemeContext } from "@/hooks/use-theme-context"; import { useThemeContext } from "@/hooks/use-theme-context";
import { searchThingEventBus } from "@/services/device_events"; import { searchThingEventBus } from "@/services/device_events";
@@ -69,6 +70,7 @@ export default function HomeScreen() {
const [things, setThings] = useState<Model.ThingsResponse | null>(null); const [things, setThings] = useState<Model.ThingsResponse | null>(null);
const platform = usePlatform(); const platform = usePlatform();
const theme = useThemeContext().colorScheme; const theme = useThemeContext().colorScheme;
const { colors } = useAppTheme();
const scale = useRef(new Animated.Value(0)).current; const scale = useRef(new Animated.Value(0)).current;
const opacity = useRef(new Animated.Value(1)).current; const opacity = useRef(new Animated.Value(1)).current;
const [shipSearchFormData, setShipSearchFormData] = useState< const [shipSearchFormData, setShipSearchFormData] = useState<
@@ -85,7 +87,7 @@ export default function HomeScreen() {
// Alarm list animation // Alarm list animation
const screenHeight = Dimensions.get("window").height; const screenHeight = Dimensions.get("window").height;
const alarmListHeight = Math.round(screenHeight * 0.3); const alarmListHeight = Math.round(screenHeight * 0.4);
const alarmTranslateY = useRef(new Animated.Value(alarmListHeight)).current; const alarmTranslateY = useRef(new Animated.Value(alarmListHeight)).current;
const alarmOpacity = useRef(new Animated.Value(0)).current; const alarmOpacity = useRef(new Animated.Value(0)).current;
const uiAnim = useRef(new Animated.Value(1)).current; // 1 visible, 0 hidden const uiAnim = useRef(new Animated.Value(1)).current; // 1 visible, 0 hidden
@@ -693,6 +695,7 @@ export default function HomeScreen() {
fontSize: 20, fontSize: 20,
textAlign: "center", textAlign: "center",
fontWeight: "600", fontWeight: "600",
color: colors.text,
}} }}
> >
Danh sách tàu thuyền Danh sách tàu thuyền
@@ -735,7 +738,7 @@ export default function HomeScreen() {
style={{ style={{
width: "50%", width: "50%",
height: 1, height: 1,
backgroundColor: "#E5E7EB", backgroundColor: colors.separator,
marginVertical: 8, marginVertical: 8,
borderRadius: 1, borderRadius: 1,
}} }}
@@ -766,22 +769,36 @@ export default function HomeScreen() {
zIndex: 10, zIndex: 10,
}} }}
> >
<View className="bg-white rounded-t-3xl shadow-md overflow-hidden h-full z-50"> <View
style={{
backgroundColor: colors.surface,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: "hidden",
height: "100%",
}}
>
<View className="flex-row items-center justify-between px-4 py-2"> <View className="flex-row items-center justify-between px-4 py-2">
<Text className="text-lg font-semibold"> <Text
style={{
fontSize: 18,
fontWeight: "600",
color: colors.text,
}}
>
Danh sách cảnh báo Danh sách cảnh báo
</Text> </Text>
<TouchableOpacity <TouchableOpacity
onPress={() => closeAlarmList()} onPress={() => closeAlarmList()}
style={{ padding: 6 }} style={{ padding: 6 }}
> >
<Ionicons name="close" size={20} color="#374151" /> <Ionicons name="close" size={20} color={colors.icon} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View <View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: "#F9FAFB", backgroundColor: colors.backgroundSecondary,
zIndex: 50, zIndex: 50,
}} }}
> >

View File

@@ -84,7 +84,12 @@ const WarningScreen = () => {
}); });
const slice = resp.data?.alarms ?? []; const slice = resp.data?.alarms ?? [];
setAlarms((prev) => (append ? [...prev, ...slice] : slice)); // Sort alarms by level descending (higher level first: SOS > Danger > Warning > Info)
const sortedSlice = [...slice].sort(
(a, b) => (b.level ?? 0) - (a.level ?? 0)
);
setAlarms((prev) => (append ? [...prev, ...sortedSlice] : sortedSlice));
setOffset(nextOffset); setOffset(nextOffset);
setHasMore(nextOffset + PAGE_SIZE < resp.data?.total!); setHasMore(nextOffset + PAGE_SIZE < resp.data?.total!);
} catch (error) { } catch (error) {

View File

@@ -1,3 +1,4 @@
import { useAppTheme } from "@/hooks/use-app-theme";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"; import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
@@ -36,6 +37,7 @@ export default function DraggablePanel({
}: DraggablePanelProps) { }: DraggablePanelProps) {
const { height: screenHeight } = useWindowDimensions(); const { height: screenHeight } = useWindowDimensions();
const bottomOffset = useBottomTabBarHeight(); const bottomOffset = useBottomTabBarHeight();
const { colors } = useAppTheme();
const minHeight = screenHeight * minHeightPct; const minHeight = screenHeight * minHeightPct;
const maxHeight = screenHeight * maxHeightPct; const maxHeight = screenHeight * maxHeightPct;
@@ -141,7 +143,12 @@ export default function DraggablePanel({
return ( return (
<Animated.View style={[styles.panelContainer, animatedStyle]}> <Animated.View style={[styles.panelContainer, animatedStyle]}>
<View style={[styles.panel, { height: maxHeight }]}> <View
style={[
styles.panel,
{ height: maxHeight, backgroundColor: colors.surface },
]}
>
{/* Header với drag handle và nút toggle */} {/* Header với drag handle và nút toggle */}
<GestureDetector gesture={panGesture}> <GestureDetector gesture={panGesture}>
<Pressable onPress={togglePanel} style={styles.header}> <Pressable onPress={togglePanel} style={styles.header}>
@@ -151,7 +158,7 @@ export default function DraggablePanel({
style={styles.toggleButton} style={styles.toggleButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
> >
<Ionicons name={iconName} size={24} color="#666" /> <Ionicons name={iconName} size={24} color={colors.icon} />
</TouchableOpacity> </TouchableOpacity>
</Pressable> </Pressable>
</GestureDetector> </GestureDetector>

View File

@@ -34,40 +34,54 @@ export const AlarmCard: React.FC<AlarmCardProps> = ({ alarm, onReload }) => {
); );
// Determine level and colors based on alarm level // Determine level and colors based on alarm level
// 0: Thông tin (Xanh lá), 1: Cảnh báo (Vàng), 2: Nguy hiểm (Đỏ), 3: Khẩn cấp SOS (Tím)
const getAlarmConfig = (level?: number) => { const getAlarmConfig = (level?: number) => {
if (level === 3) { switch (level) {
// Danger - Red case 3:
return { // SOS - Khẩn cấp (Purple)
level: 3, return {
icon: "warning" as const, level: 3,
bgColor: "#fee2e2", icon: "alert" as const,
borderColor: "#DC0E0E", bgColor: colors.card,
iconColor: "#dc2626", borderColor: "#9333EA", // Purple
statusBg: "#dcfce7", iconColor: "#9333EA",
statusText: "#166534", statusBg: "#dcfce7",
}; statusText: "#166534",
} else if (level === 2) { };
// Caution - Yellow/Orange case 2:
return { // Nguy hiểm - Đỏ
level: 2, return {
icon: "alert-circle" as const, level: 2,
bgColor: "#fef3c7", icon: "warning" as const,
borderColor: "#FF6C0C", bgColor: colors.card,
iconColor: "#d97706", borderColor: colors.error,
statusBg: "#fef08a", iconColor: colors.error,
statusText: "#713f12", statusBg: "#dcfce7",
}; statusText: "#166534",
} else { };
// Info - Green case 1:
return { // Cảnh báo - Vàng
level: 1, return {
icon: "information-circle" as const, level: 1,
bgColor: "#fffefe", icon: "alert-circle" as const,
borderColor: "#FF937E", bgColor: colors.card,
iconColor: "#FF937E", borderColor: colors.warning,
statusBg: "#dcfce7", iconColor: colors.warning,
statusText: "#166534", statusBg: "#fef08a",
}; statusText: "#713f12",
};
case 0:
default:
// Thông tin - Xanh lá
return {
level: 0,
icon: "information-circle" as const,
bgColor: colors.card,
borderColor: colors.success,
iconColor: colors.success,
statusBg: "#dcfce7",
statusText: "#166534",
};
} }
}; };
@@ -217,14 +231,16 @@ export const AlarmCard: React.FC<AlarmCardProps> = ({ alarm, onReload }) => {
style={[ style={[
styles.statusBadge, styles.statusBadge,
{ {
backgroundColor: alarm.confirmed ? "#8FD14F" : "#EEEEEE", backgroundColor: alarm.confirmed
? "#8FD14F"
: colors.surfaceSecondary,
}, },
]} ]}
> >
<Text <Text
style={[ style={[
styles.statusText, styles.statusText,
{ color: alarm.confirmed ? "#166534" : "black" }, { color: alarm.confirmed ? "#166534" : colors.text },
]} ]}
> >
{alarm.confirmed ? "Đã xác nhận" : "Chờ xác nhận"} {alarm.confirmed ? "Đã xác nhận" : "Chờ xác nhận"}
@@ -262,7 +278,10 @@ export const AlarmCard: React.FC<AlarmCardProps> = ({ alarm, onReload }) => {
Nhập ghi chú xác nhận Nhập ghi chú xác nhận
</Text> </Text>
<TextInput <TextInput
style={[styles.input, { color: colors.text }]} style={[
styles.input,
{ color: colors.text, borderColor: colors.border },
]}
placeholder="Nhập ghi chú" placeholder="Nhập ghi chú"
placeholderTextColor={colors.textSecondary} placeholderTextColor={colors.textSecondary}
multiline multiline
@@ -272,14 +291,20 @@ export const AlarmCard: React.FC<AlarmCardProps> = ({ alarm, onReload }) => {
/> />
<View style={styles.modalActions}> <View style={styles.modalActions}>
<TouchableOpacity <TouchableOpacity
style={[styles.modalButton, styles.cancelButton]} style={[
styles.modalButton,
styles.cancelButton,
{ backgroundColor: colors.surfaceSecondary },
]}
onPress={() => { onPress={() => {
setShowModal(false); setShowModal(false);
setNote(""); setNote("");
}} }}
disabled={submitting} disabled={submitting}
> >
<Text style={styles.cancelText}>Hủy</Text> <Text style={[styles.cancelText, { color: colors.text }]}>
Hủy
</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[ style={[

View File

@@ -1,5 +1,6 @@
import { AlarmData } from "@/app/(tabs)"; import { AlarmData } from "@/app/(tabs)";
import { ThemedText } from "@/components/themed-text"; import { ThemedText } from "@/components/themed-text";
import { useAppTheme } from "@/hooks/use-app-theme";
import { formatTimestamp } from "@/services/time_service"; import { formatTimestamp } from "@/services/time_service";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useCallback } from "react"; import { useCallback } from "react";
@@ -14,61 +15,66 @@ interface AlarmCardProps {
} }
// ============ Config ============ // ============ Config ============
const ALARM_CONFIG: Record< const getAlarmConfig = (isDark: boolean) => ({
AlarmType,
{
icon: keyof typeof Ionicons.glyphMap;
label: string;
bgColor: string;
borderColor: string;
iconBgColor: string;
iconColor: string;
labelColor: string;
}
> = {
entered: { entered: {
icon: "warning", icon: "warning" as keyof typeof Ionicons.glyphMap,
label: "Xâm nhập", label: "Xâm nhập",
bgColor: "bg-red-50", bgColor: isDark ? "rgba(220, 38, 38, 0.15)" : "#FEF2F2",
borderColor: "border-red-200", borderColor: isDark ? "rgba(220, 38, 38, 0.3)" : "#FECACA",
iconBgColor: "bg-red-100", iconBgColor: isDark ? "rgba(220, 38, 38, 0.25)" : "#FEE2E2",
iconColor: "#DC2626", iconColor: isDark ? "#F87171" : "#DC2626",
labelColor: "text-red-600", labelColor: isDark ? "#F87171" : "#DC2626",
}, },
approaching: { approaching: {
icon: "alert-circle", icon: "alert-circle" as keyof typeof Ionicons.glyphMap,
label: "Tiếp cận", label: "Tiếp cận",
bgColor: "bg-amber-50", bgColor: isDark ? "rgba(217, 119, 6, 0.15)" : "#FFFBEB",
borderColor: "border-amber-200", borderColor: isDark ? "rgba(217, 119, 6, 0.3)" : "#FDE68A",
iconBgColor: "bg-amber-100", iconBgColor: isDark ? "rgba(217, 119, 6, 0.25)" : "#FEF3C7",
iconColor: "#D97706", iconColor: isDark ? "#FBBF24" : "#D97706",
labelColor: "text-amber-600", labelColor: isDark ? "#FBBF24" : "#D97706",
}, },
fishing: { fishing: {
icon: "fish", icon: "fish" as keyof typeof Ionicons.glyphMap,
label: "Đánh bắt", label: "Đánh bắt",
bgColor: "bg-orange-50", bgColor: isDark ? "rgba(234, 88, 12, 0.15)" : "#FFF7ED",
borderColor: "border-orange-200", borderColor: isDark ? "rgba(234, 88, 12, 0.3)" : "#FED7AA",
iconBgColor: "bg-orange-100", iconBgColor: isDark ? "rgba(234, 88, 12, 0.25)" : "#FFEDD5",
iconColor: "#EA580C", iconColor: isDark ? "#FB923C" : "#EA580C",
labelColor: "text-orange-600", labelColor: isDark ? "#FB923C" : "#EA580C",
}, },
}; });
// ============ AlarmCard Component ============ // ============ AlarmCard Component ============
const AlarmCard = ({ alarm, onPress }: AlarmCardProps) => { const AlarmCard = ({ alarm, onPress }: AlarmCardProps) => {
const config = ALARM_CONFIG[alarm.type]; const { colors, utils } = useAppTheme();
const isDark = utils.isDark;
const alarmConfig = getAlarmConfig(isDark);
const config = alarmConfig[alarm.type];
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={onPress} onPress={onPress}
activeOpacity={0.7} activeOpacity={0.7}
className={`rounded-2xl p-4 ${config.bgColor} ${config.borderColor} border shadow-sm`} style={{
borderRadius: 16,
padding: 16,
backgroundColor: config.bgColor,
borderWidth: 1,
borderColor: config.borderColor,
}}
> >
<View className="flex-row items-start gap-3"> <View className="flex-row items-start gap-3">
{/* Icon Container */} {/* Icon Container */}
<View <View
className={`w-12 h-12 rounded-xl items-center justify-center ${config.iconBgColor}`} style={{
width: 48,
height: 48,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
backgroundColor: config.iconBgColor,
}}
> >
<Ionicons name={config.icon} size={24} color={config.iconColor} /> <Ionicons name={config.icon} size={24} color={config.iconColor} />
</View> </View>
@@ -77,12 +83,31 @@ const AlarmCard = ({ alarm, onPress }: AlarmCardProps) => {
<View className="flex-1"> <View className="flex-1">
{/* Header: Ship name + Badge */} {/* Header: Ship name + Badge */}
<View className="flex-row items-center justify-between mb-1"> <View className="flex-row items-center justify-between mb-1">
<ThemedText className="text-base font-bold text-gray-800 flex-1 mr-2"> <ThemedText
style={{
fontSize: 16,
fontWeight: "700",
color: colors.text,
flex: 1,
marginRight: 8,
}}
>
{alarm.ship_name || alarm.thing_id} {alarm.ship_name || alarm.thing_id}
</ThemedText> </ThemedText>
<View className={`px-2 py-1 rounded-full ${config.iconBgColor}`}> <View
style={{
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 9999,
backgroundColor: config.iconBgColor,
}}
>
<ThemedText <ThemedText
className={`text-xs font-semibold ${config.labelColor}`} style={{
fontSize: 12,
fontWeight: "600",
color: config.labelColor,
}}
> >
{config.label} {config.label}
</ThemedText> </ThemedText>
@@ -90,15 +115,27 @@ const AlarmCard = ({ alarm, onPress }: AlarmCardProps) => {
</View> </View>
{/* Zone Info */} {/* Zone Info */}
<ThemedText className="text-xs text-gray-600 mb-2" numberOfLines={2}> <ThemedText
style={{
fontSize: 12,
color: colors.textSecondary,
marginBottom: 8,
}}
numberOfLines={2}
>
{alarm.zone.message || alarm.zone.zone_name} {alarm.zone.message || alarm.zone.zone_name}
</ThemedText> </ThemedText>
{/* Footer: Zone ID + Time */} {/* Footer: Zone ID + Time */}
<View className="flex-row items-center justify-between"> <View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-1"> <View className="flex-row items-center gap-1">
<Ionicons name="time-outline" size={20} color="#6B7280" /> <Ionicons name="time-outline" size={20} color={colors.icon} />
<ThemedText className="text-xs text-gray-500"> <ThemedText
style={{
fontSize: 12,
color: colors.textSecondary,
}}
>
{formatTimestamp(alarm.zone.gps_time)} {formatTimestamp(alarm.zone.gps_time)}
</ThemedText> </ThemedText>
</View> </View>

View File

@@ -13,28 +13,31 @@ interface ShipInfoProps {
const ShipInfo = ({ thingMetadata }: ShipInfoProps) => { const ShipInfo = ({ thingMetadata }: ShipInfoProps) => {
const { t } = useI18n(); const { t } = useI18n();
const { colors } = useAppTheme(); const { colors, utils } = useAppTheme();
// Định nghĩa màu sắc theo trạng thái const isDark = utils.isDark;
// Định nghĩa màu sắc theo trạng thái - hỗ trợ cả light và dark theme
const statusConfig = { const statusConfig = {
normal: { normal: {
dotColor: "bg-green-500", dotColor: colors.success,
badgeColor: "bg-green-100", badgeColor: isDark ? "rgba(52, 199, 89, 0.2)" : "#dcfce7",
badgeTextColor: "text-green-700", badgeTextColor: isDark ? "#30D158" : "#15803d",
badgeText: "Bình thường", badgeText: "Bình thường",
}, },
warning: { warning: {
dotColor: "bg-yellow-500", dotColor: colors.warning,
badgeColor: "bg-yellow-100", badgeColor: isDark ? "rgba(255, 102, 0, 0.2)" : "#fef3c7",
badgeTextColor: "text-yellow-700", badgeTextColor: isDark ? "#ff9500" : "#b45309",
badgeText: "Cảnh báo", badgeText: "Cảnh báo",
}, },
danger: { danger: {
dotColor: "bg-red-500", dotColor: colors.error,
badgeColor: "bg-red-100", badgeColor: isDark ? "rgba(255, 69, 58, 0.2)" : "#fee2e2",
badgeTextColor: "text-red-700", badgeTextColor: isDark ? "#FF453A" : "#b91c1c",
badgeText: "Nguy hiểm", badgeText: "Nguy hiểm",
}, },
}; };
const getThingStatus = () => { const getThingStatus = () => {
switch (thingMetadata?.state_level) { switch (thingMetadata?.state_level) {
case STATUS_NORMAL: case STATUS_NORMAL:
@@ -67,57 +70,82 @@ const ShipInfo = ({ thingMetadata }: ShipInfoProps) => {
<View className="flex-row items-center justify-between mb-3"> <View className="flex-row items-center justify-between mb-3">
<View className="flex-row items-center gap-2"> <View className="flex-row items-center gap-2">
{/* Status dot */} {/* Status dot */}
<View className={`h-3 w-3 rounded-full ${currentStatus.dotColor}`} /> <View
style={{
height: 12,
width: 12,
borderRadius: 6,
backgroundColor: currentStatus.dotColor,
}}
/>
{/* Tên tàu */} {/* Tên tàu */}
<Text className="text-lg font-semibold text-gray-900"> <Text
style={{
fontSize: 18,
fontWeight: "600",
color: colors.text,
}}
>
{thingMetadata?.ship_name} {thingMetadata?.ship_name}
</Text> </Text>
</View> </View>
{/* Badge trạng thái */} {/* Badge trạng thái */}
<View className={`px-3 py-1 rounded-full ${currentStatus.badgeColor}`}> <View
style={{
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 9999,
backgroundColor: currentStatus.badgeColor,
}}
>
<Text <Text
className={`text-md font-medium ${currentStatus.badgeTextColor}`} style={{
fontSize: 14,
fontWeight: "500",
color: currentStatus.badgeTextColor,
}}
> >
{currentStatus.badgeText} {currentStatus.badgeText}
</Text> </Text>
</View> </View>
</View> </View>
{/* Mã tàu */} {/* Tốc độ và hướng */}
{/* <Text className="text-base text-gray-600 mb-2">{shipCode}</Text> */}
<View className="flex-row items-center justify-between gap-2 mb-3"> <View className="flex-row items-center justify-between gap-2 mb-3">
<View className="flex-row items-center gap-2 w-2/3"> <View className="flex-row items-center gap-2 w-2/3">
<Ionicons name="speedometer-outline" size={16} color="#6B7280" /> <Ionicons name="speedometer-outline" size={16} color={colors.icon} />
<Text className="text-md text-gray-600"> <Text style={{ fontSize: 14, color: colors.textSecondary }}>
{kmhToKnot(gpsData.s || 0)} {t("home.speed_units")} {kmhToKnot(gpsData.s || 0)} {t("home.speed_units")}
</Text> </Text>
</View> </View>
<View className="flex-row items-start gap-2 w-1/3 "> <View className="flex-row items-start gap-2 w-1/3 ">
<Ionicons name="compass-outline" size={16} color="#6B7280" /> <Ionicons name="compass-outline" size={16} color={colors.icon} />
<Text className="text-md text-gray-600">{gpsData.h}°</Text> <Text style={{ fontSize: 14, color: colors.textSecondary }}>
{gpsData.h}°
</Text>
</View> </View>
</View> </View>
{/* Tọa độ */} {/* Tọa độ */}
<View className="flex-row items-center justify-between gap-2 mb-2"> <View className="flex-row items-center justify-between gap-2 mb-2">
<View className="flex-row items-center gap-2 w-2/3"> <View className="flex-row items-center gap-2 w-2/3">
<Ionicons name="location-outline" size={16} color="#6B7280" /> <Ionicons name="location-outline" size={16} color={colors.icon} />
<Text className="text-md text-gray-600"> <Text style={{ fontSize: 14, color: colors.textSecondary }}>
{convertToDMS(gpsData.lat || 0, true)}, {convertToDMS(gpsData.lat || 0, true)},
{convertToDMS(gpsData.lon || 0, false)} {convertToDMS(gpsData.lon || 0, false)}
</Text> </Text>
</View> </View>
<View className=" flex-row items-start gap-2 w-1/3 "> <View className=" flex-row items-start gap-2 w-1/3 ">
<Ionicons name="time-outline" size={16} color="#6B7280" /> <Ionicons name="time-outline" size={16} color={colors.icon} />
<Text className="text-md text-gray-600"> <Text style={{ fontSize: 14, color: colors.textSecondary }}>
{formatRelativeTime(gpsData.t)} {formatRelativeTime(gpsData.t)}
</Text> </Text>
</View> </View>
</View> </View>
{/* <View className="flex">
<Description title="Trạng thái" description={thingMetadata?.state} /> {/* Trạng thái */}
</View> */}
{thingMetadata?.state !== "" && ( {thingMetadata?.state !== "" && (
<View className="flex-row items-center gap-2"> <View className="flex-row items-center gap-2">
<ThemedText style={{ color: colors.textSecondary, fontSize: 15 }}> <ThemedText style={{ color: colors.textSecondary, fontSize: 15 }}>