Compare commits
3 Commits
1ef83c9b22
...
04ca091f49
| Author | SHA1 | Date | |
|---|---|---|---|
| 04ca091f49 | |||
|
|
1b748285c9 | ||
|
|
aabd1109b2 |
@@ -247,7 +247,7 @@ export async function login(body: Model.LoginRequestBody) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export async function fetchGpsData() {
|
export async function fetchGpsData() {
|
||||||
return api.get<Model.GPSResonse>(API_GET_GPS);
|
return api.get<Model.GPSResponse>(API_GET_GPS);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -388,7 +388,7 @@ export default function TabLayout() {
|
|||||||
1. **State management:**
|
1. **State management:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [gpsData, setGpsData] = useState<Model.GPSResonse | null>(null);
|
const [gpsData, setGpsData] = useState<Model.GPSResponse | null>(null);
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Fetch GPS data:**
|
2. **Fetch GPS data:**
|
||||||
|
|||||||
@@ -30,19 +30,11 @@ import {
|
|||||||
convertWKTtoLatLngString,
|
convertWKTtoLatLngString,
|
||||||
} from "@/utils/geom";
|
} from "@/utils/geom";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import { Animated, Image as RNImage, StyleSheet, View } from "react-native";
|
||||||
Animated,
|
|
||||||
Image as RNImage,
|
|
||||||
StyleSheet,
|
|
||||||
View
|
|
||||||
} from "react-native";
|
|
||||||
import MapView, { Circle, Marker } from "react-native-maps";
|
import MapView, { Circle, Marker } from "react-native-maps";
|
||||||
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const [gpsData, setGpsData] = useState<Model.GPSResonse | null>(
|
const [gpsData, setGpsData] = useState<Model.GPSResponse | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const [alarmData, setAlarmData] = useState<Model.AlarmResponse | null>(null);
|
const [alarmData, setAlarmData] = useState<Model.AlarmResponse | null>(null);
|
||||||
const [entityData, setEntityData] = useState<
|
const [entityData, setEntityData] = useState<
|
||||||
Model.TransformedEntity[] | null
|
Model.TransformedEntity[] | null
|
||||||
@@ -54,9 +46,8 @@ export default function HomeScreen() {
|
|||||||
const [circleRadius, setCircleRadius] = useState(100);
|
const [circleRadius, setCircleRadius] = useState(100);
|
||||||
const [zoomLevel, setZoomLevel] = useState(10);
|
const [zoomLevel, setZoomLevel] = useState(10);
|
||||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||||
const [polylineCoordinates, setPolylineCoordinates] = useState<
|
const [polylineCoordinates, setPolylineCoordinates] =
|
||||||
PolylineWithLabelProps | null
|
useState<PolylineWithLabelProps | null>(null);
|
||||||
>(null);
|
|
||||||
const [polygonCoordinates, setPolygonCoordinates] = useState<
|
const [polygonCoordinates, setPolygonCoordinates] = useState<
|
||||||
PolygonWithLabelProps[]
|
PolygonWithLabelProps[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -76,7 +67,7 @@ export default function HomeScreen() {
|
|||||||
getEntitiesEventBus();
|
getEntitiesEventBus();
|
||||||
getBanzonesEventBus();
|
getBanzonesEventBus();
|
||||||
getTrackPointsEventBus();
|
getTrackPointsEventBus();
|
||||||
const queryGpsData = (gpsData: Model.GPSResonse) => {
|
const queryGpsData = (gpsData: Model.GPSResponse) => {
|
||||||
if (gpsData) {
|
if (gpsData) {
|
||||||
// console.log("GPS Data: ", gpsData);
|
// console.log("GPS Data: ", gpsData);
|
||||||
setGpsData(gpsData);
|
setGpsData(gpsData);
|
||||||
@@ -182,7 +173,6 @@ export default function HomeScreen() {
|
|||||||
label: zone?.zone_name ?? "",
|
label: zone?.zone_name ?? "",
|
||||||
content: zone?.message ?? "",
|
content: zone?.message ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if (geom_type === 1) {
|
} else if (geom_type === 1) {
|
||||||
// foundPolygon = true;
|
// foundPolygon = true;
|
||||||
@@ -294,7 +284,8 @@ export default function HomeScreen() {
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
// edges={["top"]}
|
// edges={["top"]}
|
||||||
style={styles.container}>
|
style={styles.container}
|
||||||
|
>
|
||||||
<MapView
|
<MapView
|
||||||
onMapReady={handleMapReady}
|
onMapReady={handleMapReady}
|
||||||
onRegionChangeComplete={handleRegionChangeComplete}
|
onRegionChangeComplete={handleRegionChangeComplete}
|
||||||
@@ -351,7 +342,9 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PolygonWithLabel
|
<PolygonWithLabel
|
||||||
key={`polygon-${index}-${gpsData?.lat || 0}-${gpsData?.lon || 0}`}
|
key={`polygon-${index}-${gpsData?.lat || 0}-${
|
||||||
|
gpsData?.lon || 0
|
||||||
|
}`}
|
||||||
coordinates={polygon.coordinates}
|
coordinates={polygon.coordinates}
|
||||||
label={polygon.label}
|
label={polygon.label}
|
||||||
content={polygon.content}
|
content={polygon.content}
|
||||||
@@ -367,7 +360,11 @@ export default function HomeScreen() {
|
|||||||
)}
|
)}
|
||||||
{gpsData !== null && (
|
{gpsData !== null && (
|
||||||
<Marker
|
<Marker
|
||||||
key={platform === IOS_PLATFORM ? `${gpsData.lat}-${gpsData.lon}` : "gps-data"}
|
key={
|
||||||
|
platform === IOS_PLATFORM
|
||||||
|
? `${gpsData.lat}-${gpsData.lon}`
|
||||||
|
: "gps-data"
|
||||||
|
}
|
||||||
coordinate={{
|
coordinate={{
|
||||||
latitude: gpsData.lat,
|
latitude: gpsData.lat,
|
||||||
longitude: gpsData.lon,
|
longitude: gpsData.lon,
|
||||||
@@ -412,7 +409,8 @@ export default function HomeScreen() {
|
|||||||
height: 32,
|
height: 32,
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
rotate: `${typeof gpsData.h === "number" && !isNaN(gpsData.h)
|
rotate: `${
|
||||||
|
typeof gpsData.h === "number" && !isNaN(gpsData.h)
|
||||||
? gpsData.h
|
? gpsData.h
|
||||||
: 0
|
: 0
|
||||||
}deg`,
|
}deg`,
|
||||||
|
|||||||
@@ -5,16 +5,14 @@ import CrewListTable from "@/components/tripInfo/CrewListTable";
|
|||||||
import FishingToolsTable from "@/components/tripInfo/FishingToolsList";
|
import FishingToolsTable from "@/components/tripInfo/FishingToolsList";
|
||||||
import NetListTable from "@/components/tripInfo/NetListTable";
|
import NetListTable from "@/components/tripInfo/NetListTable";
|
||||||
import TripCostTable from "@/components/tripInfo/TripCostTable";
|
import TripCostTable from "@/components/tripInfo/TripCostTable";
|
||||||
import { useTrip } from "@/state/use-trip";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function TripInfoScreen() {
|
export default function TripInfoScreen() {
|
||||||
const { trip, getTrip } = useTrip();
|
// const { trip, getTrip } = useTrip();
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
getTrip();
|
// getTrip();
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import { Stack, useRouter } from "expo-router";
|
|||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import Toast from "react-native-toast-message";
|
// import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider/gluestack-ui-provider";
|
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider/gluestack-ui-provider";
|
||||||
|
import { toastConfig } from "@/config";
|
||||||
import { setRouterInstance } from "@/config/auth";
|
import { setRouterInstance } from "@/config/auth";
|
||||||
import "@/global.css";
|
import "@/global.css";
|
||||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -52,7 +53,7 @@ export default function RootLayout() {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<Toast />
|
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</GluestackUIProvider>
|
</GluestackUIProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,40 @@
|
|||||||
|
import { queryGpsData } from "@/controller/DeviceController";
|
||||||
|
import {
|
||||||
|
queryStartNewHaul,
|
||||||
|
queryUpdateTripState,
|
||||||
|
} from "@/controller/TripController";
|
||||||
|
import {
|
||||||
|
showErrorToast,
|
||||||
|
showSuccessToast,
|
||||||
|
showWarningToast,
|
||||||
|
} from "@/services/toast_service";
|
||||||
|
import { useTrip } from "@/state/use-trip";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
import { Alert, StyleSheet, View } from "react-native";
|
||||||
|
import IconButton from "./IconButton";
|
||||||
|
import CreateOrUpdateHaulModal from "./tripInfo/modal/CreateOrUpdateHaulModal";
|
||||||
|
|
||||||
interface StartButtonProps {
|
interface StartButtonProps {
|
||||||
title?: string;
|
gpsData?: Model.GPSResponse;
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonCreateNewHaulOrTrip: React.FC<StartButtonProps> = ({
|
const ButtonCreateNewHaulOrTrip: React.FC<StartButtonProps> = ({
|
||||||
title = "Bắt đầu mẻ lưới",
|
gpsData,
|
||||||
onPress,
|
onPress,
|
||||||
}) => {
|
}) => {
|
||||||
const [isStarted, setIsStarted] = useState(false);
|
const [isStarted, setIsStarted] = useState(false);
|
||||||
|
const [isFinishHaulModalOpen, setIsFinishHaulModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { trip, getTrip } = useTrip();
|
||||||
|
useEffect(() => {
|
||||||
|
getTrip();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkHaulFinished = () => {
|
||||||
|
return trip?.fishing_logs?.some((h) => h.status === 0);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
if (isStarted) {
|
if (isStarted) {
|
||||||
@@ -53,24 +76,103 @@ const ButtonCreateNewHaulOrTrip: React.FC<StartButtonProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStartTrip = async (state: number, note?: string) => {
|
||||||
|
if (trip?.trip_status !== 2) {
|
||||||
|
showWarningToast("Chuyến đi đã được bắt đầu hoặc hoàn thành.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resp = await queryUpdateTripState({
|
||||||
|
status: state,
|
||||||
|
note: note || "",
|
||||||
|
});
|
||||||
|
if (resp.status === 200) {
|
||||||
|
showSuccessToast("Bắt đầu chuyến đi thành công!");
|
||||||
|
getTrip();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error stating trip :", error);
|
||||||
|
showErrorToast("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewHaul = async () => {
|
||||||
|
if (trip?.fishing_logs?.some((f) => f.status === 0)) {
|
||||||
|
showWarningToast(
|
||||||
|
"Vui lòng kết thúc mẻ lưới hiện tại trước khi bắt đầu mẻ mới"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!gpsData) {
|
||||||
|
const response = await queryGpsData();
|
||||||
|
gpsData = response.data;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const body: Model.NewFishingLogRequest = {
|
||||||
|
trip_id: trip?.id || "",
|
||||||
|
start_at: new Date(),
|
||||||
|
start_lat: gpsData.lat,
|
||||||
|
start_lon: gpsData.lon,
|
||||||
|
weather_description: "Nắng đẹp",
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await queryStartNewHaul(body);
|
||||||
|
if (resp.status === 200) {
|
||||||
|
showSuccessToast("Bắt đầu mẻ lưới mới thành công!");
|
||||||
|
getTrip();
|
||||||
|
} else {
|
||||||
|
showErrorToast("Tạo mẻ lưới mới thất bại!");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
// showErrorToast("Tạo mẻ lưới mới thất bại!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Không render gì nếu trip đã hoàn thành hoặc bị hủy
|
||||||
|
if (trip?.trip_status === 4 || trip?.trip_status === 5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<View>
|
||||||
style={[styles.button, isStarted && styles.buttonActive]}
|
{trip?.trip_status === 2 ? (
|
||||||
onPress={handlePress}
|
<IconButton
|
||||||
activeOpacity={0.8}
|
icon={<AntDesign name="plus" />}
|
||||||
|
type="primary"
|
||||||
|
style={{ backgroundColor: "green", borderRadius: 10 }}
|
||||||
|
onPress={async () => handleStartTrip(3)}
|
||||||
>
|
>
|
||||||
<View style={styles.content}>
|
Bắt đầu chuyến đi
|
||||||
<AntDesign
|
</IconButton>
|
||||||
name={isStarted ? "close" : "plus"}
|
) : checkHaulFinished() ? (
|
||||||
size={18}
|
<IconButton
|
||||||
color="#fff"
|
icon={<AntDesign name="plus" color={"white"} />}
|
||||||
style={styles.icon}
|
type="primary"
|
||||||
|
style={{ borderRadius: 10 }}
|
||||||
|
onPress={() => setIsFinishHaulModalOpen(true)}
|
||||||
|
>
|
||||||
|
Kết thúc mẻ lưới
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
icon={<AntDesign name="plus" color={"white"} />}
|
||||||
|
type="primary"
|
||||||
|
style={{ borderRadius: 10 }}
|
||||||
|
onPress={async () => {
|
||||||
|
createNewHaul();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bắt đầu mẻ lưới
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<CreateOrUpdateHaulModal
|
||||||
|
isVisible={isFinishHaulModalOpen}
|
||||||
|
onClose={function (): void {
|
||||||
|
setIsFinishHaulModalOpen(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.text}>
|
|
||||||
{isStarted ? "Kết thúc mẻ lưới" : title}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
159
components/IconButton.tsx
Normal file
159
components/IconButton.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
GestureResponderEvent,
|
||||||
|
StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
type ButtonType = "primary" | "default" | "dashed" | "text" | "link" | "danger";
|
||||||
|
type ButtonShape = "default" | "circle" | "round";
|
||||||
|
type ButtonSize = "small" | "middle" | "large";
|
||||||
|
|
||||||
|
export interface IconButtonProps {
|
||||||
|
type?: ButtonType;
|
||||||
|
shape?: ButtonShape;
|
||||||
|
size?: ButtonSize;
|
||||||
|
icon?: React.ReactNode; // render an icon component, e.g. <AntDesign name="plus" />
|
||||||
|
loading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onPress?: (e?: GestureResponderEvent) => void;
|
||||||
|
children?: React.ReactNode; // label text
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
block?: boolean; // full width
|
||||||
|
activeOpacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IconButton
|
||||||
|
* A lightweight Button component inspired by Ant Design Button API, tuned for React Native.
|
||||||
|
* Accepts an `icon` prop as a React node for maximum flexibility.
|
||||||
|
*/
|
||||||
|
const IconButton: React.FC<IconButtonProps> = ({
|
||||||
|
type = "default",
|
||||||
|
shape = "default",
|
||||||
|
size = "middle",
|
||||||
|
icon,
|
||||||
|
loading = false,
|
||||||
|
disabled = false,
|
||||||
|
onPress,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
block = false,
|
||||||
|
activeOpacity = 0.8,
|
||||||
|
}) => {
|
||||||
|
const sizeMap = {
|
||||||
|
small: { height: 32, fontSize: 14, paddingHorizontal: 10 },
|
||||||
|
middle: { height: 40, fontSize: 16, paddingHorizontal: 14 },
|
||||||
|
large: { height: 48, fontSize: 18, paddingHorizontal: 18 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const colors: Record<
|
||||||
|
ButtonType,
|
||||||
|
{ backgroundColor?: string; textColor: string; borderColor?: string }
|
||||||
|
> = {
|
||||||
|
primary: { backgroundColor: "#4ecdc4", textColor: "#fff" },
|
||||||
|
default: {
|
||||||
|
backgroundColor: "#f2f2f2",
|
||||||
|
textColor: "#111",
|
||||||
|
borderColor: "#e6e6e6",
|
||||||
|
},
|
||||||
|
dashed: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
textColor: "#111",
|
||||||
|
borderColor: "#d9d9d9",
|
||||||
|
},
|
||||||
|
text: { backgroundColor: "transparent", textColor: "#111" },
|
||||||
|
link: { backgroundColor: "transparent", textColor: "#4ecdc4" },
|
||||||
|
danger: { backgroundColor: "#e74c3c", textColor: "#fff" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const sz = sizeMap[size];
|
||||||
|
const color = colors[type];
|
||||||
|
|
||||||
|
const isCircle = shape === "circle";
|
||||||
|
const isRound = shape === "round";
|
||||||
|
|
||||||
|
const handlePress = (e: GestureResponderEvent) => {
|
||||||
|
if (disabled || loading) return;
|
||||||
|
onPress?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={activeOpacity}
|
||||||
|
onPress={handlePress}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
{
|
||||||
|
height: sz.height,
|
||||||
|
paddingHorizontal: isCircle ? 0 : sz.paddingHorizontal,
|
||||||
|
backgroundColor: color.backgroundColor ?? "transparent",
|
||||||
|
borderColor: color.borderColor ?? "transparent",
|
||||||
|
borderWidth: type === "dashed" ? 1 : color.borderColor ? 1 : 0,
|
||||||
|
width: isCircle ? sz.height : block ? "100%" : undefined,
|
||||||
|
borderRadius: isCircle ? sz.height / 2 : isRound ? 999 : 8,
|
||||||
|
opacity: disabled ? 0.6 : 1,
|
||||||
|
},
|
||||||
|
type === "dashed" ? { borderStyle: "dashed" } : null,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.content}>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator
|
||||||
|
size={"small"}
|
||||||
|
color={color.textColor}
|
||||||
|
style={styles.iconContainer}
|
||||||
|
/>
|
||||||
|
) : icon ? (
|
||||||
|
<View style={styles.iconContainer}>{icon}</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{children ? (
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
styles.text,
|
||||||
|
{
|
||||||
|
color: color.textColor,
|
||||||
|
fontSize: sz.fontSize,
|
||||||
|
marginLeft: icon || loading ? 6 : 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 8,
|
||||||
|
borderColor: "transparent",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IconButton;
|
||||||
@@ -2,10 +2,10 @@ import { convertToDMS, kmhToKnot } from "@/utils/geom";
|
|||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Animated, TouchableOpacity, View } from "react-native";
|
import { Animated, TouchableOpacity, View } from "react-native";
|
||||||
|
import ButtonCreateNewHaulOrTrip from "../ButtonCreateNewHaulOrTrip";
|
||||||
import { Description } from "./Description";
|
import { Description } from "./Description";
|
||||||
|
|
||||||
type GPSInfoPanelProps = {
|
type GPSInfoPanelProps = {
|
||||||
gpsData: Model.GPSResonse | undefined;
|
gpsData: Model.GPSResponse | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
||||||
@@ -43,13 +43,28 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: blockBottom,
|
bottom: blockBottom,
|
||||||
left: 5,
|
left: 5,
|
||||||
width: 48,
|
// width: 48,
|
||||||
height: 48,
|
// height: 48,
|
||||||
backgroundColor: "blue",
|
// backgroundColor: "blue",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
zIndex: 30,
|
zIndex: 30,
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<ButtonCreateNewHaulOrTrip gpsData={gpsData} />
|
||||||
|
{/* <TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
// showInfoToast("oad");
|
||||||
|
showWarningToast("This is a warning toast!");
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 z-10 bg-white rounded-full p-1"
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name={isExpanded ? "close" : "close"}
|
||||||
|
size={20}
|
||||||
|
color="#666"
|
||||||
/>
|
/>
|
||||||
|
</TouchableOpacity> */}
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -5,161 +5,6 @@ import { Animated, Text, TouchableOpacity, View } from "react-native";
|
|||||||
import CrewDetailModal from "./modal/CrewDetailModal";
|
import CrewDetailModal from "./modal/CrewDetailModal";
|
||||||
import styles from "./style/CrewListTable.styles";
|
import styles from "./style/CrewListTable.styles";
|
||||||
|
|
||||||
const mockCrews: Model.TripCrews[] = [
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "480863197307",
|
|
||||||
role: "crew",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.230053Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "480863197307",
|
|
||||||
name: "Huỳnh Tấn Trang",
|
|
||||||
phone: "0838944284",
|
|
||||||
email: "huynhtantrang@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2025-01-11T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "49915 Poplar Avenue",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "714834545296",
|
|
||||||
role: "crew",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.301376Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "714834545296",
|
|
||||||
name: "Trương Văn Nam",
|
|
||||||
phone: "0773396753",
|
|
||||||
email: "truongvannam@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2025-07-24T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "3287 Carlotta Underpass",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "049299828990",
|
|
||||||
role: "crew",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.373037Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "049299828990",
|
|
||||||
name: "Đặng Anh Minh",
|
|
||||||
phone: "0827640820",
|
|
||||||
email: "danganhminh@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2024-10-30T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "68909 Gerda Burgs",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "851494873747",
|
|
||||||
role: "captain",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.442774Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "851494873747",
|
|
||||||
name: "Tô Thị Linh",
|
|
||||||
phone: "0337906041",
|
|
||||||
email: "tothilinh@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2025-06-18T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "6676 Kulas Groves",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "384839614682",
|
|
||||||
role: "crew",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.515532Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "384839614682",
|
|
||||||
name: "Lê Thanh Hoa",
|
|
||||||
phone: "0937613034",
|
|
||||||
email: "lethanhhoa@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2025-07-17T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "244 Cicero Estate",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "702319275290",
|
|
||||||
role: "crew",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.588038Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "702319275290",
|
|
||||||
name: "Nguyễn Phước Hải",
|
|
||||||
phone: "0347859214",
|
|
||||||
email: "nguyenphuochai@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2025-08-13T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "6874 Devon Key",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "943534816439",
|
|
||||||
role: "crew",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.668984Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "943534816439",
|
|
||||||
name: "Lý Hữu Hà",
|
|
||||||
phone: "0768548881",
|
|
||||||
email: "lyhuuha@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2025-08-18T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "655 Middle Street",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TripID: "f9884294-a7f2-46dc-aaf2-032da08a1ab6",
|
|
||||||
PersonalID: "096528446981",
|
|
||||||
role: "crew",
|
|
||||||
joined_at: new Date("2025-11-06T08:13:33.74379Z"),
|
|
||||||
left_at: null,
|
|
||||||
note: null,
|
|
||||||
Person: {
|
|
||||||
personal_id: "096528446981",
|
|
||||||
name: "Trần Xuân Thi",
|
|
||||||
phone: "0963449523",
|
|
||||||
email: "tranxuanthi@crew.sgw.vn",
|
|
||||||
birth_date: new Date("2024-09-21T00:00:00Z"),
|
|
||||||
note: "",
|
|
||||||
address: "59344 Burley Isle",
|
|
||||||
created_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
updated_at: new Date("0001-01-01T00:00:00Z"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const CrewListTable: React.FC = () => {
|
const CrewListTable: React.FC = () => {
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const [contentHeight, setContentHeight] = useState<number>(0);
|
const [contentHeight, setContentHeight] = useState<number>(0);
|
||||||
@@ -171,13 +16,13 @@ const CrewListTable: React.FC = () => {
|
|||||||
|
|
||||||
const { trip } = useTrip();
|
const { trip } = useTrip();
|
||||||
|
|
||||||
const data: Model.TripCrews[] = trip?.crews ?? mockCrews;
|
const data: Model.TripCrews[] = trip?.crews ?? [];
|
||||||
|
|
||||||
const tongThanhVien = data.length;
|
const tongThanhVien = data.length;
|
||||||
|
|
||||||
// Reset animated height khi dữ liệu thay đổi
|
// Reset animated height khi dữ liệu thay đổi
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setContentHeight(0); // Reset để tính lại chiều cao
|
// setContentHeight(0); // Reset để tính lại chiều cao
|
||||||
setCollapsed(true); // Reset về trạng thái gập lại
|
setCollapsed(true); // Reset về trạng thái gập lại
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const FishingToolsTable: React.FC = () => {
|
|||||||
|
|
||||||
// Reset animated height khi dữ liệu thay đổi
|
// Reset animated height khi dữ liệu thay đổi
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setContentHeight(0); // Reset để tính lại chiều cao
|
// setContentHeight(0); // Reset để tính lại chiều cao
|
||||||
setCollapsed(true); // Reset về trạng thái gập lại
|
setCollapsed(true); // Reset về trạng thái gập lại
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
|||||||
@@ -284,7 +284,10 @@ const NetListTable: React.FC = () => {
|
|||||||
{/* Modal chi tiết */}
|
{/* Modal chi tiết */}
|
||||||
<NetDetailModal
|
<NetDetailModal
|
||||||
visible={modalVisible}
|
visible={modalVisible}
|
||||||
onClose={() => setModalVisible(false)}
|
onClose={() => {
|
||||||
|
console.log("OnCLose");
|
||||||
|
setModalVisible(false);
|
||||||
|
}}
|
||||||
netData={selectedNet}
|
netData={selectedNet}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const TripCostTable: React.FC = () => {
|
|||||||
|
|
||||||
// Reset animated height khi dữ liệu thay đổi
|
// Reset animated height khi dữ liệu thay đổi
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setContentHeight(0); // Reset để tính lại chiều cao
|
// setContentHeight(0); // Reset để tính lại chiều cao
|
||||||
setCollapsed(true); // Reset về trạng thái gập lại
|
setCollapsed(true); // Reset về trạng thái gập lại
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
|||||||
29
components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
Normal file
29
components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Modal, Text } from "react-native";
|
||||||
|
|
||||||
|
interface CreateOrUpdateHaulModalProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
haulData?: Model.FishingLog | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
||||||
|
isVisible,
|
||||||
|
onClose,
|
||||||
|
haulData,
|
||||||
|
}) => {
|
||||||
|
const [isCreateMode, setIsCreateMode] = React.useState(!haulData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={isVisible}
|
||||||
|
animationType="slide"
|
||||||
|
presentationStyle="pageSheet"
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<Text>{isCreateMode ? "Create Haul" : "Update Haul"}</Text>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateOrUpdateHaulModal;
|
||||||
@@ -91,9 +91,9 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
|
|||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
if (!netData) return null;
|
// if (!netData) return null;
|
||||||
|
|
||||||
const isCompleted = netData.trangThai === "Đã hoàn thành";
|
const isCompleted = netData?.trangThai === "Đã hoàn thành";
|
||||||
|
|
||||||
// Danh sách tên cá có sẵn
|
// Danh sách tên cá có sẵn
|
||||||
const fishNameOptions = [
|
const fishNameOptions = [
|
||||||
@@ -210,7 +210,7 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setEditableCatchList(netData.catchList || []);
|
setEditableCatchList(netData?.catchList || []);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleExpanded = (index: number) => {
|
const handleToggleExpanded = (index: number) => {
|
||||||
@@ -343,7 +343,10 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<ScrollView style={styles.content}>
|
<ScrollView style={styles.content}>
|
||||||
{/* Thông tin chung */}
|
{/* Thông tin chung */}
|
||||||
<InfoSection netData={netData} isCompleted={isCompleted} />
|
<InfoSection
|
||||||
|
netData={netData ?? undefined}
|
||||||
|
isCompleted={isCompleted}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Danh sách cá bắt được */}
|
{/* Danh sách cá bắt được */}
|
||||||
<CatchSectionHeader totalCatch={totalCatch} />
|
<CatchSectionHeader totalCatch={totalCatch} />
|
||||||
@@ -372,7 +375,7 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Ghi chú */}
|
{/* Ghi chú */}
|
||||||
<NotesSection ghiChu={netData.ghiChu} />
|
<NotesSection ghiChu={netData?.ghiChu} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface NetDetail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface InfoSectionProps {
|
interface InfoSectionProps {
|
||||||
netData: NetDetail;
|
netData?: NetDetail;
|
||||||
isCompleted: boolean;
|
isCompleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +25,9 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
|
|||||||
netData,
|
netData,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!netData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const infoItems = [
|
const infoItems = [
|
||||||
{ label: "Số thứ tự", value: netData.stt },
|
{ label: "Số thứ tự", value: netData.stt },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
export default StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
paddingBottom: 8,
|
||||||
|
backgroundColor: "#f8f9fa",
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#e9ecef",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
closeButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#007bff",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
fieldGroup: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#333",
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ccc",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
infoValue: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#555",
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
rowGroup: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
fishNameDropdown: {
|
||||||
|
// Custom styles if needed
|
||||||
|
},
|
||||||
|
optionsStatusFishList: {
|
||||||
|
// Custom styles if needed
|
||||||
|
},
|
||||||
|
optionsList: {
|
||||||
|
maxHeight: 150,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ccc",
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
position: "absolute",
|
||||||
|
top: 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
},
|
||||||
|
selectButton: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ccc",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 8,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
selectButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
optionItem: {
|
||||||
|
padding: 10,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#eee",
|
||||||
|
},
|
||||||
|
optionText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ddd",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
backgroundColor: "#f9f9f9",
|
||||||
|
},
|
||||||
|
removeButton: {
|
||||||
|
backgroundColor: "#dc3545",
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
removeButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: "#dc3545",
|
||||||
|
fontSize: 12,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
buttonGroup: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-around",
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
backgroundColor: "#007bff",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
editButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
backgroundColor: "#28a745",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
addButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
saveButton: {
|
||||||
|
backgroundColor: "#007bff",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
saveButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
backgroundColor: "#6c757d",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
cancelButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
addFishButton: {
|
||||||
|
backgroundColor: "#17a2b8",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
addFishButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import Toast from "react-native-toast-message";
|
|
||||||
|
|
||||||
export enum ToastType {
|
|
||||||
SUCCESS = "success",
|
|
||||||
ERROR = "error",
|
|
||||||
WARNING = "error", // react-native-toast-message không có 'warning', dùng 'error'
|
|
||||||
INFO = "info",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Success toast
|
|
||||||
*/
|
|
||||||
export const showToastSuccess = (message: string, title?: string): void => {
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: title || "Success",
|
|
||||||
text2: message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error toast
|
|
||||||
*/
|
|
||||||
export const showToastError = (message: string, title?: string): void => {
|
|
||||||
Toast.show({
|
|
||||||
type: ToastType.ERROR,
|
|
||||||
text1: title || ToastType.ERROR,
|
|
||||||
text2: message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Info toast
|
|
||||||
*/
|
|
||||||
export const showToastInfo = (message: string, title?: string): void => {
|
|
||||||
Toast.show({
|
|
||||||
type: ToastType.INFO,
|
|
||||||
text1: title || ToastType.INFO,
|
|
||||||
text2: message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Warning toast
|
|
||||||
*/
|
|
||||||
export const showToastWarning = (message: string, title?: string): void => {
|
|
||||||
Toast.show({
|
|
||||||
type: ToastType.WARNING,
|
|
||||||
text1: title || ToastType.WARNING,
|
|
||||||
text2: message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default toast
|
|
||||||
*/
|
|
||||||
export const showToastDefault = (message: string, title?: string): void => {
|
|
||||||
Toast.show({
|
|
||||||
type: ToastType.INFO,
|
|
||||||
text1: title || ToastType.INFO,
|
|
||||||
text2: message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom toast với type tùy chọn
|
|
||||||
*/
|
|
||||||
export const show = (
|
|
||||||
message: string,
|
|
||||||
type: ToastType,
|
|
||||||
title?: string
|
|
||||||
): void => {
|
|
||||||
const titleText = title || type.charAt(0).toUpperCase() + type.slice(1);
|
|
||||||
Toast.show({
|
|
||||||
type,
|
|
||||||
text1: titleText,
|
|
||||||
text2: message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
214
config/toast.tsx
Normal file
214
config/toast.tsx
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import {
|
||||||
|
BaseToast,
|
||||||
|
BaseToastProps,
|
||||||
|
SuccessToast,
|
||||||
|
} from "react-native-toast-message";
|
||||||
|
|
||||||
|
export const Colors: any = {
|
||||||
|
light: {
|
||||||
|
text: "#000",
|
||||||
|
back: "#ffffff",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
text: "#ffffff",
|
||||||
|
back: "#2B2D2E",
|
||||||
|
},
|
||||||
|
default: "#3498db",
|
||||||
|
info: "#3498db",
|
||||||
|
success: "#07bc0c",
|
||||||
|
warn: {
|
||||||
|
background: "#ffffff",
|
||||||
|
text: "black",
|
||||||
|
iconColor: "#f1c40f",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
background: "#ffffff",
|
||||||
|
text: "black",
|
||||||
|
iconColor: "#e74c3c",
|
||||||
|
},
|
||||||
|
textDefault: "#4c4c4c",
|
||||||
|
textDark: "black",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toastConfig = {
|
||||||
|
success: (props: BaseToastProps) => (
|
||||||
|
<SuccessToast
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
borderLeftColor: Colors.success,
|
||||||
|
backgroundColor: Colors.success,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
}}
|
||||||
|
text1Style={{
|
||||||
|
color: "white",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
text2Style={{
|
||||||
|
color: "#f0f0f0",
|
||||||
|
}}
|
||||||
|
renderLeadingIcon={() => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 40,
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="check-circle" size={30} color="white" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
default: (props: BaseToastProps) => (
|
||||||
|
<BaseToast
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
borderLeftColor: Colors.default,
|
||||||
|
backgroundColor: Colors.default,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
}}
|
||||||
|
text1Style={{
|
||||||
|
color: "white",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
text2Style={{
|
||||||
|
color: "#f0f0f0",
|
||||||
|
}}
|
||||||
|
renderLeadingIcon={() => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 50,
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="info" size={30} color="white" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
info: (props: BaseToastProps) => (
|
||||||
|
<BaseToast
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
borderLeftColor: Colors.info,
|
||||||
|
backgroundColor: Colors.info,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
}}
|
||||||
|
text1Style={{
|
||||||
|
color: "white",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
text2Style={{
|
||||||
|
color: "#f0f0f0",
|
||||||
|
}}
|
||||||
|
renderLeadingIcon={() => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 50,
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="info-outline" size={30} color="white" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
warn: (props: BaseToastProps) => (
|
||||||
|
<BaseToast
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
borderLeftColor: Colors.warn.background,
|
||||||
|
backgroundColor: Colors.warn.background,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
}}
|
||||||
|
text1Style={{
|
||||||
|
color: Colors.warn.text,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
text2Style={{
|
||||||
|
color: "#333",
|
||||||
|
}}
|
||||||
|
renderLeadingIcon={() => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 50,
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name="warning"
|
||||||
|
size={30}
|
||||||
|
color={Colors.warn.iconColor}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
error: (props: BaseToastProps) => (
|
||||||
|
<BaseToast
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
borderLeftColor: Colors.error.background,
|
||||||
|
backgroundColor: Colors.error.background,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
}}
|
||||||
|
text1Style={{
|
||||||
|
color: Colors.error.text,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
text2Style={{
|
||||||
|
color: "#f0f0f0",
|
||||||
|
}}
|
||||||
|
renderLeadingIcon={() => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 50,
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name="error"
|
||||||
|
size={30}
|
||||||
|
color={Colors.error.iconColor}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { transformEntityResponse } from "@/utils/tranform";
|
import { transformEntityResponse } from "@/utils/tranform";
|
||||||
|
|
||||||
export async function queryGpsData() {
|
export async function queryGpsData() {
|
||||||
return api.get<Model.GPSResonse>(API_GET_GPS);
|
return api.get<Model.GPSResponse>(API_GET_GPS);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryAlarm() {
|
export async function queryAlarm() {
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { api } from "@/config";
|
import { api } from "@/config";
|
||||||
import { API_GET_TRIP } from "@/constants";
|
import {
|
||||||
|
API_GET_TRIP,
|
||||||
|
API_HAUL_HANDLE,
|
||||||
|
API_UPDATE_TRIP_STATUS,
|
||||||
|
} from "@/constants";
|
||||||
|
|
||||||
export async function queryTrip() {
|
export async function queryTrip() {
|
||||||
return api.get<Model.Trip>(API_GET_TRIP);
|
return api.get<Model.Trip>(API_GET_TRIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queryUpdateTripState(body: Model.TripUpdateStateRequest) {
|
||||||
|
return api.put(API_UPDATE_TRIP_STATUS, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function queryStartNewHaul(body: Model.NewFishingLogRequest) {
|
||||||
|
return api.put(API_HAUL_HANDLE, body);
|
||||||
|
}
|
||||||
|
|||||||
14
controller/typings.d.ts
vendored
14
controller/typings.d.ts
vendored
@@ -7,7 +7,7 @@ declare namespace Model {
|
|||||||
token?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GPSResonse {
|
interface GPSResponse {
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lon: number;
|
||||||
s: number;
|
s: number;
|
||||||
@@ -174,4 +174,16 @@ declare namespace Model {
|
|||||||
fish_condition?: string;
|
fish_condition?: string;
|
||||||
gear_usage?: string;
|
gear_usage?: string;
|
||||||
}
|
}
|
||||||
|
interface NewFishingLogRequest {
|
||||||
|
trip_id: string;
|
||||||
|
start_at: Date; // ISO datetime
|
||||||
|
start_lat: number;
|
||||||
|
start_lon: number;
|
||||||
|
weather_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TripUpdateStateRequest {
|
||||||
|
status: number;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
services/toast_service.tsx
Normal file
29
services/toast_service.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
|
export function showInfoToast(message: string) {
|
||||||
|
Toast.show({
|
||||||
|
type: "info",
|
||||||
|
text1: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showSuccessToast(message: string) {
|
||||||
|
Toast.show({
|
||||||
|
type: "success",
|
||||||
|
text1: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showErrorToast(message: string) {
|
||||||
|
Toast.show({
|
||||||
|
type: "error",
|
||||||
|
text1: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showWarningToast(message: string) {
|
||||||
|
Toast.show({
|
||||||
|
type: "warn",
|
||||||
|
text1: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export const useTrip = create<Trip>((set) => ({
|
|||||||
getTrip: async () => {
|
getTrip: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await queryTrip();
|
const response = await queryTrip();
|
||||||
console.log("Trip fetching: ", response.data);
|
console.log("Trip fetching API");
|
||||||
|
|
||||||
set({ trip: response.data, loading: false });
|
set({ trip: response.data, loading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user