Files
sgw-owner-app/app/(tabs)/warning.tsx
2025-12-09 11:37:19 +07:00

315 lines
8.6 KiB
TypeScript

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<Model.AlarmPayload>({
offset: 0,
limit: PAGE_SIZE,
order: "time",
dir: "desc",
});
const [alarms, setAlarms] = useState<Model.Alarm[]>([]);
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 }) => (
<AlarmCard alarm={item} onReload={handleAlarmReload} />
),
[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 (
<SafeAreaView style={styles.container} edges={["top"]}>
<ThemedView style={styles.content}>
{/* Header */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<ThemedText style={styles.titleText}>Cảnh báo</ThemedText>
</View>
<View style={styles.badgeContainer}>
<TouchableOpacity onPress={toggleSearchForm}>
<Ionicons
size={20}
name="filter-outline"
color={hasFilters ? colors.primary : colors.text}
/>
</TouchableOpacity>
</View>
</View>
{/* Search Form */}
{isShowSearchForm && (
<Animated.View style={{ opacity: formOpacity, zIndex: 100 }}>
<AlarmSearchForm
initialValue={{
name: defaultAlarmParams.name || "",
level: defaultAlarmParams.level || 0,
confirmed: defaultAlarmParams.confirmed,
}}
onSubmit={onSearch}
onReset={toggleSearchForm}
/>
</Animated.View>
)}
{/* Alarm List */}
{alarms.length > 0 ? (
<FlatList
data={alarms}
renderItem={renderAlarmCard}
keyExtractor={keyExtractor}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
refreshing={refreshing}
onRefresh={handleRefresh}
ListFooterComponent={
isLoadingMore ? (
<View style={styles.footer}>
<ActivityIndicator size="small" color="#dc2626" />
<ThemedText style={styles.footerText}>Đang tải...</ThemedText>
</View>
) : null
}
/>
) : (
<View style={styles.emptyContainer}>
<Ionicons name="shield-checkmark" size={48} color="#16a34a" />
<ThemedText style={styles.emptyText}>
Không cảnh báo nào
</ThemedText>
</View>
)}
</ThemedView>
</SafeAreaView>
);
};
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",
},
});