diff --git a/app/_layout.tsx b/app/_layout.tsx
index 1b519ba..84af030 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -7,14 +7,15 @@ import { Stack, useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
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 { toastConfig } from "@/config";
import { setRouterInstance } from "@/config/auth";
import "@/global.css";
import { useColorScheme } from "@/hooks/use-color-scheme";
+import Toast from "react-native-toast-message";
import "../global.css";
-
export default function RootLayout() {
const colorScheme = useColorScheme();
const router = useRouter();
@@ -52,7 +53,7 @@ export default function RootLayout() {
/>
-
+
);
diff --git a/components/ButtonCreateNewHaulOrTrip.tsx b/components/ButtonCreateNewHaulOrTrip.tsx
index 13c2e8a..2873882 100644
--- a/components/ButtonCreateNewHaulOrTrip.tsx
+++ b/components/ButtonCreateNewHaulOrTrip.tsx
@@ -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 React, { useState } from "react";
-import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";
+import React, { useEffect, useState } from "react";
+import { Alert, StyleSheet, View } from "react-native";
+import IconButton from "./IconButton";
+import CreateOrUpdateHaulModal from "./tripInfo/modal/CreateOrUpdateHaulModal";
interface StartButtonProps {
- title?: string;
+ gpsData?: Model.GPSResponse;
onPress?: () => void;
}
const ButtonCreateNewHaulOrTrip: React.FC = ({
- title = "Bắt đầu mẻ lưới",
+ gpsData,
onPress,
}) => {
const [isStarted, setIsStarted] = useState(false);
+ const [isFinishHaulModalOpen, setIsFinishHaulModalOpen] = useState(false);
+
+ const { trip, getTrip } = useTrip();
+ useEffect(() => {
+ getTrip();
+ }, [trip === null]);
+
+ const checkHaulFinished = () => {
+ return trip?.fishing_logs?.some((h) => h.status === 0);
+ };
const handlePress = () => {
if (isStarted) {
@@ -53,24 +76,103 @@ const ButtonCreateNewHaulOrTrip: React.FC = ({
}
};
+ 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 (
-
-
-
-
- {isStarted ? "Kết thúc mẻ lưới" : title}
-
-
-
+
+ {trip?.trip_status === 2 ? (
+ }
+ type="primary"
+ style={{ backgroundColor: "green", borderRadius: 10 }}
+ onPress={async () => handleStartTrip(3)}
+ >
+ Bắt đầu chuyến đi
+
+ ) : checkHaulFinished() ? (
+ }
+ type="primary"
+ style={{ borderRadius: 10 }}
+ onPress={() => setIsFinishHaulModalOpen(true)}
+ >
+ Kết thúc mẻ lưới
+
+ ) : (
+ }
+ type="primary"
+ style={{ borderRadius: 10 }}
+ onPress={async () => {
+ createNewHaul();
+ }}
+ >
+ Bắt đầu mẻ lưới
+
+ )}
+
+
);
};
diff --git a/components/IconButton.tsx b/components/IconButton.tsx
new file mode 100644
index 0000000..d3a6d05
--- /dev/null
+++ b/components/IconButton.tsx
@@ -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.
+ loading?: boolean;
+ disabled?: boolean;
+ onPress?: (e?: GestureResponderEvent) => void;
+ children?: React.ReactNode; // label text
+ style?: StyleProp;
+ 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 = ({
+ 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 (
+
+
+ {loading ? (
+
+ ) : icon ? (
+ {icon}
+ ) : null}
+
+ {children ? (
+
+ {children}
+
+ ) : null}
+
+
+ );
+};
+
+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;
diff --git a/components/map/GPSInfoPanel.tsx b/components/map/GPSInfoPanel.tsx
index f56f317..a6a53bb 100644
--- a/components/map/GPSInfoPanel.tsx
+++ b/components/map/GPSInfoPanel.tsx
@@ -2,10 +2,10 @@ import { convertToDMS, kmhToKnot } from "@/utils/geom";
import { MaterialIcons } from "@expo/vector-icons";
import { useEffect, useRef, useState } from "react";
import { Animated, TouchableOpacity, View } from "react-native";
+import ButtonCreateNewHaulOrTrip from "../ButtonCreateNewHaulOrTrip";
import { Description } from "./Description";
-
type GPSInfoPanelProps = {
- gpsData: Model.GPSResonse | undefined;
+ gpsData: Model.GPSResponse | undefined;
};
const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
@@ -43,13 +43,28 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
position: "absolute",
bottom: blockBottom,
left: 5,
- width: 48,
- height: 48,
- backgroundColor: "blue",
+ // width: 48,
+ // height: 48,
+ // backgroundColor: "blue",
borderRadius: 4,
zIndex: 30,
}}
- />
+ >
+
+ {/* {
+ // showInfoToast("oad");
+ showWarningToast("This is a warning toast!");
+ }}
+ className="absolute top-2 right-2 z-10 bg-white rounded-full p-1"
+ >
+
+ */}
+
{
{/* Modal chi tiết */}
setModalVisible(false)}
+ onClose={() => {
+ console.log("OnCLose");
+ setModalVisible(false);
+ }}
netData={selectedNet}
/>
diff --git a/components/tripInfo/modal/CreateOrUpdateHaulModal.tsx b/components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
new file mode 100644
index 0000000..2e32d7d
--- /dev/null
+++ b/components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
@@ -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 = ({
+ isVisible,
+ onClose,
+ haulData,
+}) => {
+ const [isCreateMode, setIsCreateMode] = React.useState(!haulData);
+
+ return (
+
+ {isCreateMode ? "Create Haul" : "Update Haul"}
+
+ );
+};
+
+export default CreateOrUpdateHaulModal;
diff --git a/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx b/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx
index b5349d3..ceaf2f0 100644
--- a/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx
+++ b/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx
@@ -91,9 +91,9 @@ const NetDetailModal: React.FC = ({
}
}, [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
const fishNameOptions = [
@@ -210,7 +210,7 @@ const NetDetailModal: React.FC = ({
const handleCancel = () => {
setIsEditing(false);
- setEditableCatchList(netData.catchList || []);
+ setEditableCatchList(netData?.catchList || []);
};
const handleToggleExpanded = (index: number) => {
@@ -343,7 +343,10 @@ const NetDetailModal: React.FC = ({
{/* Content */}
{/* Thông tin chung */}
-
+
{/* Danh sách cá bắt được */}
@@ -372,7 +375,7 @@ const NetDetailModal: React.FC = ({
/>
{/* Ghi chú */}
-
+
diff --git a/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx b/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx
index 43da266..d53430d 100644
--- a/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx
+++ b/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx
@@ -17,7 +17,7 @@ interface NetDetail {
}
interface InfoSectionProps {
- netData: NetDetail;
+ netData?: NetDetail;
isCompleted: boolean;
}
@@ -25,6 +25,9 @@ export const InfoSection: React.FC = ({
netData,
isCompleted,
}) => {
+ if (!netData) {
+ return null;
+ }
const infoItems = [
{ label: "Số thứ tự", value: netData.stt },
{
diff --git a/components/tripInfo/modal/NetDetailModal/style/NetDetailModal.styles.ts b/components/tripInfo/modal/NetDetailModal/style/NetDetailModal.styles.ts
new file mode 100644
index 0000000..5089336
--- /dev/null
+++ b/components/tripInfo/modal/NetDetailModal/style/NetDetailModal.styles.ts
@@ -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,
+ },
+});
diff --git a/config/toast.ts b/config/toast.ts
deleted file mode 100644
index c884608..0000000
--- a/config/toast.ts
+++ /dev/null
@@ -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,
- });
-};
diff --git a/config/toast.tsx b/config/toast.tsx
new file mode 100644
index 0000000..0a5f098
--- /dev/null
+++ b/config/toast.tsx
@@ -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) => (
+ (
+
+
+
+ )}
+ />
+ ),
+ default: (props: BaseToastProps) => (
+ (
+
+
+
+ )}
+ />
+ ),
+ info: (props: BaseToastProps) => (
+ (
+
+
+
+ )}
+ />
+ ),
+ warn: (props: BaseToastProps) => (
+ (
+
+
+
+ )}
+ />
+ ),
+ error: (props: BaseToastProps) => (
+ (
+
+
+
+ )}
+ />
+ ),
+};
diff --git a/controller/DeviceController.ts b/controller/DeviceController.ts
index 09aaaae..2df127a 100644
--- a/controller/DeviceController.ts
+++ b/controller/DeviceController.ts
@@ -9,7 +9,7 @@ import {
import { transformEntityResponse } from "@/utils/tranform";
export async function queryGpsData() {
- return api.get(API_GET_GPS);
+ return api.get(API_GET_GPS);
}
export async function queryAlarm() {
diff --git a/controller/TripController.ts b/controller/TripController.ts
index 9bc7acd..a27397c 100644
--- a/controller/TripController.ts
+++ b/controller/TripController.ts
@@ -1,6 +1,18 @@
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() {
return api.get(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);
+}
diff --git a/controller/typings.d.ts b/controller/typings.d.ts
index e92b6c0..b9d00e7 100644
--- a/controller/typings.d.ts
+++ b/controller/typings.d.ts
@@ -7,7 +7,7 @@ declare namespace Model {
token?: string;
}
- interface GPSResonse {
+ interface GPSResponse {
lat: number;
lon: number;
s: number;
@@ -174,4 +174,16 @@ declare namespace Model {
fish_condition?: 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;
+ }
}
diff --git a/services/toast_service.tsx b/services/toast_service.tsx
new file mode 100644
index 0000000..3187d19
--- /dev/null
+++ b/services/toast_service.tsx
@@ -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,
+ });
+}