import { AlarmCard } from "@/components/alarm/AlarmCard"; import AlarmSearchForm from "@/components/alarm/AlarmSearchForm"; import { ThemedText } from "@/components/themed-text"; import { ThemedView } from "@/components/themed-view"; import { queryAlarms } from "@/controller/AlarmController"; import { useThemeContext } from "@/hooks/use-theme-context"; import { Ionicons } from "@expo/vector-icons"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, Animated, FlatList, LayoutAnimation, Platform, StyleSheet, TouchableOpacity, View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; const PAGE_SIZE = 2; const WarningScreen = () => { const [defaultAlarmParams, setDefaultAlarmParams] = useState({ offset: 0, limit: PAGE_SIZE, order: "time", dir: "desc", }); const [alarms, setAlarms] = useState([]); const [loading, setLoading] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [refreshing, setRefreshing] = useState(false); const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(true); const [isShowSearchForm, setIsShowSearchForm] = useState(false); const [formOpacity] = useState(new Animated.Value(0)); const { colors } = useThemeContext(); const hasFilters = useMemo(() => { return Boolean( (defaultAlarmParams as any)?.name || ((defaultAlarmParams as any)?.level !== undefined && (defaultAlarmParams as any).level !== 0) || (defaultAlarmParams as any)?.confirmed !== undefined ); }, [defaultAlarmParams]); useEffect(() => { getAlarmsData(0, false); }, []); useEffect(() => { if (isShowSearchForm) { // Reset opacity to 0, then animate to 1 formOpacity.setValue(0); Animated.timing(formOpacity, { toValue: 1, duration: 300, useNativeDriver: true, }).start(); } else { formOpacity.setValue(0); } }, [isShowSearchForm, formOpacity]); const getAlarmsData = async ( nextOffset = 0, append = false, paramsOverride?: Model.AlarmPayload ) => { try { if (append) setIsLoadingMore(true); else setLoading(true); // console.log("Call alarm with offset: ", nextOffset); const usedParams = paramsOverride ?? defaultAlarmParams; // console.log("params: ", usedParams); const resp = await queryAlarms({ ...usedParams, offset: nextOffset, }); const slice = resp.data?.alarms ?? []; setAlarms((prev) => (append ? [...prev, ...slice] : slice)); setOffset(nextOffset); setHasMore(nextOffset + PAGE_SIZE < resp.data?.total!); } catch (error) { console.error("Cannot get Alarm Data: ", error); } finally { setLoading(false); setIsLoadingMore(false); setRefreshing(false); } }; const handleAlarmReload = useCallback((onReload: boolean) => { if (onReload) { getAlarmsData(0, false, undefined); } }, []); const renderAlarmCard = useCallback( ({ item }: { item: Model.Alarm }) => ( ), [handleAlarmReload] ); const keyExtractor = useCallback( (item: Model.Alarm, index: number) => `${`${item.id} + ${item.time} + ${item.level} + ${index}` || index}`, [] ); const handleLoadMore = useCallback(() => { if (isLoadingMore || !hasMore) return; const nextOffset = offset + PAGE_SIZE; getAlarmsData(nextOffset, true); }, [isLoadingMore, hasMore, offset]); const handleRefresh = useCallback(() => { setRefreshing(true); getAlarmsData(0, false, undefined); }, []); const onSearch = useCallback( (values: { name?: string; level?: number; confirmed?: boolean }) => { const mapped = { offset: 0, limit: defaultAlarmParams.limit, order: defaultAlarmParams.order, dir: defaultAlarmParams.dir, ...(values.name && { name: values.name }), ...(values.level && values.level !== 0 && { level: values.level }), ...(values.confirmed !== undefined && { confirmed: values.confirmed }), }; setDefaultAlarmParams(mapped); // Call getAlarmsData with the mapped params directly so the // request uses the updated params immediately (setState is async) getAlarmsData(0, false, mapped); toggleSearchForm(); }, [defaultAlarmParams] ); const toggleSearchForm = useCallback(() => { if (Platform.OS === "ios") { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); } if (isShowSearchForm) { // Hide form Animated.timing(formOpacity, { toValue: 0, duration: 300, useNativeDriver: true, }).start(() => { setIsShowSearchForm(false); }); } else { // Show form setIsShowSearchForm(true); Animated.timing(formOpacity, { toValue: 1, duration: 300, useNativeDriver: true, }).start(); } }, [isShowSearchForm, formOpacity]); return ( {/* Header */} Cảnh báo {/* Search Form */} {isShowSearchForm && ( )} {/* Alarm List */} {alarms.length > 0 ? ( Đang tải... ) : null } /> ) : ( Không có cảnh báo nào )} ); }; export default WarningScreen; const styles = StyleSheet.create({ container: { flex: 1, }, content: { flex: 1, }, header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: "#e5e7eb", }, headerLeft: { flexDirection: "row", alignItems: "center", gap: 12, }, iconContainer: { width: 40, height: 40, borderRadius: 8, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center", }, titleText: { fontSize: 24, fontWeight: "700", }, badgeContainer: { // backgroundColor: "#dc2626", paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, }, badgeText: { color: "#fff", fontSize: 14, fontWeight: "600", }, listContent: { paddingHorizontal: 16, paddingVertical: 16, }, footer: { paddingVertical: 16, alignItems: "center", justifyContent: "center", flexDirection: "row", gap: 8, }, footerText: { fontSize: 14, fontWeight: "500", }, emptyContainer: { flex: 1, alignItems: "center", justifyContent: "center", gap: 16, }, emptyText: { fontSize: 16, fontWeight: "500", }, });