Gip khi cuộn xuống ( tab diary )

This commit is contained in:
2025-12-08 10:05:04 +07:00
parent 695066a5e7
commit 347bd1a7c1
4 changed files with 165 additions and 50 deletions

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { import {
ActivityIndicator,
FlatList,
Platform, Platform,
ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
@@ -29,6 +30,12 @@ export default function diary() {
selectedShip: null, // Tàu được chọn selectedShip: null, // Tàu được chọn
}); });
// State for pagination
const [allTrips, setAllTrips] = useState<any[]>([]);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const isInitialLoad = useRef(true);
// Body call API things (đang fix cứng) // Body call API things (đang fix cứng)
const payloadThings: Model.SearchThingBody = { const payloadThings: Model.SearchThingBody = {
offset: 0, offset: 0,
@@ -65,18 +72,48 @@ export default function diary() {
}, },
}); });
const { tripsList, getTripsList } = useTripsList(); const { tripsList, getTripsList, loading } = useTripsList();
// console.log("Payload trips:", payloadTrips);
// Gọi API trips lần đầu // Gọi API trips lần đầu
useEffect(() => { useEffect(() => {
isInitialLoad.current = true;
setAllTrips([]);
setHasMore(true);
getTripsList(payloadTrips); getTripsList(payloadTrips);
}, []); }, []);
// Gọi lại API khi payload thay đổi (do filter) // Xử lý khi nhận được dữ liệu mới từ API
useEffect(() => { useEffect(() => {
getTripsList(payloadTrips); if (tripsList?.trips && tripsList.trips.length > 0) {
console.log("Payload trips:", payloadTrips); if (isInitialLoad.current || payloadTrips.offset === 0) {
}, [payloadTrips]); // Load lần đầu hoặc reset do filter
setAllTrips(tripsList.trips);
isInitialLoad.current = false;
} else {
// Load more - append data
setAllTrips((prevTrips) => {
// Tránh duplicate bằng cách check ID
const existingIds = new Set(prevTrips.map((trip) => trip.id));
const newTrips = tripsList.trips!.filter(
(trip) => !existingIds.has(trip.id)
);
return [...prevTrips, ...newTrips];
});
}
// Check if có thêm data không
const totalFetched = payloadTrips.offset + tripsList.trips.length;
setHasMore(totalFetched < (tripsList.total || 0));
setIsLoadingMore(false);
} else if (tripsList && payloadTrips.offset === 0) {
// Trường hợp API trả về rỗng khi filter
setAllTrips([]);
setHasMore(false);
setIsLoadingMore(false);
}
}, [tripsList]);
const handleFilter = () => { const handleFilter = () => {
setShowFilterModal(true); setShowFilterModal(true);
@@ -85,10 +122,11 @@ export default function diary() {
const handleApplyFilters = (newFilters: FilterValues) => { const handleApplyFilters = (newFilters: FilterValues) => {
setFilters(newFilters); setFilters(newFilters);
// Cập nhật payload với filter mới // Cập nhật payload với filter mới và reset offset
// Lưu ý: status gửi lên server là string // Lưu ý: status gửi lên server là string
const updatedPayload: Model.TripListBody = { const updatedPayload: Model.TripListBody = {
...payloadTrips, ...payloadTrips,
offset: 0, // Reset về đầu khi filter
metadata: { metadata: {
...payloadTrips.metadata, ...payloadTrips.metadata,
from: newFilters.startDate from: newFilters.startDate
@@ -104,10 +142,30 @@ export default function diary() {
}, },
}; };
// Reset trips khi apply filter mới
setAllTrips([]);
setHasMore(true);
setPayloadTrips(updatedPayload); setPayloadTrips(updatedPayload);
getTripsList(updatedPayload);
setShowFilterModal(false); setShowFilterModal(false);
}; };
// Hàm load more data khi scroll đến cuối
const handleLoadMore = useCallback(() => {
if (isLoadingMore || !hasMore) {
return;
}
setIsLoadingMore(true);
const newOffset = payloadTrips.offset + payloadTrips.limit;
const updatedPayload = {
...payloadTrips,
offset: newOffset,
};
setPayloadTrips(updatedPayload);
getTripsList(updatedPayload);
}, [isLoadingMore, hasMore, payloadTrips]);
const handleTripPress = (tripId: string) => { const handleTripPress = (tripId: string) => {
// TODO: Navigate to trip detail // TODO: Navigate to trip detail
console.log("Trip pressed:", tripId); console.log("Trip pressed:", tripId);
@@ -157,6 +215,59 @@ export default function diary() {
}, },
}; };
// Render mỗi item trong FlatList
const renderTripItem = useCallback(
({ item }: { item: any }) => (
<TripCard
trip={item}
onPress={() => handleTripPress(item.id)}
onView={() => handleViewTrip(item.id)}
onEdit={() => handleEditTrip(item.id)}
onTeam={() => handleViewTeam(item.id)}
onSend={() => handleSendTrip(item.id)}
onDelete={() => handleDeleteTrip(item.id)}
/>
),
[]
);
// Key extractor cho FlatList
const keyExtractor = useCallback((item: any) => item.id, []);
// Loading indicator khi load more
const renderFooter = () => {
// Không hiển thị loading footer nếu không có dữ liệu hoặc không đang load more
if (!isLoadingMore || allTrips.length === 0) return null;
return (
<View style={styles.loadingFooter}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>
{t("diary.loadingMore")}
</Text>
</View>
);
};
// Empty component
const renderEmpty = () => {
// Hiển thị loading khi đang load (lần đầu hoặc load more) và chưa có dữ liệu
if (loading && allTrips.length === 0) {
return (
<View style={styles.emptyState}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
// Chỉ hiển thị "không có dữ liệu" khi đã load xong và thực sự không có trips
return (
<View style={styles.emptyState}>
<Text style={[styles.emptyText, themedStyles.emptyText]}>
{t("diary.noTripsFound")}
</Text>
</View>
);
};
return ( return (
<SafeAreaView style={[styles.safeArea, themedStyles.safeArea]} edges={["top"]}> <SafeAreaView style={[styles.safeArea, themedStyles.safeArea]} edges={["top"]}>
<View style={styles.container}> <View style={styles.container}>
@@ -189,33 +300,22 @@ export default function diary() {
{t("diary.tripListCount", { count: tripsList?.total || 0 })} {t("diary.tripListCount", { count: tripsList?.total || 0 })}
</Text> </Text>
{/* Trip List */} {/* Trip List with FlatList */}
<ScrollView <FlatList
style={styles.scrollView} data={allTrips}
renderItem={renderTripItem}
keyExtractor={keyExtractor}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> onEndReached={handleLoadMore}
{tripsList?.trips?.map((trip) => ( onEndReachedThreshold={0.5}
<TripCard ListFooterComponent={renderFooter}
key={trip.id} ListEmptyComponent={renderEmpty}
trip={trip} removeClippedSubviews={true}
onPress={() => handleTripPress(trip.id)} maxToRenderPerBatch={10}
onView={() => handleViewTrip(trip.id)} windowSize={10}
onEdit={() => handleEditTrip(trip.id)} initialNumToRender={10}
onTeam={() => handleViewTeam(trip.id)}
onSend={() => handleSendTrip(trip.id)}
onDelete={() => handleDeleteTrip(trip.id)}
/> />
))}
{(!tripsList || !tripsList.trips || tripsList.trips.length === 0) && (
<View style={styles.emptyState}>
<Text style={[styles.emptyText, themedStyles.emptyText]}>
{t("diary.noTripsFound")}
</Text>
</View>
)}
</ScrollView>
</View> </View>
{/* Filter Modal */} {/* Filter Modal */}
@@ -289,9 +389,6 @@ const styles = StyleSheet.create({
default: "System", default: "System",
}), }),
}, },
scrollView: {
flex: 1,
},
scrollContent: { scrollContent: {
paddingBottom: 20, paddingBottom: 20,
}, },
@@ -308,4 +405,19 @@ const styles = StyleSheet.create({
default: "System", default: "System",
}), }),
}, },
loadingFooter: {
paddingVertical: 20,
alignItems: "center",
justifyContent: "center",
flexDirection: "row",
gap: 10,
},
loadingText: {
fontSize: 14,
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
}); });

