Compare commits

...

3 Commits

Author SHA1 Message Date
04ca091f49 fix open table 2025-11-06 17:59:40 +07:00
Tran Anh Tuan
1b748285c9 sửa l fix xcall api getTrip 2025-11-06 17:45:03 +07:00
Tran Anh Tuan
aabd1109b2 thêm toast, thêm logic cho phần ButtonCreateNewHaulOrTrip 2025-11-06 17:30:04 +07:00
22 changed files with 831 additions and 310 deletions

View File

@@ -247,7 +247,7 @@ export async function login(body: Model.LoginRequestBody) {
```typescript
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:**
```typescript
const [gpsData, setGpsData] = useState<Model.GPSResonse | null>(null);
const [gpsData, setGpsData] = useState<Model.GPSResponse | null>(null);
```
2. **Fetch GPS data:**

View File

@@ -30,19 +30,11 @@ import {
convertWKTtoLatLngString,
} from "@/utils/geom";
import { useEffect, useRef, useState } from "react";
import {
Animated,
Image as RNImage,
StyleSheet,
View
} from "react-native";
import { Animated, Image as RNImage, StyleSheet, View } from "react-native";
import MapView, { Circle, Marker } from "react-native-maps";
export default function HomeScreen() {
const [gpsData, setGpsData] = useState<Model.GPSResonse | null>(
null
);
const [gpsData, setGpsData] = useState<Model.GPSResponse | null>(null);
const [alarmData, setAlarmData] = useState<Model.AlarmResponse | null>(null);
const [entityData, setEntityData] = useState<
Model.TransformedEntity[] | null
@@ -54,9 +46,8 @@ export default function HomeScreen() {
const [circleRadius, setCircleRadius] = useState(100);
const [zoomLevel, setZoomLevel] = useState(10);
const [isFirstLoad, setIsFirstLoad] = useState(true);
const [polylineCoordinates, setPolylineCoordinates] = useState<
PolylineWithLabelProps | null
>(null);
const [polylineCoordinates, setPolylineCoordinates] =
useState<PolylineWithLabelProps | null>(null);
const [polygonCoordinates, setPolygonCoordinates] = useState<
PolygonWithLabelProps[]
>([]);
@@ -76,7 +67,7 @@ export default function HomeScreen() {
getEntitiesEventBus();
getBanzonesEventBus();
getTrackPointsEventBus();
const queryGpsData = (gpsData: Model.GPSResonse) => {
const queryGpsData = (gpsData: Model.GPSResponse) => {
if (gpsData) {
// console.log("GPS Data: ", gpsData);
setGpsData(gpsData);
@@ -182,7 +173,6 @@ export default function HomeScreen() {
label: zone?.zone_name ?? "",
content: zone?.message ?? "",
});
}
} else if (geom_type === 1) {
// foundPolygon = true;
@@ -294,7 +284,8 @@ export default function HomeScreen() {
return (
<View
// edges={["top"]}
style={styles.container}>
style={styles.container}
>
<MapView
onMapReady={handleMapReady}
onRegionChangeComplete={handleRegionChangeComplete}
@@ -351,7 +342,9 @@ export default function HomeScreen() {
return (
<PolygonWithLabel
key={`polygon-${index}-${gpsData?.lat || 0}-${gpsData?.lon || 0}`}
key={`polygon-${index}-${gpsData?.lat || 0}-${
gpsData?.lon || 0
}`}
coordinates={polygon.coordinates}
label={polygon.label}
content={polygon.content}
@@ -367,7 +360,11 @@ export default function HomeScreen() {
)}
{gpsData !== null && (
<Marker
key={platform === IOS_PLATFORM ? `${gpsData.lat}-${gpsData.lon}` : "gps-data"}
key={
platform === IOS_PLATFORM
? `${gpsData.lat}-${gpsData.lon}`
: "gps-data"
}
coordinate={{
latitude: gpsData.lat,
longitude: gpsData.lon,
@@ -412,10 +409,11 @@ export default function HomeScreen() {
height: 32,
transform: [
{
rotate: `${typeof gpsData.h === "number" && !isNaN(gpsData.h)
rotate: `${
typeof gpsData.h === "number" && !isNaN(gpsData.h)
? gpsData.h
: 0
}deg`,
}deg`,
},
],
}}

View File

@@ -5,16 +5,14 @@ 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 { useTrip } from "@/state/use-trip";
import { useEffect } from "react";
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
export default function TripInfoScreen() {
const { trip, getTrip } = useTrip();
useEffect(() => {
getTrip();
}, []);
// const { trip, getTrip } = useTrip();
// useEffect(() => {
// getTrip();
// }, []);
return (
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>

View File

@@ -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() {
/>
</Stack>
<StatusBar style="auto" />
<Toast />
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
</ThemeProvider>
</GluestackUIProvider>
);

View File

@@ -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<StartButtonProps> = ({
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();
}, []);
const checkHaulFinished = () => {
return trip?.fishing_logs?.some((h) => h.status === 0);
};
const handlePress = () => {
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 (
<TouchableOpacity
style={[styles.button, isStarted && styles.buttonActive]}
onPress={handlePress}
activeOpacity={0.8}
>
<View style={styles.content}>
<AntDesign
name={isStarted ? "close" : "plus"}
size={18}
color="#fff"
style={styles.icon}
/>
<Text style={styles.text}>
{isStarted ? "Kết thúc mẻ lưới" : title}
</Text>
</View>
</TouchableOpacity>
<View>
{trip?.trip_status === 2 ? (
<IconButton
icon={<AntDesign name="plus" />}
type="primary"
style={{ backgroundColor: "green", borderRadius: 10 }}
onPress={async () => handleStartTrip(3)}
>
Bắt đu chuyến đi
</IconButton>
) : checkHaulFinished() ? (
<IconButton
icon={<AntDesign name="plus" color={"white"} />}
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);
}}
/>
</View>
);
};

159
components/IconButton.tsx Normal file
View 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;

View File

@@ -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,
}}
/>
>
<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
style={{

View File

@@ -5,161 +5,6 @@ import { Animated, Text, TouchableOpacity, View } from "react-native";
import CrewDetailModal from "./modal/CrewDetailModal";
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 [collapsed, setCollapsed] = useState(true);
const [contentHeight, setContentHeight] = useState<number>(0);
@@ -171,13 +16,13 @@ const CrewListTable: React.FC = () => {
const { trip } = useTrip();
const data: Model.TripCrews[] = trip?.crews ?? mockCrews;
const data: Model.TripCrews[] = trip?.crews ?? [];
const tongThanhVien = data.length;
// Reset animated height khi dữ liệu thay đổi
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
}, [data]);

View File

@@ -15,7 +15,7 @@ const FishingToolsTable: React.FC = () => {
// Reset animated height khi dữ liệu thay đổi
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
}, [data]);

View File

@@ -284,7 +284,10 @@ const NetListTable: React.FC = () => {
{/* Modal chi tiết */}
<NetDetailModal
visible={modalVisible}
onClose={() => setModalVisible(false)}
onClose={() => {
console.log("OnCLose");
setModalVisible(false);
}}
netData={selectedNet}
/>
</View>

View File

@@ -22,7 +22,7 @@ const TripCostTable: React.FC = () => {
// Reset animated height khi dữ liệu thay đổi
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
}, [data]);

View 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;

View File

@@ -91,9 +91,9 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
}
}, [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<NetDetailModalProps> = ({
const handleCancel = () => {
setIsEditing(false);
setEditableCatchList(netData.catchList || []);
setEditableCatchList(netData?.catchList || []);
};
const handleToggleExpanded = (index: number) => {
@@ -343,7 +343,10 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
{/* Content */}
<ScrollView style={styles.content}>
{/* Thông tin chung */}
<InfoSection netData={netData} isCompleted={isCompleted} />
<InfoSection
netData={netData ?? undefined}
isCompleted={isCompleted}
/>
{/* Danh sách cá bắt được */}
<CatchSectionHeader totalCatch={totalCatch} />
@@ -372,7 +375,7 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
/>
{/* Ghi chú */}
<NotesSection ghiChu={netData.ghiChu} />
<NotesSection ghiChu={netData?.ghiChu} />
</ScrollView>
</View>
</Modal>

View File

@@ -17,7 +17,7 @@ interface NetDetail {
}
interface InfoSectionProps {
netData: NetDetail;
netData?: NetDetail;
isCompleted: boolean;
}
@@ -25,6 +25,9 @@ export const InfoSection: React.FC<InfoSectionProps> = ({
netData,
isCompleted,
}) => {
if (!netData) {
return null;
}
const infoItems = [
{ label: "Số thứ tự", value: netData.stt },
{

View File

@@ -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,
},
});

View File

@@ -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
View 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>
)}
/>
),
};

View File

@@ -9,7 +9,7 @@ import {
import { transformEntityResponse } from "@/utils/tranform";
export async function queryGpsData() {
return api.get<Model.GPSResonse>(API_GET_GPS);
return api.get<Model.GPSResponse>(API_GET_GPS);
}
export async function queryAlarm() {

View File

@@ -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<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);
}

View File

@@ -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;
}
}

View 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,
});
}

View File

@@ -13,7 +13,7 @@ export const useTrip = create<Trip>((set) => ({
getTrip: async () => {
try {
const response = await queryTrip();
console.log("Trip fetching: ", response.data);
console.log("Trip fetching API");
set({ trip: response.data, loading: false });
} catch (error) {