Compare commits

...

3 Commits

Author SHA1 Message Date
Tran Anh Tuan
aabd1109b2 thêm toast, thêm logic cho phần ButtonCreateNewHaulOrTrip 2025-11-06 17:30:04 +07:00
1ef83c9b22 fill data API CrewList, FishingTools, TripCost 2025-11-06 17:28:10 +07:00
6288e79622 call API trip 2025-11-05 23:47:54 +07:00
25 changed files with 1215 additions and 385 deletions

View File

@@ -5,10 +5,17 @@ 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();
}, []);
return (
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
<View style={styles.header}>

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

@@ -1,86 +1,162 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import React, { useRef, useState } from "react";
import { useTrip } from "@/state/use-trip";
import React, { useEffect, useRef, useState } from "react";
import { Animated, Text, TouchableOpacity, View } from "react-native";
import CrewDetailModal from "./modal/CrewDetailModal";
import styles from "./style/CrewListTable.styles";
// ---------------------------
// 🧩 Interface
// ---------------------------
interface CrewMember {
id: string;
maDinhDanh: string;
ten: string;
chucVu: string;
ngaySinh?: string;
cccd?: string;
soDienThoai?: string;
diaChi?: string;
ngayVaoLam?: string;
trinhDoChuyenMon?: string;
bangCap?: string;
tinhTrang?: string;
}
// ---------------------------
// ⚓ Dữ liệu mẫu
// ---------------------------
const data: CrewMember[] = [
const mockCrews: Model.TripCrews[] = [
{
id: "10",
maDinhDanh: "ChuTau",
ten: "Nguyễn Nhật Minh",
chucVu: "Chủ tàu",
ngaySinh: "08/06/2006",
cccd: "079085012345",
soDienThoai: "0912345678",
diaChi: "Hà Nội",
ngayVaoLam: "",
trinhDoChuyenMon: "Thuyền trưởng hạng I",
bangCap: "Bằng thuyền trưởng xa bờ",
tinhTrang: "Đang làm việc",
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"),
},
},
{
id: "1",
maDinhDanh: "TV001",
ten: "Nguyễn Văn A",
chucVu: "Thuyền trưởng",
ngaySinh: "20/05/1988",
cccd: "079088011111",
soDienThoai: "0901234567",
diaChi: "456 Đường Cảng, Phường Thanh Khê, Đà Nẵng",
ngayVaoLam: "15/06/2015",
trinhDoChuyenMon: "Thuyền trưởng hạng II",
bangCap: "Bằng thuyền trưởng ven bờ",
tinhTrang: "Đang làm việc",
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"),
},
},
{
id: "2",
maDinhDanh: "TV002",
ten: "Trần Văn B",
chucVu: "Máy trưởng",
ngaySinh: "10/08/1990",
cccd: "079090022222",
soDienThoai: "0987654321",
diaChi: "789 Đường Nguyễn Văn Linh, Quận Sơn Trà, Đà Nẵng",
ngayVaoLam: "20/03/2016",
trinhDoChuyenMon: "Máy trưởng hạng III",
bangCap: "Bằng máy trưởng ven bờ",
tinhTrang: "Đang làm việc",
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"),
},
},
{
id: "3",
maDinhDanh: "TV003",
ten: "Lê Văn C",
chucVu: "Thủy thủ",
ngaySinh: "25/12/1995",
cccd: "079095033333",
soDienThoai: "0976543210",
diaChi: "321 Đường Hoàng Sa, Quận Ngũ Hành Sơn, Đà Nẵng",
ngayVaoLam: "10/07/2018",
trinhDoChuyenMon: "Thủy thủ hạng I",
bangCap: "Chứng chỉ thủy thủ",
tinhTrang: "Đang làm việc",
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"),
},
},
];
@@ -89,9 +165,22 @@ const CrewListTable: React.FC = () => {
const [contentHeight, setContentHeight] = useState<number>(0);
const animatedHeight = useRef(new Animated.Value(0)).current;
const [modalVisible, setModalVisible] = useState(false);
const [selectedCrew, setSelectedCrew] = useState<CrewMember | null>(null);
const [selectedCrew, setSelectedCrew] = useState<Model.TripCrews | null>(
null
);
const { trip } = useTrip();
const data: Model.TripCrews[] = trip?.crews ?? mockCrews;
const tongThanhVien = data.length;
// Reset animated height khi dữ liệu thay đổi
useEffect(() => {
setContentHeight(0); // Reset để tính lại chiều cao
setCollapsed(true); // Reset về trạng thái gập lại
}, [data]);
const handleToggle = () => {
const toValue = collapsed ? contentHeight : 0;
Animated.timing(animatedHeight, {
@@ -103,7 +192,7 @@ const CrewListTable: React.FC = () => {
};
const handleCrewPress = (crewId: string) => {
const crew = data.find((item) => item.id === crewId);
const crew = data.find((item) => item.Person.personal_id === crewId);
if (crew) {
setSelectedCrew(crew);
setModalVisible(true);
@@ -146,9 +235,6 @@ const CrewListTable: React.FC = () => {
>
{/* Header */}
<View style={[styles.row, styles.tableHeader]}>
<Text style={[styles.cell, styles.left, styles.headerText]}>
đnh danh
</Text>
<View style={styles.cellWrapper}>
<Text style={[styles.cell, styles.headerText]}>Tên</Text>
</View>
@@ -159,25 +245,23 @@ const CrewListTable: React.FC = () => {
{/* Body */}
{data.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.maDinhDanh}</Text>
<View key={item.Person.personal_id} style={styles.row}>
<TouchableOpacity
style={styles.cellWrapper}
onPress={() => handleCrewPress(item.id)}
onPress={() => handleCrewPress(item.Person.personal_id)}
>
<Text style={[styles.cell, styles.linkText]}>{item.ten}</Text>
<Text style={[styles.cell, styles.linkText]}>
{item.Person.name}
</Text>
</TouchableOpacity>
<Text style={[styles.cell, styles.right]}>{item.chucVu}</Text>
<Text style={[styles.cell, styles.right]}>{item.role}</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.left, styles.footerText]}>
Tổng cộng
</Text>
<Text style={[styles.cell, styles.footerText]}>Tổng cộng</Text>
<Text style={[styles.cell, styles.footerTotal]}>{tongThanhVien}</Text>
<Text style={[styles.cell, styles.right]}></Text>
</View>
</View>
@@ -185,9 +269,6 @@ const CrewListTable: React.FC = () => {
<Animated.View style={{ height: animatedHeight, overflow: "hidden" }}>
{/* Header */}
<View style={[styles.row, styles.tableHeader]}>
<Text style={[styles.cell, styles.left, styles.headerText]}>
đnh danh
</Text>
<View style={styles.cellWrapper}>
<Text style={[styles.cell, styles.headerText]}>Tên</Text>
</View>
@@ -198,25 +279,23 @@ const CrewListTable: React.FC = () => {
{/* Body */}
{data.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.maDinhDanh}</Text>
<View key={item.Person.personal_id} style={styles.row}>
<TouchableOpacity
style={styles.cellWrapper}
onPress={() => handleCrewPress(item.id)}
onPress={() => handleCrewPress(item.Person.personal_id)}
>
<Text style={[styles.cell, styles.linkText]}>{item.ten}</Text>
<Text style={[styles.cell, styles.linkText]}>
{item.Person.name}
</Text>
</TouchableOpacity>
<Text style={[styles.cell, styles.right]}>{item.chucVu}</Text>
<Text style={[styles.cell, styles.right]}>{item.role}</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.left, styles.footerText]}>
Tổng cộng
</Text>
<Text style={[styles.cell, styles.footerText]}>Tổng cộng</Text>
<Text style={[styles.cell, styles.footerTotal]}>{tongThanhVien}</Text>
<Text style={[styles.cell, styles.right]}></Text>
</View>
</Animated.View>

View File

@@ -1,31 +1,23 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import React, { useRef, useState } from "react";
import { useTrip } from "@/state/use-trip";
import React, { useEffect, useRef, useState } from "react";
import { Animated, Text, TouchableOpacity, View } from "react-native";
import styles from "./style/FishingToolsTable.styles";
// ---------------------------
// 🧩 Interface
// ---------------------------
interface ToolItem {
id: string;
ten: string;
soLuong: number;
}
// ---------------------------
// 🎣 Dữ liệu mẫu
// ---------------------------
const data: ToolItem[] = [
{ id: "1", ten: "Lưới kéo", soLuong: 1 },
{ id: "2", ten: "Cần câu", soLuong: 1 },
{ id: "3", ten: "Mồi câu", soLuong: 5 },
];
const FishingToolsTable: React.FC = () => {
const [collapsed, setCollapsed] = useState(true);
const [contentHeight, setContentHeight] = useState<number>(0);
const animatedHeight = useRef(new Animated.Value(0)).current;
const tongSoLuong = data.reduce((sum, item) => sum + item.soLuong, 0);
const { trip } = useTrip();
const data: Model.FishingGear[] = trip?.fishing_gears ?? [];
const tongSoLuong = data.reduce((sum, item) => sum + Number(item.number), 0);
// Reset animated height khi dữ liệu thay đổi
useEffect(() => {
setContentHeight(0); // Reset để tính lại chiều cao
setCollapsed(true); // Reset về trạng thái gập lại
}, [data]);
const handleToggle = () => {
const toValue = collapsed ? contentHeight : 0;
@@ -73,10 +65,10 @@ const FishingToolsTable: React.FC = () => {
</View>
{/* Body */}
{data.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.ten}</Text>
<Text style={[styles.cell, styles.right]}>{item.soLuong}</Text>
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.name}</Text>
<Text style={[styles.cell, styles.right]}>{item.number}</Text>
</View>
))}
@@ -102,10 +94,10 @@ const FishingToolsTable: React.FC = () => {
</View>
{/* Body */}
{data.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.ten}</Text>
<Text style={[styles.cell, styles.right]}>{item.soLuong}</Text>
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.name}</Text>
<Text style={[styles.cell, styles.right]}>{item.number}</Text>
</View>
))}

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

@@ -1,59 +1,10 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import React, { useRef, useState } from "react";
import { useTrip } from "@/state/use-trip";
import React, { useEffect, useRef, useState } from "react";
import { Animated, Text, TouchableOpacity, View } from "react-native";
import TripCostDetailModal from "./modal/TripCostDetailModal";
import styles from "./style/TripCostTable.styles";
// ---------------------------
// 🧩 Interface
// ---------------------------
interface CostItem {
id: string;
loai: string;
soLuong: number;
donVi: string;
chiPhi: number;
tongChiPhi: number;
}
// ---------------------------
// 📊 Dữ liệu mẫu
// ---------------------------
const data: CostItem[] = [
{
id: "1",
loai: "Nhiên liệu",
soLuong: 1000,
donVi: "liters",
chiPhi: 20000,
tongChiPhi: 20000000,
},
{
id: "2",
loai: "Lương thực",
soLuong: 500,
donVi: "kg",
chiPhi: 30000,
tongChiPhi: 15000000,
},
{
id: "3",
loai: "Lương thuyền viên",
soLuong: 10,
donVi: "people",
chiPhi: 5000000,
tongChiPhi: 50000000,
},
{
id: "4",
loai: "Muối đá",
soLuong: 100,
donVi: "kg",
chiPhi: 20000,
tongChiPhi: 2000000,
},
];
// ---------------------------
// 💰 Component chính
// ---------------------------
@@ -63,7 +14,17 @@ const TripCostTable: React.FC = () => {
const [contentHeight, setContentHeight] = useState<number>(0);
const [modalVisible, setModalVisible] = useState(false);
const animatedHeight = useRef(new Animated.Value(0)).current;
const tongCong = data.reduce((sum, item) => sum + item.tongChiPhi, 0);
const { trip } = useTrip();
const data: Model.TripCost[] = trip?.trip_cost ?? [];
const tongCong = data.reduce((sum, item) => sum + item.total_cost, 0);
// Reset animated height khi dữ liệu thay đổi
useEffect(() => {
setContentHeight(0); // Reset để tính lại chiều cao
setCollapsed(true); // Reset về trạng thái gập lại
}, [data]);
const handleToggle = () => {
const toValue = collapsed ? contentHeight : 0;
@@ -132,11 +93,11 @@ const TripCostTable: React.FC = () => {
</View>
{/* Body */}
{data.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.loai}</Text>
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.type}</Text>
<Text style={[styles.cell, styles.right]}>
{item.tongChiPhi.toLocaleString()}
{item.total_cost.toLocaleString()}
</Text>
</View>
))}
@@ -152,12 +113,14 @@ const TripCostTable: React.FC = () => {
</View>
{/* View Detail Button */}
<TouchableOpacity
style={styles.viewDetailButton}
onPress={handleViewDetail}
>
<Text style={styles.viewDetailText}>Xem chi tiết</Text>
</TouchableOpacity>
{data.length > 0 && (
<TouchableOpacity
style={styles.viewDetailButton}
onPress={handleViewDetail}
>
<Text style={styles.viewDetailText}>Xem chi tiết</Text>
</TouchableOpacity>
)}
</View>
<Animated.View style={{ height: animatedHeight, overflow: "hidden" }}>
@@ -170,11 +133,11 @@ const TripCostTable: React.FC = () => {
</View>
{/* Body */}
{data.map((item) => (
<View key={item.id} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.loai}</Text>
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.type}</Text>
<Text style={[styles.cell, styles.right]}>
{item.tongChiPhi.toLocaleString()}
{item.total_cost.toLocaleString()}
</Text>
</View>
))}
@@ -190,12 +153,14 @@ const TripCostTable: React.FC = () => {
</View>
{/* View Detail Button */}
<TouchableOpacity
style={styles.viewDetailButton}
onPress={handleViewDetail}
>
<Text style={styles.viewDetailText}>Xem chi tiết</Text>
</TouchableOpacity>
{data.length > 0 && (
<TouchableOpacity
style={styles.viewDetailButton}
onPress={handleViewDetail}
>
<Text style={styles.viewDetailText}>Xem chi tiết</Text>
</TouchableOpacity>
)}
</Animated.View>
{/* Modal */}

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

@@ -6,25 +6,11 @@ import styles from "./style/CrewDetailModal.styles";
// ---------------------------
// 🧩 Interface
// ---------------------------
interface CrewMember {
id: string;
maDinhDanh: string;
ten: string;
chucVu: string;
ngaySinh?: string;
cccd?: string;
soDienThoai?: string;
diaChi?: string;
ngayVaoLam?: string;
trinhDoChuyenMon?: string;
bangCap?: string;
tinhTrang?: string;
}
interface CrewDetailModalProps {
visible: boolean;
onClose: () => void;
crewData: CrewMember | null;
crewData: Model.TripCrews | null;
}
// ---------------------------
@@ -38,20 +24,28 @@ const CrewDetailModal: React.FC<CrewDetailModalProps> = ({
if (!crewData) return null;
const infoItems = [
{ label: "Mã định danh", value: crewData.maDinhDanh },
{ label: "Họ và tên", value: crewData.ten },
{ label: "Chức vụ", value: crewData.chucVu },
{ label: "Ngày sinh", value: crewData.ngaySinh || "Chưa cập nhật" },
{ label: "CCCD/CMND", value: crewData.cccd || "Chưa cập nhật" },
{ label: "Số điện thoại", value: crewData.soDienThoai || "Chưa cập nhật" },
{ label: "Địa chỉ", value: crewData.diaChi || "Chưa cập nhật" },
{ label: "Ngày vào làm", value: crewData.ngayVaoLam || "Chưa cập nhật" },
{ label: "Mã định danh", value: crewData.Person.personal_id },
{ label: "Họ và tên", value: crewData.Person.name },
{ label: "Chức vụ", value: crewData.role },
{
label: "Trình độ chuyên môn",
value: crewData.trinhDoChuyenMon || "Chưa cập nhật",
label: "Ngày sinh",
value: crewData.Person.birth_date
? new Date(crewData.Person.birth_date).toLocaleDateString()
: "Chưa cập nhật",
},
{ label: "Số điện thoại", value: crewData.Person.phone || "Chưa cập nhật" },
{ label: "Địa chỉ", value: crewData.Person.address || "Chưa cập nhật" },
{
label: "Ngày vào làm",
value: crewData.joined_at
? new Date(crewData.joined_at).toLocaleDateString()
: "Chưa cập nhật",
},
{ label: "Ghi chú", value: crewData.note || "Chưa cập nhật" },
{
label: "Tình trạng",
value: crewData.left_at ? "Đã nghỉ" : "Đang làm việc",
},
{ label: "Bằng cấp", value: crewData.bangCap || "Chưa cập nhật" },
{ label: "Tình trạng", value: crewData.tinhTrang || "Đang làm việc" },
];
return (

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,5 +1,5 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import {
KeyboardAvoidingView,
Modal,
@@ -15,19 +15,10 @@ import styles from "./style/TripCostDetailModal.styles";
// ---------------------------
// 🧩 Interface
// ---------------------------
interface CostItem {
id: string;
loai: string;
soLuong: number;
donVi: string;
chiPhi: number;
tongChiPhi: number;
}
interface TripCostDetailModalProps {
visible: boolean;
onClose: () => void;
data: CostItem[];
data: Model.TripCost[];
}
// ---------------------------
@@ -39,9 +30,14 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
data,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editableData, setEditableData] = useState<CostItem[]>(data);
const [editableData, setEditableData] = useState<Model.TripCost[]>(data);
const tongCong = editableData.reduce((sum, item) => sum + item.tongChiPhi, 0);
// Cập nhật editableData khi props data thay đổi (API fetch xong)
useEffect(() => {
setEditableData(data);
}, [data]);
const tongCong = editableData.reduce((sum, item) => sum + item.total_cost, 0);
const handleEdit = () => {
setIsEditing(!isEditing);
@@ -50,7 +46,7 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
const handleSave = () => {
setIsEditing(false);
// TODO: Save data to backend
// console.log("Saved data:", editableData);
console.log("Saved data:", editableData);
};
const handleCancel = () => {
@@ -58,16 +54,23 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
setEditableData(data); // Reset to original data
};
const updateItem = (id: string, field: keyof CostItem, value: string) => {
const updateItem = (
index: number,
field: keyof Model.TripCost,
value: string
) => {
setEditableData((prev) =>
prev.map((item) => {
if (item.id === id) {
const numValue =
field === "loai" || field === "donVi" ? value : Number(value) || 0;
const updated = { ...item, [field]: numValue };
// Recalculate tongChiPhi
if (field === "soLuong" || field === "chiPhi") {
updated.tongChiPhi = updated.soLuong * updated.chiPhi;
prev.map((item, idx) => {
if (idx === index) {
const updated = { ...item, [field]: value };
// Recalculate total_cost
if (field === "amount" || field === "cost_per_unit") {
const amount =
Number(field === "amount" ? value : item.amount) || 0;
const costPerUnit =
Number(field === "cost_per_unit" ? value : item.cost_per_unit) ||
0;
updated.total_cost = amount * costPerUnit;
}
return updated;
}
@@ -133,15 +136,15 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
{/* Content */}
<ScrollView style={styles.content}>
{editableData.map((item) => (
<View key={item.id} style={styles.itemCard}>
{editableData.map((item, index) => (
<View key={index} style={styles.itemCard}>
{/* Loại */}
<View style={styles.fieldGroup}>
<Text style={styles.label}>Loại chi phí</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={item.loai}
onChangeText={(value) => updateItem(item.id, "loai", value)}
value={item.type}
onChangeText={(value) => updateItem(index, "type", value)}
editable={isEditing}
placeholder="Nhập loại chi phí"
/>
@@ -155,9 +158,9 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
<Text style={styles.label}>Số lượng</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={String(item.soLuong)}
value={String(item.amount ?? "")}
onChangeText={(value) =>
updateItem(item.id, "soLuong", value)
updateItem(index, "amount", value)
}
editable={isEditing}
keyboardType="numeric"
@@ -168,10 +171,8 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
<Text style={styles.label}>Đơn vị</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={item.donVi}
onChangeText={(value) =>
updateItem(item.id, "donVi", value)
}
value={item.unit}
onChangeText={(value) => updateItem(index, "unit", value)}
editable={isEditing}
placeholder="kg, lít..."
/>
@@ -183,9 +184,9 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
<Text style={styles.label}>Chi phí/đơn vị (VNĐ)</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={String(item.chiPhi)}
value={String(item.cost_per_unit ?? "")}
onChangeText={(value) =>
updateItem(item.id, "chiPhi", value)
updateItem(index, "cost_per_unit", value)
}
editable={isEditing}
keyboardType="numeric"
@@ -198,7 +199,7 @@ const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
<Text style={styles.label}>Tổng chi phí</Text>
<View style={styles.totalContainer}>
<Text style={styles.totalText}>
{item.tongChiPhi.toLocaleString()} VNĐ
{item.total_cost.toLocaleString()} VNĐ
</Text>
</View>
</View>

View File

@@ -48,13 +48,10 @@ export default StyleSheet.create({
textAlign: "center",
},
cellWrapper: {
flex: 1.5,
flex: 1,
justifyContent: "center",
alignItems: "center",
},
left: {
textAlign: "center",
},
right: {
textAlign: "center",
},

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

@@ -44,5 +44,3 @@ export const API_UPDATE_FISHING_LOGS = "/api/sgw/fishingLog";
export const API_SOS = "/api/sgw/sos";
export const API_PATH_SHIP_TRACK_POINTS = "/api/sgw/trackpoints";
export const API_GET_ALL_BANZONES = "/api/sgw/banzones";
// Smatec

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

@@ -0,0 +1,18 @@
import { api } from "@/config";
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

@@ -1,4 +1,5 @@
import * as AuthController from "./AuthController";
import * as DeviceController from "./DeviceController";
import * as MapController from "./MapController";
export { AuthController, DeviceController, MapController };
import * as TripController from "./TripController";
export { AuthController, DeviceController, MapController, TripController };

View File

@@ -7,7 +7,7 @@ declare namespace Model {
token?: string;
}
interface GPSResonse {
interface GPSResponse {
lat: number;
lon: number;
s: number;
@@ -90,4 +90,100 @@ declare namespace Model {
message?: string;
started_at?: number;
}
// Trip
interface Trip {
id: string;
ship_id: string;
ship_length: number;
vms_id: string;
name: string;
fishing_gears: FishingGear[]; // Dụng cụ đánh cá
crews?: TripCrews[]; // Thuyền viên
departure_time: string; // ISO datetime string
departure_port_id: number;
arrival_time: string; // ISO datetime string
arrival_port_id: number;
fishing_ground_codes: number[];
total_catch_weight: number | null;
total_species_caught: number | null;
trip_cost: TripCost[]; // Chi phí chuyến đi
trip_status: number;
approved_by: string;
notes: string | null;
fishing_logs: FishingLog[] | null; // tuỳ dữ liệu chi tiết có thể định nghĩa thêm
sync: boolean;
}
// Dụng cụ đánh cá
interface FishingGear {
name: string;
number: string;
}
// Thuyền viên
interface TripCrews {
TripID: string;
PersonalID: string;
role: string;
joined_at: Date;
left_at: Date | null;
note: string | null;
Person: TripCrewPerson;
}
interface TripCrewPerson {
personal_id: string;
name: string;
phone: string;
email: string;
birth_date: Date; // ISO string (có thể chuyển sang Date nếu parse trước)
note: string;
address: string;
created_at: Date;
updated_at: Date;
}
// Chi phí chuyến đi
interface TripCost {
type: string;
unit: string;
amount: number;
total_cost: number;
cost_per_unit: number;
}
// Thông tin mẻ lưới
interface FishingLog {
fishing_log_id: string;
trip_id: string;
start_at: Date; // ISO datetime
end_at: Date; // ISO datetime
start_lat: number;
start_lon: number;
haul_lat: number;
haul_lon: number;
status: number;
weather_description: string;
info?: FishingLogInfo[]; // Thông tin cá
sync: boolean;
}
// Thông tin cá
interface FishingLogInfo {
fish_species_id?: number;
fish_name?: string;
catch_number?: number;
catch_unit?: string;
fish_size?: number;
fish_rarity?: number;
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,
});
}

26
state/use-trip.ts Normal file
View File

@@ -0,0 +1,26 @@
import { queryTrip } from "@/controller/TripController";
import { create } from "zustand";
type Trip = {
trip: Model.Trip | null;
getTrip: () => Promise<void>;
error: string | null;
loading?: boolean;
};
export const useTrip = create<Trip>((set) => ({
trip: null,
getTrip: async () => {
try {
const response = await queryTrip();
console.log("Trip fetching: ", response.data);
set({ trip: response.data, loading: false });
} catch (error) {
console.error("Error when fetch trip: ", error);
set({ error: "Failed to fetch trip data", loading: false });
set({ trip: null });
}
},
error: null,
}));