Compare commits
3 Commits
62b18e5bc0
...
aabd1109b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aabd1109b2 | ||
| 1ef83c9b22 | |||
| 6288e79622 |
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
159
components/IconButton.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
GestureResponderEvent,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
|
||||
type ButtonType = "primary" | "default" | "dashed" | "text" | "link" | "danger";
|
||||
type ButtonShape = "default" | "circle" | "round";
|
||||
type ButtonSize = "small" | "middle" | "large";
|
||||
|
||||
export interface IconButtonProps {
|
||||
type?: ButtonType;
|
||||
shape?: ButtonShape;
|
||||
size?: ButtonSize;
|
||||
icon?: React.ReactNode; // render an icon component, e.g. <AntDesign name="plus" />
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
onPress?: (e?: GestureResponderEvent) => void;
|
||||
children?: React.ReactNode; // label text
|
||||
style?: StyleProp<ViewStyle>;
|
||||
block?: boolean; // full width
|
||||
activeOpacity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* IconButton
|
||||
* A lightweight Button component inspired by Ant Design Button API, tuned for React Native.
|
||||
* Accepts an `icon` prop as a React node for maximum flexibility.
|
||||
*/
|
||||
const IconButton: React.FC<IconButtonProps> = ({
|
||||
type = "default",
|
||||
shape = "default",
|
||||
size = "middle",
|
||||
icon,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
onPress,
|
||||
children,
|
||||
style,
|
||||
block = false,
|
||||
activeOpacity = 0.8,
|
||||
}) => {
|
||||
const sizeMap = {
|
||||
small: { height: 32, fontSize: 14, paddingHorizontal: 10 },
|
||||
middle: { height: 40, fontSize: 16, paddingHorizontal: 14 },
|
||||
large: { height: 48, fontSize: 18, paddingHorizontal: 18 },
|
||||
} as const;
|
||||
|
||||
const colors: Record<
|
||||
ButtonType,
|
||||
{ backgroundColor?: string; textColor: string; borderColor?: string }
|
||||
> = {
|
||||
primary: { backgroundColor: "#4ecdc4", textColor: "#fff" },
|
||||
default: {
|
||||
backgroundColor: "#f2f2f2",
|
||||
textColor: "#111",
|
||||
borderColor: "#e6e6e6",
|
||||
},
|
||||
dashed: {
|
||||
backgroundColor: "#fff",
|
||||
textColor: "#111",
|
||||
borderColor: "#d9d9d9",
|
||||
},
|
||||
text: { backgroundColor: "transparent", textColor: "#111" },
|
||||
link: { backgroundColor: "transparent", textColor: "#4ecdc4" },
|
||||
danger: { backgroundColor: "#e74c3c", textColor: "#fff" },
|
||||
};
|
||||
|
||||
const sz = sizeMap[size];
|
||||
const color = colors[type];
|
||||
|
||||
const isCircle = shape === "circle";
|
||||
const isRound = shape === "round";
|
||||
|
||||
const handlePress = (e: GestureResponderEvent) => {
|
||||
if (disabled || loading) return;
|
||||
onPress?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={activeOpacity}
|
||||
onPress={handlePress}
|
||||
disabled={disabled || loading}
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
height: sz.height,
|
||||
paddingHorizontal: isCircle ? 0 : sz.paddingHorizontal,
|
||||
backgroundColor: color.backgroundColor ?? "transparent",
|
||||
borderColor: color.borderColor ?? "transparent",
|
||||
borderWidth: type === "dashed" ? 1 : color.borderColor ? 1 : 0,
|
||||
width: isCircle ? sz.height : block ? "100%" : undefined,
|
||||
borderRadius: isCircle ? sz.height / 2 : isRound ? 999 : 8,
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
},
|
||||
type === "dashed" ? { borderStyle: "dashed" } : null,
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size={"small"}
|
||||
color={color.textColor}
|
||||
style={styles.iconContainer}
|
||||
/>
|
||||
) : icon ? (
|
||||
<View style={styles.iconContainer}>{icon}</View>
|
||||
) : null}
|
||||
|
||||
{children ? (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
styles.text,
|
||||
{
|
||||
color: color.textColor,
|
||||
fontSize: sz.fontSize,
|
||||
marginLeft: icon || loading ? 6 : 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 8,
|
||||
borderColor: "transparent",
|
||||
},
|
||||
content: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
text: {
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
export default IconButton;
|
||||
@@ -2,10 +2,10 @@ import { convertToDMS, kmhToKnot } from "@/utils/geom";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { 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={{
|
||||
|
||||
@@ -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]}>
|
||||
Mã đị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]}>
|
||||
Mã đị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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
29
components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
Normal file
29
components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { Modal, Text } from "react-native";
|
||||
|
||||
interface CreateOrUpdateHaulModalProps {
|
||||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
haulData?: Model.FishingLog | null;
|
||||
}
|
||||
|
||||
const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
||||
isVisible,
|
||||
onClose,
|
||||
haulData,
|
||||
}) => {
|
||||
const [isCreateMode, setIsCreateMode] = React.useState(!haulData);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={isVisible}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Text>{isCreateMode ? "Create Haul" : "Update Haul"}</Text>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrUpdateHaulModal;
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 8,
|
||||
backgroundColor: "#f8f9fa",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#e9ecef",
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "#333",
|
||||
},
|
||||
closeButton: {
|
||||
padding: 8,
|
||||
},
|
||||
closeButtonText: {
|
||||
fontSize: 16,
|
||||
color: "#007bff",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
fieldGroup: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#333",
|
||||
marginBottom: 4,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ccc",
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
fontSize: 16,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 16,
|
||||
color: "#555",
|
||||
paddingVertical: 8,
|
||||
},
|
||||
rowGroup: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
fishNameDropdown: {
|
||||
// Custom styles if needed
|
||||
},
|
||||
optionsStatusFishList: {
|
||||
// Custom styles if needed
|
||||
},
|
||||
optionsList: {
|
||||
maxHeight: 150,
|
||||
borderWidth: 1,
|
||||
borderColor: "#ccc",
|
||||
borderRadius: 4,
|
||||
backgroundColor: "#fff",
|
||||
position: "absolute",
|
||||
top: 40,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
},
|
||||
selectButton: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ccc",
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
backgroundColor: "#fff",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
selectButtonText: {
|
||||
fontSize: 16,
|
||||
color: "#333",
|
||||
},
|
||||
optionItem: {
|
||||
padding: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#eee",
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 16,
|
||||
color: "#333",
|
||||
},
|
||||
card: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
backgroundColor: "#f9f9f9",
|
||||
},
|
||||
removeButton: {
|
||||
backgroundColor: "#dc3545",
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
alignSelf: "flex-end",
|
||||
marginTop: 8,
|
||||
},
|
||||
removeButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
},
|
||||
errorText: {
|
||||
color: "#dc3545",
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
buttonGroup: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
marginTop: 16,
|
||||
},
|
||||
editButton: {
|
||||
backgroundColor: "#007bff",
|
||||
padding: 10,
|
||||
borderRadius: 4,
|
||||
},
|
||||
editButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: "#28a745",
|
||||
padding: 10,
|
||||
borderRadius: 4,
|
||||
},
|
||||
addButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: "#007bff",
|
||||
padding: 10,
|
||||
borderRadius: 4,
|
||||
},
|
||||
saveButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: "#6c757d",
|
||||
padding: 10,
|
||||
borderRadius: 4,
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
addFishButton: {
|
||||
backgroundColor: "#17a2b8",
|
||||
padding: 10,
|
||||
borderRadius: 4,
|
||||
marginBottom: 16,
|
||||
},
|
||||
addFishButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,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>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export enum ToastType {
|
||||
SUCCESS = "success",
|
||||
ERROR = "error",
|
||||
WARNING = "error", // react-native-toast-message không có 'warning', dùng 'error'
|
||||
INFO = "info",
|
||||
}
|
||||
|
||||
/**
|
||||
* Success toast
|
||||
*/
|
||||
export const showToastSuccess = (message: string, title?: string): void => {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: title || "Success",
|
||||
text2: message,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Error toast
|
||||
*/
|
||||
export const showToastError = (message: string, title?: string): void => {
|
||||
Toast.show({
|
||||
type: ToastType.ERROR,
|
||||
text1: title || ToastType.ERROR,
|
||||
text2: message,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Info toast
|
||||
*/
|
||||
export const showToastInfo = (message: string, title?: string): void => {
|
||||
Toast.show({
|
||||
type: ToastType.INFO,
|
||||
text1: title || ToastType.INFO,
|
||||
text2: message,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Warning toast
|
||||
*/
|
||||
export const showToastWarning = (message: string, title?: string): void => {
|
||||
Toast.show({
|
||||
type: ToastType.WARNING,
|
||||
text1: title || ToastType.WARNING,
|
||||
text2: message,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Default toast
|
||||
*/
|
||||
export const showToastDefault = (message: string, title?: string): void => {
|
||||
Toast.show({
|
||||
type: ToastType.INFO,
|
||||
text1: title || ToastType.INFO,
|
||||
text2: message,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom toast với type tùy chọn
|
||||
*/
|
||||
export const show = (
|
||||
message: string,
|
||||
type: ToastType,
|
||||
title?: string
|
||||
): void => {
|
||||
const titleText = title || type.charAt(0).toUpperCase() + type.slice(1);
|
||||
Toast.show({
|
||||
type,
|
||||
text1: titleText,
|
||||
text2: message,
|
||||
});
|
||||
};
|
||||
214
config/toast.tsx
Normal file
214
config/toast.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
BaseToast,
|
||||
BaseToastProps,
|
||||
SuccessToast,
|
||||
} from "react-native-toast-message";
|
||||
|
||||
export const Colors: any = {
|
||||
light: {
|
||||
text: "#000",
|
||||
back: "#ffffff",
|
||||
},
|
||||
dark: {
|
||||
text: "#ffffff",
|
||||
back: "#2B2D2E",
|
||||
},
|
||||
default: "#3498db",
|
||||
info: "#3498db",
|
||||
success: "#07bc0c",
|
||||
warn: {
|
||||
background: "#ffffff",
|
||||
text: "black",
|
||||
iconColor: "#f1c40f",
|
||||
},
|
||||
error: {
|
||||
background: "#ffffff",
|
||||
text: "black",
|
||||
iconColor: "#e74c3c",
|
||||
},
|
||||
textDefault: "#4c4c4c",
|
||||
textDark: "black",
|
||||
};
|
||||
|
||||
export const toastConfig = {
|
||||
success: (props: BaseToastProps) => (
|
||||
<SuccessToast
|
||||
{...props}
|
||||
style={{
|
||||
borderLeftColor: Colors.success,
|
||||
backgroundColor: Colors.success,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
text1Style={{
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
text2Style={{
|
||||
color: "#f0f0f0",
|
||||
}}
|
||||
renderLeadingIcon={() => (
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 40,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="check-circle" size={30} color="white" />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
default: (props: BaseToastProps) => (
|
||||
<BaseToast
|
||||
{...props}
|
||||
style={{
|
||||
borderLeftColor: Colors.default,
|
||||
backgroundColor: Colors.default,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
text1Style={{
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
text2Style={{
|
||||
color: "#f0f0f0",
|
||||
}}
|
||||
renderLeadingIcon={() => (
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 50,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="info" size={30} color="white" />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
info: (props: BaseToastProps) => (
|
||||
<BaseToast
|
||||
{...props}
|
||||
style={{
|
||||
borderLeftColor: Colors.info,
|
||||
backgroundColor: Colors.info,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
text1Style={{
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
text2Style={{
|
||||
color: "#f0f0f0",
|
||||
}}
|
||||
renderLeadingIcon={() => (
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 50,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="info-outline" size={30} color="white" />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
warn: (props: BaseToastProps) => (
|
||||
<BaseToast
|
||||
{...props}
|
||||
style={{
|
||||
borderLeftColor: Colors.warn.background,
|
||||
backgroundColor: Colors.warn.background,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
text1Style={{
|
||||
color: Colors.warn.text,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
text2Style={{
|
||||
color: "#333",
|
||||
}}
|
||||
renderLeadingIcon={() => (
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 50,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="warning"
|
||||
size={30}
|
||||
color={Colors.warn.iconColor}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
error: (props: BaseToastProps) => (
|
||||
<BaseToast
|
||||
{...props}
|
||||
style={{
|
||||
borderLeftColor: Colors.error.background,
|
||||
backgroundColor: Colors.error.background,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
text1Style={{
|
||||
color: Colors.error.text,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
text2Style={{
|
||||
color: "#f0f0f0",
|
||||
}}
|
||||
renderLeadingIcon={() => (
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 50,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="error"
|
||||
size={30}
|
||||
color={Colors.error.iconColor}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
18
controller/TripController.ts
Normal file
18
controller/TripController.ts
Normal 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);
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
98
controller/typings.d.ts
vendored
98
controller/typings.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
29
services/toast_service.tsx
Normal file
29
services/toast_service.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export function showInfoToast(message: string) {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
text1: message,
|
||||
});
|
||||
}
|
||||
|
||||
export function showSuccessToast(message: string) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: message,
|
||||
});
|
||||
}
|
||||
|
||||
export function showErrorToast(message: string) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: message,
|
||||
});
|
||||
}
|
||||
|
||||
export function showWarningToast(message: string) {
|
||||
Toast.show({
|
||||
type: "warn",
|
||||
text1: message,
|
||||
});
|
||||
}
|
||||
26
state/use-trip.ts
Normal file
26
state/use-trip.ts
Normal 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,
|
||||
}));
|
||||
Reference in New Issue
Block a user