Gip khi cuộn xuống ( tab diary )
This commit is contained in:
@@ -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",
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:",
|
||||||
|
|||||||
@@ -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:",
|
||||||
|
|||||||
Reference in New Issue
Block a user