View File

@@ -204,16 +204,16 @@ export default function TripCard({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
borderRadius: 12, borderRadius: 8,
padding: 16, padding: 12,
marginBottom: 12, marginBottom: 8,
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 2, height: 1,
}, },
shadowOpacity: 0.05, shadowOpacity: 0.05,
shadowRadius: 8, shadowRadius: 4,
elevation: 2, elevation: 2,
borderWidth: 1, borderWidth: 1,
}, },
@@ -221,7 +221,7 @@ const styles = StyleSheet.create({
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "flex-start", alignItems: "flex-start",
marginBottom: 16, marginBottom: 8,
}, },
headerLeft: { headerLeft: {
flexDirection: "row", flexDirection: "row",
@@ -229,7 +229,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
titleContainer: { titleContainer: {
marginLeft: 12, marginLeft: 8,
flex: 1, flex: 1,
}, },
title: { title: {
@@ -244,9 +244,9 @@ const styles = StyleSheet.create({
}, },
badge: { badge: {
paddingHorizontal: 12, paddingHorizontal: 8,
paddingVertical: 4, paddingVertical: 2,
borderRadius: 12, borderRadius: 8,
}, },
badgeText: { badgeText: {
fontSize: 12, fontSize: 12,
@@ -258,13 +258,13 @@ const styles = StyleSheet.create({
}), }),
}, },
infoGrid: { infoGrid: {
gap: 12, gap: 6,
marginBottom: 12, marginBottom: 8,
}, },
infoRow: { infoRow: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
paddingVertical: 8, paddingVertical: 4,
}, },
label: { label: {
fontSize: 14, fontSize: 14,
@@ -285,7 +285,7 @@ const styles = StyleSheet.create({
}, },
divider: { divider: {
height: 1, height: 1,
marginVertical: 12, marginVertical: 8,
}, },
actionsContainer: { actionsContainer: {
flexDirection: "row", flexDirection: "row",
@@ -296,6 +296,7 @@ const styles = StyleSheet.create({
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 4, gap: 4,
paddingVertical: 4,
}, },
actionText: { actionText: {
fontSize: 14, fontSize: 14,

View File

@@ -67,6 +67,7 @@
"tripList": "Trip List", "tripList": "Trip List",
"tripListCount": "Trip List ({{count}})", "tripListCount": "Trip List ({{count}})",
"noTripsFound": "No matching trips found", "noTripsFound": "No matching trips found",
"loadingMore": "Loading more...",
"reset": "Reset", "reset": "Reset",
"apply": "Apply", "apply": "Apply",
"selectedFilters": "Selected filters:", "selectedFilters": "Selected filters:",

View File

@@ -67,6 +67,7 @@
"tripList": "Danh sách chuyến đi", "tripList": "Danh sách chuyến đi",
"tripListCount": "Danh sách chuyến đi ({{count}})", "tripListCount": "Danh sách chuyến đi ({{count}})",
"noTripsFound": "Không tìm thấy chuyến đi phù hợp", "noTripsFound": "Không tìm thấy chuyến đi phù hợp",
"loadingMore": "Đang tải thêm...",
"reset": "Đặt lại", "reset": "Đặt lại",
"apply": "Áp dụng", "apply": "Áp dụng",
"selectedFilters": "Bộ lọc đã chọn:", "selectedFilters": "Bộ lọc đã chọn:",