424 lines
11 KiB
TypeScript
424 lines
11 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
FlatList,
|
|
Platform,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import FilterButton from "@/components/diary/FilterButton";
|
|
import TripCard from "@/components/diary/TripCard";
|
|
import FilterModal, { FilterValues } from "@/components/diary/FilterModal";
|
|
import { useThings } from "@/state/use-thing";
|
|
import { useTripsList } from "@/state/use-tripslist";
|
|
import dayjs from "dayjs";
|
|
import { useI18n } from "@/hooks/use-i18n";
|
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
|
|
export default function diary() {
|
|
const { t } = useI18n();
|
|
const { colors } = useThemeContext();
|
|
const [showFilterModal, setShowFilterModal] = useState(false);
|
|
const [filters, setFilters] = useState<FilterValues>({
|
|
status: null,
|
|
startDate: null,
|
|
endDate: null,
|
|
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)
|
|
const payloadThings: Model.SearchThingBody = {
|
|
offset: 0,
|
|
limit: 200,
|
|
order: "name",
|
|
dir: "asc",
|
|
metadata: {
|
|
not_empty: "ship_name, ship_reg_number",
|
|
},
|
|
};
|
|
|
|
// Gọi API things
|
|
const { getThings } = useThings();
|
|
useEffect(() => {
|
|
getThings(payloadThings);
|
|
}, []);
|
|
|
|
// State cho payload trips
|
|
const [payloadTrips, setPayloadTrips] = useState<Model.TripListBody>({
|
|
name: "",
|
|
order: "",
|
|
dir: "desc",
|
|
offset: 0,
|
|
limit: 10,
|
|
metadata: {
|
|
from: "",
|
|
to: "",
|
|
ship_name: "",
|
|
reg_number: "",
|
|
province_code: "",
|
|
owner_id: "",
|
|
ship_id: "",
|
|
status: "",
|
|
},
|
|
});
|
|
|
|
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);
|
|
}, []);
|
|
|
|
// Xử lý khi nhận được dữ liệu mới từ API
|
|
useEffect(() => {
|
|
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);
|
|
};
|
|
|
|
const handleApplyFilters = (newFilters: FilterValues) => {
|
|
setFilters(newFilters);
|
|
|
|
// 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
|
|
? dayjs(newFilters.startDate).startOf("day").toISOString()
|
|
: "",
|
|
to: newFilters.endDate
|
|
? dayjs(newFilters.endDate).endOf("day").toISOString()
|
|
: "",
|
|
// Convert number status sang string để gửi lên server
|
|
status: newFilters.status !== null ? String(newFilters.status) : "",
|
|
// Thêm ship_id từ tàu đã chọn
|
|
ship_name: newFilters.selectedShip?.shipName || "",
|
|
},
|
|
};
|
|
|
|
// 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);
|
|
};
|
|
|
|
const handleViewTrip = (tripId: string) => {
|
|
console.log("View trip:", tripId);
|
|
// TODO: Navigate to trip detail view
|
|
};
|
|
|
|
const handleEditTrip = (tripId: string) => {
|
|
console.log("Edit trip:", tripId);
|
|
// TODO: Navigate to trip edit screen
|
|
};
|
|
|
|
const handleViewTeam = (tripId: string) => {
|
|
console.log("View team:", tripId);
|
|
// TODO: Navigate to team management
|
|
};
|
|
|
|
const handleSendTrip = (tripId: string) => {
|
|
console.log("Send trip:", tripId);
|
|
// TODO: Send trip for approval
|
|
};
|
|
|
|
const handleDeleteTrip = (tripId: string) => {
|
|
console.log("Delete trip:", tripId);
|
|
// TODO: Show confirmation dialog and delete trip
|
|
};
|
|
|
|
// Dynamic styles based on theme
|
|
const themedStyles = {
|
|
safeArea: {
|
|
backgroundColor: colors.background,
|
|
},
|
|
titleText: {
|
|
color: colors.text,
|
|
},
|
|
countText: {
|
|
color: colors.textSecondary,
|
|
},
|
|
addButton: {
|
|
backgroundColor: colors.primary,
|
|
},
|
|
emptyText: {
|
|
color: colors.textSecondary,
|
|
},
|
|
};
|
|
|
|
// 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 (
|
|
<SafeAreaView style={[styles.safeArea, themedStyles.safeArea]} edges={["top"]}>
|
|
<View style={styles.container}>
|
|
{/* Header */}
|
|
<Text style={[styles.titleText, themedStyles.titleText]}>{t("diary.title")}</Text>
|
|
|
|
{/* Filter & Add Button Row */}
|
|
<View style={styles.actionRow}>
|
|
<FilterButton
|
|
onPress={handleFilter}
|
|
isFiltered={
|
|
filters.status !== null ||
|
|
filters.startDate !== null ||
|
|
filters.endDate !== null ||
|
|
filters.selectedShip !== null
|
|
}
|
|
/>
|
|
<TouchableOpacity
|
|
style={[styles.addButton, themedStyles.addButton]}
|
|
onPress={() => console.log("Add trip")}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons name="add" size={20} color="#FFFFFF" />
|
|
<Text style={styles.addButtonText}>{t("diary.addTrip")}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Trip Count */}
|
|
<Text style={[styles.countText, themedStyles.countText]}>
|
|
{t("diary.tripListCount", { count: tripsList?.total || 0 })}
|
|
</Text>
|
|
|
|
{/* Trip List with FlatList */}
|
|
<FlatList
|
|
data={allTrips}
|
|
renderItem={renderTripItem}
|
|
keyExtractor={keyExtractor}
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
onEndReached={handleLoadMore}
|
|
onEndReachedThreshold={0.5}
|
|
ListFooterComponent={renderFooter}
|
|
ListEmptyComponent={renderEmpty}
|
|
removeClippedSubviews={true}
|
|
maxToRenderPerBatch={10}
|
|
windowSize={10}
|
|
initialNumToRender={10}
|
|
/>
|
|
</View>
|
|
|
|
{/* Filter Modal */}
|
|
<FilterModal
|
|
visible={showFilterModal}
|
|
onClose={() => setShowFilterModal(false)}
|
|
onApply={handleApplyFilters}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safeArea: {
|
|
flex: 1,
|
|
},
|
|
container: {
|
|
flex: 1,
|
|
padding: 10,
|
|
},
|
|
titleText: {
|
|
fontSize: 28,
|
|
fontWeight: "700",
|
|
lineHeight: 36,
|
|
marginBottom: 10,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
actionRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "flex-start",
|
|
alignItems: "center",
|
|
gap: 12,
|
|
marginBottom: 12,
|
|
},
|
|
headerRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginTop: 20,
|
|
marginBottom: 12,
|
|
},
|
|
countText: {
|
|
fontSize: 16,
|
|
fontWeight: "600",
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
marginBottom: 10,
|
|
},
|
|
addButton: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 8,
|
|
borderRadius: 8,
|
|
gap: 6,
|
|
},
|
|
addButtonText: {
|
|
fontSize: 14,
|
|
fontWeight: "600",
|
|
color: "#FFFFFF",
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
scrollContent: {
|
|
paddingBottom: 20,
|
|
},
|
|
emptyState: {
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
paddingVertical: 60,
|
|
},
|
|
emptyText: {
|
|
fontSize: 16,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
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",
|
|
}),
|
|
},
|
|
});
|