diff --git a/app/(tabs)/diary.tsx b/app/(tabs)/diary.tsx index 9dc8fed..cb7bcd6 100644 --- a/app/(tabs)/diary.tsx +++ b/app/(tabs)/diary.tsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { + ActivityIndicator, + FlatList, Platform, - ScrollView, StyleSheet, Text, TouchableOpacity, @@ -29,6 +30,12 @@ export default function diary() { selectedShip: null, // Tàu được chọn }); + // State for pagination + const [allTrips, setAllTrips] = useState([]); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); + const isInitialLoad = useRef(true); + // Body call API things (đang fix cứng) const payloadThings: Model.SearchThingBody = { 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 useEffect(() => { + isInitialLoad.current = true; + setAllTrips([]); + setHasMore(true); 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(() => { - getTripsList(payloadTrips); - console.log("Payload trips:", payloadTrips); - }, [payloadTrips]); + if (tripsList?.trips && tripsList.trips.length > 0) { + if (isInitialLoad.current || payloadTrips.offset === 0) { + // 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 = () => { setShowFilterModal(true); @@ -85,10 +122,11 @@ export default function diary() { const handleApplyFilters = (newFilters: FilterValues) => { 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 const updatedPayload: Model.TripListBody = { ...payloadTrips, + offset: 0, // Reset về đầu khi filter metadata: { ...payloadTrips.metadata, from: newFilters.startDate @@ -104,10 +142,30 @@ export default function diary() { }, }; + // Reset trips khi apply filter mới + setAllTrips([]); + setHasMore(true); setPayloadTrips(updatedPayload); + getTripsList(updatedPayload); 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) => { // TODO: Navigate to trip detail 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 }) => ( + 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 ( + + + + {t("diary.loadingMore")} + + + ); + }; + + // 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 ( + + + + ); + } + // Chỉ hiển thị "không có dữ liệu" khi đã load xong và thực sự không có trips + return ( + + + {t("diary.noTripsFound")} + + + ); + }; + return ( @@ -189,33 +300,22 @@ export default function diary() { {t("diary.tripListCount", { count: tripsList?.total || 0 })} - {/* Trip List */} - - {tripsList?.trips?.map((trip) => ( - handleTripPress(trip.id)} - onView={() => handleViewTrip(trip.id)} - onEdit={() => handleEditTrip(trip.id)} - onTeam={() => handleViewTeam(trip.id)} - onSend={() => handleSendTrip(trip.id)} - onDelete={() => handleDeleteTrip(trip.id)} - /> - ))} - - {(!tripsList || !tripsList.trips || tripsList.trips.length === 0) && ( - - - {t("diary.noTripsFound")} - - - )} - + onEndReached={handleLoadMore} + onEndReachedThreshold={0.5} + ListFooterComponent={renderFooter} + ListEmptyComponent={renderEmpty} + removeClippedSubviews={true} + maxToRenderPerBatch={10} + windowSize={10} + initialNumToRender={10} + /> {/* Filter Modal */} @@ -289,9 +389,6 @@ const styles = StyleSheet.create({ default: "System", }), }, - scrollView: { - flex: 1, - }, scrollContent: { paddingBottom: 20, }, @@ -308,4 +405,19 @@ const styles = StyleSheet.create({ 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", + }), + }, }); diff --git a/components/diary/TripCard.tsx b/components/diary/TripCard.tsx index eefcd57..392d17c 100644 --- a/components/diary/TripCard.tsx +++ b/components/diary/TripCard.tsx @@ -204,16 +204,16 @@ export default function TripCard({ const styles = StyleSheet.create({ card: { - borderRadius: 12, - padding: 16, - marginBottom: 12, + borderRadius: 8, + padding: 12, + marginBottom: 8, shadowColor: "#000", shadowOffset: { width: 0, - height: 2, + height: 1, }, shadowOpacity: 0.05, - shadowRadius: 8, + shadowRadius: 4, elevation: 2, borderWidth: 1, }, @@ -221,7 +221,7 @@ const styles = StyleSheet.create({ flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", - marginBottom: 16, + marginBottom: 8, }, headerLeft: { flexDirection: "row", @@ -229,7 +229,7 @@ const styles = StyleSheet.create({ flex: 1, }, titleContainer: { - marginLeft: 12, + marginLeft: 8, flex: 1, }, title: { @@ -244,9 +244,9 @@ const styles = StyleSheet.create({ }, badge: { - paddingHorizontal: 12, - paddingVertical: 4, - borderRadius: 12, + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 8, }, badgeText: { fontSize: 12, @@ -258,13 +258,13 @@ const styles = StyleSheet.create({ }), }, infoGrid: { - gap: 12, - marginBottom: 12, + gap: 6, + marginBottom: 8, }, infoRow: { flexDirection: "row", justifyContent: "space-between", - paddingVertical: 8, + paddingVertical: 4, }, label: { fontSize: 14, @@ -285,7 +285,7 @@ const styles = StyleSheet.create({ }, divider: { height: 1, - marginVertical: 12, + marginVertical: 8, }, actionsContainer: { flexDirection: "row", @@ -296,6 +296,7 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", gap: 4, + paddingVertical: 4, }, actionText: { fontSize: 14, diff --git a/locales/en.json b/locales/en.json index 953f8fd..f990cc1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -67,6 +67,7 @@ "tripList": "Trip List", "tripListCount": "Trip List ({{count}})", "noTripsFound": "No matching trips found", + "loadingMore": "Loading more...", "reset": "Reset", "apply": "Apply", "selectedFilters": "Selected filters:", diff --git a/locales/vi.json b/locales/vi.json index b17dc85..389f7ef 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -67,6 +67,7 @@ "tripList": "Danh sách chuyến đi", "tripListCount": "Danh sách chuyến đi ({{count}})", "noTripsFound": "Không tìm thấy chuyến đi phù hợp", + "loadingMore": "Đang tải thêm...", "reset": "Đặt lại", "apply": "Áp dụng", "selectedFilters": "Bộ lọc đã chọn:",