thêm toast, thêm logic cho phần ButtonCreateNewHaulOrTrip
This commit is contained in:
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
|
||||
29
components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
Normal file
29
components/tripInfo/modal/CreateOrUpdateHaulModal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { Modal, Text } from "react-native";
|
||||
|
||||
interface CreateOrUpdateHaulModalProps {
|
||||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
haulData?: Model.FishingLog | null;
|
||||
}
|
||||
|
||||
const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
|
||||
isVisible,
|
||||
onClose,
|
||||
haulData,
|
||||
}) => {
|
||||
const [isCreateMode, setIsCreateMode] = React.useState(!haulData);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={isVisible}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Text>{isCreateMode ? "Create Haul" : "Update Haul"}</Text>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrUpdateHaulModal;
|
||||
@@ -91,9 +91,9 @@ const NetDetailModal: React.FC<NetDetailModalProps> = ({
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user