thêm giao diện quản lý thuyền

This commit is contained in:
Tran Anh Tuan
2025-12-10 19:49:54 +07:00
parent df4318fed4
commit 3e1c4dcbc5
24 changed files with 2091 additions and 135 deletions

View File

@@ -3,8 +3,10 @@ import { Tabs, useSegments } from "expo-router";
import { HapticTab } from "@/components/haptic-tab";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { queryProfile } from "@/controller/AuthController";
import { useI18n } from "@/hooks/use-i18n";
import { useColorScheme } from "@/hooks/use-theme-context";
import { addUserStorage } from "@/utils/storage";
import { useEffect, useRef } from "react";
export default function TabLayout() {
@@ -29,6 +31,23 @@ export default function TabLayout() {
}
}, [currentSegment]);
useEffect(() => {
const getUserProfile = async () => {
try {
const resp = await queryProfile();
if (resp.data && resp.status === 200) {
await addUserStorage(
resp.data.id || "",
resp.data.metadata?.user_type || ""
);
}
} catch (error) {
console.error("Error when get Profile: ", error);
}
};
getUserProfile();
}, []);
return (
<Tabs
screenOptions={{

View File

@@ -1,35 +1,178 @@
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
import DevicesScreen from "@/components/manager/devices";
import FleetsScreen from "@/components/manager/fleets";
import ShipsScreen from "@/components/manager/ships";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { Colors } from "@/config";
import { ColorScheme, useTheme } from "@/hooks/use-theme-context";
import { useEffect, useMemo, useRef, useState } from "react";
import { Animated, StyleSheet, TouchableOpacity, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
export default function manager() {
const { colors, colorScheme } = useTheme();
const styles = useMemo(
() => createStyles(colors, colorScheme),
[colors, colorScheme]
);
const [selected, setSelected] = useState<"ships" | "devices" | "fleets">(
"ships"
);
const [containerWidth, setContainerWidth] = useState(0);
const indicatorTranslate = useRef(new Animated.Value(0)).current;
const SEGMENT_COUNT = 3;
const indexMap: Record<string, number> = {
ships: 0,
devices: 1,
fleets: 2,
};
const SegmentButton = ({
label,
active,
onPress,
}: {
label: string;
active?: boolean;
onPress?: () => void;
}) => {
return (
<TouchableOpacity
style={[styles.segmentButton, active && styles.segmentButtonActive]}
onPress={onPress}
activeOpacity={0.8}
>
<ThemedText
style={[styles.segmentText, active && styles.segmentTextActive]}
>
{label}
</ThemedText>
</TouchableOpacity>
);
};
useEffect(() => {
if (containerWidth <= 0) return;
const segmentWidth = containerWidth / SEGMENT_COUNT;
const toValue = indexMap[selected] * segmentWidth;
Animated.spring(indicatorTranslate, {
toValue,
useNativeDriver: true,
friction: 14,
tension: 100,
}).start();
}, [selected, containerWidth, indicatorTranslate]);
return (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView contentContainerStyle={styles.scrollContent}>
<SafeAreaView style={{ flex: 1 }} edges={["top"]}>
<ThemedView style={styles.scrollContent}>
<View style={styles.container}>
<Text style={styles.titleText}>Quản tàu </Text>
<ThemedView style={styles.header}>
<View
style={styles.segmentContainer}
onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
>
{/* sliding indicator */}
{containerWidth > 0 && (
<Animated.View
style={[
styles.indicator,
{
width: Math.max(containerWidth / SEGMENT_COUNT - 8, 0),
transform: [{ translateX: indicatorTranslate }],
},
]}
/>
)}
<SegmentButton
label="Tàu"
active={selected === "ships"}
onPress={() => setSelected("ships")}
/>
<SegmentButton
label="Thiết bị"
active={selected === "devices"}
onPress={() => setSelected("devices")}
/>
<SegmentButton
label="Đội tàu"
active={selected === "fleets"}
onPress={() => setSelected("fleets")}
/>
</View>
</ThemedView>
<View style={styles.contentWrapper}>
{selected === "ships" && <ShipsScreen />}
{selected === "devices" && <DevicesScreen />}
{selected === "fleets" && <FleetsScreen />}
</View>
</View>
</ScrollView>
</ThemedView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
scrollContent: {
flexGrow: 1,
},
container: {
alignItems: "center",
padding: 15,
},
titleText: {
fontSize: 32,
fontWeight: "700",
lineHeight: 40,
marginBottom: 30,
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
});
const createStyles = (colors: typeof Colors.light, scheme: ColorScheme) =>
StyleSheet.create({
scrollContent: {
flexGrow: 1,
},
container: {
alignItems: "center",
flex: 1,
},
header: {
width: "100%",
paddingVertical: 8,
paddingHorizontal: 4,
},
segmentContainer: {
flexDirection: "row",
backgroundColor: colors.backgroundSecondary,
borderRadius: 10,
padding: 4,
alignSelf: "stretch",
},
segmentButton: {
flex: 1,
paddingVertical: 8,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
},
segmentButtonActive: {
backgroundColor: scheme === "dark" ? "#435B66" : colors.surface,
shadowColor: scheme === "dark" ? "transparent" : "#000",
shadowOpacity: scheme === "dark" ? 0 : 0.1,
shadowRadius: 4,
elevation: scheme === "dark" ? 0 : 2,
},
segmentText: {
fontSize: 14,
fontWeight: "600",
color: colors.textSecondary,
},
segmentTextActive: {
color: colors.text,
},
indicator: {
position: "absolute",
left: 4,
top: 4,
bottom: 4,
backgroundColor: scheme === "dark" ? "#435B66" : colors.surface,
borderRadius: 8,
shadowColor: scheme === "dark" ? "transparent" : "#000",
shadowOpacity: scheme === "dark" ? 0 : 0.1,
shadowRadius: 4,
elevation: scheme === "dark" ? 0 : 2,
},
contentWrapper: {
flex: 1,
alignSelf: "stretch",
width: "100%",
},
});

View File

@@ -1,83 +0,0 @@
// import ButtonCancelTrip from "@/components/ButtonCancelTrip";
// import ButtonCreateNewHaulOrTrip from "@/components/ButtonCreateNewHaulOrTrip";
// import ButtonEndTrip from "@/components/ButtonEndTrip";
// import CrewListTable from "@/components/tripInfo/CrewListTable";
// import FishingToolsTable from "@/components/tripInfo/FishingToolsList";
// import NetListTable from "@/components/tripInfo/NetListTable";
// import TripCostTable from "@/components/tripInfo/TripCostTable";
// import { useI18n } from "@/hooks/use-i18n";
// import { useThemeContext } from "@/hooks/use-theme-context";
// import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
// import { SafeAreaView } from "react-native-safe-area-context";
// export default function TripInfoScreen() {
// const { t } = useI18n();
// const { colors } = useThemeContext();
// return (
// <SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
// <View style={styles.header}>
// <Text style={[styles.titleText, { color: colors.text }]}>
// {t("trip.infoTrip")}
// </Text>
// <View style={styles.buttonWrapper}>
// <ButtonCreateNewHaulOrTrip />
// </View>
// </View>
// <ScrollView contentContainerStyle={styles.scrollContent}>
// <View style={styles.container}>
// <TripCostTable />
// <FishingToolsTable />
// <CrewListTable />
// <NetListTable />
// <View style={styles.buttonRow}>
// <ButtonCancelTrip />
// <ButtonEndTrip />
// </View>
// </View>
// </ScrollView>
// </SafeAreaView>
// );
// }
// const styles = StyleSheet.create({
// safeArea: {
// flex: 1,
// paddingBottom: 5,
// },
// scrollContent: {
// flexGrow: 1,
// },
// header: {
// width: "100%",
// paddingHorizontal: 15,
// paddingTop: 15,
// paddingBottom: 10,
// alignItems: "center",
// },
// buttonWrapper: {
// width: "100%",
// flexDirection: "row",
// justifyContent: "flex-end",
// },
// container: {
// alignItems: "center",
// paddingHorizontal: 15,
// },
// buttonRow: {
// flexDirection: "row",
// gap: 10,
// marginTop: 15,
// marginBottom: 15,
// },
// titleText: {
// fontSize: 32,
// fontWeight: "700",
// lineHeight: 40,
// paddingBottom: 10,
// fontFamily: Platform.select({
// ios: "System",
// android: "Roboto",
// default: "System",
// }),
// },
// });