Cập nhật API thêm trip (validate)
This commit is contained in:
@@ -37,6 +37,7 @@ export default function diary() {
|
|||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const isInitialLoad = useRef(true);
|
const isInitialLoad = useRef(true);
|
||||||
|
const flatListRef = useRef<FlatList>(null);
|
||||||
|
|
||||||
// Body call API things (đang fix cứng)
|
// Body call API things (đang fix cứng)
|
||||||
const payloadThings: Model.SearchThingBody = {
|
const payloadThings: Model.SearchThingBody = {
|
||||||
@@ -198,6 +199,25 @@ export default function diary() {
|
|||||||
// TODO: Show confirmation dialog and delete trip
|
// TODO: Show confirmation dialog and delete trip
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle sau khi thêm chuyến đi thành công
|
||||||
|
const handleTripAddSuccess = useCallback(() => {
|
||||||
|
// Reset về trang đầu và gọi lại API
|
||||||
|
isInitialLoad.current = true;
|
||||||
|
setAllTrips([]);
|
||||||
|
setHasMore(true);
|
||||||
|
const resetPayload: Model.TripListBody = {
|
||||||
|
...payloadTrips,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
setPayloadTrips(resetPayload);
|
||||||
|
getTripsList(resetPayload);
|
||||||
|
|
||||||
|
// Scroll FlatList lên đầu
|
||||||
|
setTimeout(() => {
|
||||||
|
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||||
|
}, 100);
|
||||||
|
}, [payloadTrips, getTripsList]);
|
||||||
|
|
||||||
// Dynamic styles based on theme
|
// Dynamic styles based on theme
|
||||||
const themedStyles = {
|
const themedStyles = {
|
||||||
safeArea: {
|
safeArea: {
|
||||||
@@ -304,6 +324,7 @@ export default function diary() {
|
|||||||
|
|
||||||
{/* Trip List with FlatList */}
|
{/* Trip List with FlatList */}
|
||||||
<FlatList
|
<FlatList
|
||||||
|
ref={flatListRef}
|
||||||
data={allTrips}
|
data={allTrips}
|
||||||
renderItem={renderTripItem}
|
renderItem={renderTripItem}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
@@ -331,6 +352,7 @@ export default function diary() {
|
|||||||
<AddTripModal
|
<AddTripModal
|
||||||
visible={showAddTripModal}
|
visible={showAddTripModal}
|
||||||
onClose={() => setShowAddTripModal(false)}
|
onClose={() => setShowAddTripModal(false)}
|
||||||
|
onSuccess={handleTripAddSuccess}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { queryLastTrip } from "@/controller/TripController";
|
|||||||
import { showErrorToast } from "@/services/toast_service";
|
import { showErrorToast } from "@/services/toast_service";
|
||||||
|
|
||||||
interface AutoFillSectionProps {
|
interface AutoFillSectionProps {
|
||||||
onAutoFill: (tripData: Model.Trip, selectedShipId: string) => void;
|
onAutoFill: (tripData: Model.Trip, selectedThingId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) {
|
export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) {
|
||||||
@@ -36,7 +36,7 @@ export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) {
|
|||||||
things
|
things
|
||||||
?.filter((thing) => thing.id != null)
|
?.filter((thing) => thing.id != null)
|
||||||
.map((thing) => ({
|
.map((thing) => ({
|
||||||
id: thing.id as string,
|
thingId: thing.id as string,
|
||||||
shipName: thing.metadata?.ship_name || "",
|
shipName: thing.metadata?.ship_name || "",
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
@@ -46,17 +46,17 @@ export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) {
|
|||||||
return ship.shipName.toLowerCase().includes(searchLower);
|
return ship.shipName.toLowerCase().includes(searchLower);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectShip = async (shipId: string) => {
|
const handleSelectShip = async (thingId: string) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await queryLastTrip(shipId);
|
const response = await queryLastTrip(thingId);
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
// Close the modal first before showing alert
|
// Close the modal first before showing alert
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setSearchText("");
|
setSearchText("");
|
||||||
|
|
||||||
// Pass shipId (thingId) along with trip data for filling ShipSelector
|
// Pass thingId along with trip data for filling ShipSelector
|
||||||
onAutoFill(response.data, shipId);
|
onAutoFill(response.data, thingId);
|
||||||
|
|
||||||
// Use Alert instead of Toast so it appears above all modals
|
// Use Alert instead of Toast so it appears above all modals
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
@@ -182,9 +182,9 @@ export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) {
|
|||||||
{filteredShips.length > 0 ? (
|
{filteredShips.length > 0 ? (
|
||||||
filteredShips.map((ship) => (
|
filteredShips.map((ship) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={ship.id}
|
key={ship.thingId}
|
||||||
style={[styles.option, themedStyles.option]}
|
style={[styles.option, themedStyles.option]}
|
||||||
onPress={() => handleSelectShip(ship.id)}
|
onPress={() => handleSelectShip(ship.thingId)}
|
||||||
>
|
>
|
||||||
<View style={styles.optionContent}>
|
<View style={styles.optionContent}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export default function TripDurationPicker({
|
|||||||
const { colors, colorScheme } = useThemeContext();
|
const { colors, colorScheme } = useThemeContext();
|
||||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||||
|
|
||||||
|
// Temp states to hold the picker value before confirming
|
||||||
|
const [tempStartDate, setTempStartDate] = useState<Date>(new Date());
|
||||||
|
const [tempEndDate, setTempEndDate] = useState<Date>(new Date());
|
||||||
|
|
||||||
const formatDate = (date: Date | null) => {
|
const formatDate = (date: Date | null) => {
|
||||||
if (!date) return "";
|
if (!date) return "";
|
||||||
@@ -38,20 +42,64 @@ export default function TripDurationPicker({
|
|||||||
return `${day}/${month}/${year}`;
|
return `${day}/${month}/${year}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenStartPicker = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const dateToUse = startDate || today;
|
||||||
|
// If no date selected, immediately set to today
|
||||||
|
if (!startDate) {
|
||||||
|
onStartDateChange(today);
|
||||||
|
}
|
||||||
|
// Always set tempStartDate to the date we're using (today if no date was selected)
|
||||||
|
setTempStartDate(dateToUse);
|
||||||
|
setShowStartPicker(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEndPicker = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const dateToUse = endDate || today;
|
||||||
|
// If no date selected, immediately set to today
|
||||||
|
if (!endDate) {
|
||||||
|
onEndDateChange(today);
|
||||||
|
}
|
||||||
|
// Always set tempEndDate to the date we're using (today if no date was selected)
|
||||||
|
setTempEndDate(dateToUse);
|
||||||
|
setShowEndPicker(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleStartDateChange = (event: any, selectedDate?: Date) => {
|
const handleStartDateChange = (event: any, selectedDate?: Date) => {
|
||||||
setShowStartPicker(Platform.OS === "ios");
|
if (Platform.OS === "android") {
|
||||||
if (selectedDate) {
|
setShowStartPicker(false);
|
||||||
|
if (event.type === "set" && selectedDate) {
|
||||||
|
onStartDateChange(selectedDate);
|
||||||
|
}
|
||||||
|
} else if (selectedDate) {
|
||||||
|
// For iOS, update both temp and actual date immediately
|
||||||
|
setTempStartDate(selectedDate);
|
||||||
onStartDateChange(selectedDate);
|
onStartDateChange(selectedDate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEndDateChange = (event: any, selectedDate?: Date) => {
|
const handleEndDateChange = (event: any, selectedDate?: Date) => {
|
||||||
setShowEndPicker(Platform.OS === "ios");
|
if (Platform.OS === "android") {
|
||||||
if (selectedDate) {
|
setShowEndPicker(false);
|
||||||
|
if (event.type === "set" && selectedDate) {
|
||||||
|
onEndDateChange(selectedDate);
|
||||||
|
}
|
||||||
|
} else if (selectedDate) {
|
||||||
|
// For iOS, update both temp and actual date immediately
|
||||||
|
setTempEndDate(selectedDate);
|
||||||
onEndDateChange(selectedDate);
|
onEndDateChange(selectedDate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConfirmStartDate = () => {
|
||||||
|
setShowStartPicker(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmEndDate = () => {
|
||||||
|
setShowEndPicker(false);
|
||||||
|
};
|
||||||
|
|
||||||
const themedStyles = {
|
const themedStyles = {
|
||||||
label: { color: colors.text },
|
label: { color: colors.text },
|
||||||
dateInput: {
|
dateInput: {
|
||||||
@@ -79,7 +127,7 @@ export default function TripDurationPicker({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.dateInput, themedStyles.dateInput]}
|
style={[styles.dateInput, themedStyles.dateInput]}
|
||||||
onPress={() => setShowStartPicker(true)}
|
onPress={handleOpenStartPicker}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@@ -106,7 +154,7 @@ export default function TripDurationPicker({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.dateInput, themedStyles.dateInput]}
|
style={[styles.dateInput, themedStyles.dateInput]}
|
||||||
onPress={() => setShowEndPicker(true)}
|
onPress={handleOpenEndPicker}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@@ -141,12 +189,12 @@ export default function TripDurationPicker({
|
|||||||
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
||||||
{t("diary.selectStartDate")}
|
{t("diary.selectStartDate")}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
<TouchableOpacity onPress={handleConfirmStartDate}>
|
||||||
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={startDate || new Date()}
|
value={tempStartDate}
|
||||||
mode="date"
|
mode="date"
|
||||||
display={Platform.OS === "ios" ? "spinner" : "default"}
|
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
@@ -173,12 +221,12 @@ export default function TripDurationPicker({
|
|||||||
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
||||||
{t("diary.selectEndDate")}
|
{t("diary.selectEndDate")}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
<TouchableOpacity onPress={handleConfirmEndDate}>
|
||||||
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={endDate || new Date()}
|
value={tempEndDate}
|
||||||
mode="date"
|
mode="date"
|
||||||
display={Platform.OS === "ios" ? "spinner" : "default"}
|
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
@@ -19,52 +21,24 @@ import PortSelector from "@/components/diary/addTripModal/PortSelector";
|
|||||||
import BasicInfoInput from "@/components/diary/addTripModal/BasicInfoInput";
|
import BasicInfoInput from "@/components/diary/addTripModal/BasicInfoInput";
|
||||||
import ShipSelector from "./ShipSelector";
|
import ShipSelector from "./ShipSelector";
|
||||||
import AutoFillSection from "./AutoFillSection";
|
import AutoFillSection from "./AutoFillSection";
|
||||||
|
import { createTrip } from "@/controller/TripController";
|
||||||
|
|
||||||
|
// Internal component interfaces - extend from Model with local id for state management
|
||||||
// Internal component interfaces
|
export interface FishingGear extends Model.FishingGear {
|
||||||
export interface FishingGear {
|
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
|
||||||
number: string; // Changed from quantity to number (string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TripCost {
|
export interface TripCost extends Model.TripCost {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
|
||||||
amount: number;
|
|
||||||
unit: string;
|
|
||||||
cost_per_unit: number;
|
|
||||||
total_cost: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API body interface
|
|
||||||
export interface TripAPIBody {
|
|
||||||
thing_id?: string; // Ship ID
|
|
||||||
name: string;
|
|
||||||
departure_time: string; // ISO string
|
|
||||||
departure_port_id: number;
|
|
||||||
arrival_time: string; // ISO string
|
|
||||||
arrival_port_id: number;
|
|
||||||
fishing_ground_codes: number[]; // Array of numbers
|
|
||||||
fishing_gears: Array<{
|
|
||||||
name: string;
|
|
||||||
number: string;
|
|
||||||
}>;
|
|
||||||
trip_cost: Array<{
|
|
||||||
type: string;
|
|
||||||
amount: number;
|
|
||||||
unit: string;
|
|
||||||
cost_per_unit: number;
|
|
||||||
total_cost: number;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddTripModalProps {
|
interface AddTripModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void; // Callback khi thêm chuyến đi thành công
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
export default function AddTripModal({ visible, onClose, onSuccess }: AddTripModalProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
|
|
||||||
@@ -78,6 +52,7 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
|||||||
const [departurePortId, setDeparturePortId] = useState<number>(1);
|
const [departurePortId, setDeparturePortId] = useState<number>(1);
|
||||||
const [arrivalPortId, setArrivalPortId] = useState<number>(1);
|
const [arrivalPortId, setArrivalPortId] = useState<number>(1);
|
||||||
const [fishingGroundCodes, setFishingGroundCodes] = useState<string>(""); // Input as string, convert to array
|
const [fishingGroundCodes, setFishingGroundCodes] = useState<string>(""); // Input as string, convert to array
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
// Reset form
|
// Reset form
|
||||||
@@ -99,17 +74,19 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
|||||||
setSelectedShipId(selectedThingId);
|
setSelectedShipId(selectedThingId);
|
||||||
|
|
||||||
// Fill trip name
|
// Fill trip name
|
||||||
if (tripData.name) {
|
// if (tripData.name) {
|
||||||
setTripName(tripData.name);
|
// setTripName(tripData.name);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Fill fishing gears
|
// Fill fishing gears
|
||||||
if (tripData.fishing_gears && Array.isArray(tripData.fishing_gears)) {
|
if (tripData.fishing_gears && Array.isArray(tripData.fishing_gears)) {
|
||||||
const gears: FishingGear[] = tripData.fishing_gears.map((gear, index) => ({
|
const gears: FishingGear[] = tripData.fishing_gears.map(
|
||||||
id: `auto-${Date.now()}-${index}`,
|
(gear, index) => ({
|
||||||
name: gear.name || "",
|
id: `auto-${Date.now()}-${index}`,
|
||||||
number: gear.number?.toString() || "",
|
name: gear.name || "",
|
||||||
}));
|
number: gear.number?.toString() || "",
|
||||||
|
})
|
||||||
|
);
|
||||||
setFishingGears(gears);
|
setFishingGears(gears);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +112,33 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fill fishing ground codes
|
// Fill fishing ground codes
|
||||||
if (tripData.fishing_ground_codes && Array.isArray(tripData.fishing_ground_codes)) {
|
if (
|
||||||
|
tripData.fishing_ground_codes &&
|
||||||
|
Array.isArray(tripData.fishing_ground_codes)
|
||||||
|
) {
|
||||||
setFishingGroundCodes(tripData.fishing_ground_codes.join(", "));
|
setFishingGroundCodes(tripData.fishing_ground_codes.join(", "));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
|
// Validate thingId is required
|
||||||
|
if (!selectedShipId) {
|
||||||
|
Alert.alert(t("common.error"), t("diary.validation.shipRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates are required
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
Alert.alert(t("common.error"), t("diary.validation.datesRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate trip name is required
|
||||||
|
if (!tripName.trim()) {
|
||||||
|
Alert.alert(t("common.error"), t("diary.validation.tripNameRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse fishing ground codes from comma-separated string to array of numbers
|
// Parse fishing ground codes from comma-separated string to array of numbers
|
||||||
const fishingGroundCodesArray = fishingGroundCodes
|
const fishingGroundCodesArray = fishingGroundCodes
|
||||||
.split(",")
|
.split(",")
|
||||||
@@ -148,8 +146,8 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
|||||||
.filter((code) => !isNaN(code));
|
.filter((code) => !isNaN(code));
|
||||||
|
|
||||||
// Format API body
|
// Format API body
|
||||||
const apiBody: TripAPIBody = {
|
const apiBody: Model.TripAPIBody = {
|
||||||
thing_id: selectedShipId || undefined,
|
thing_id: selectedShipId,
|
||||||
name: tripName,
|
name: tripName,
|
||||||
departure_time: startDate ? startDate.toISOString() : "",
|
departure_time: startDate ? startDate.toISOString() : "",
|
||||||
departure_port_id: departurePortId,
|
departure_port_id: departurePortId,
|
||||||
@@ -169,13 +167,37 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Simulate API call - log the formatted data
|
setIsSubmitting(true);
|
||||||
console.log("=== Submitting Trip Data (API Format) ===");
|
try {
|
||||||
console.log(JSON.stringify(apiBody, null, 2));
|
const response = await createTrip(selectedShipId, apiBody);
|
||||||
console.log("=== End Trip Data ===");
|
|
||||||
|
if (response.data) {
|
||||||
// Reset form and close modal
|
// Show success alert
|
||||||
handleCancel();
|
Alert.alert(
|
||||||
|
t("common.success"),
|
||||||
|
t("diary.createTripSuccess")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call onSuccess callback
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form and close modal
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error creating trip:", error);
|
||||||
|
// Log detailed error information for debugging
|
||||||
|
if (error.response) {
|
||||||
|
console.error("Response status:", error.response.status);
|
||||||
|
console.error("Response data:", JSON.stringify(error.response.data, null, 2));
|
||||||
|
}
|
||||||
|
console.log("Request body was:", JSON.stringify(apiBody, null, 2));
|
||||||
|
Alert.alert(t("common.error"), t("diary.createTripError"));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const themedStyles = {
|
const themedStyles = {
|
||||||
@@ -240,16 +262,10 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
|||||||
<TripNameInput value={tripName} onChange={setTripName} />
|
<TripNameInput value={tripName} onChange={setTripName} />
|
||||||
|
|
||||||
{/* Fishing Gear List */}
|
{/* Fishing Gear List */}
|
||||||
<FishingGearList
|
<FishingGearList items={fishingGears} onChange={setFishingGears} />
|
||||||
items={fishingGears}
|
|
||||||
onChange={setFishingGears}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Trip Cost List */}
|
{/* Trip Cost List */}
|
||||||
<MaterialCostList
|
<MaterialCostList items={tripCosts} onChange={setTripCosts} />
|
||||||
items={tripCosts}
|
|
||||||
onChange={setTripCosts}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Trip Duration */}
|
{/* Trip Duration */}
|
||||||
<TripDurationPicker
|
<TripDurationPicker
|
||||||
@@ -281,18 +297,29 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
|||||||
onPress={handleCancel}
|
onPress={handleCancel}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={[styles.cancelButtonText, themedStyles.cancelButtonText]}>
|
<Text
|
||||||
|
style={[styles.cancelButtonText, themedStyles.cancelButtonText]}
|
||||||
|
>
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.submitButton, themedStyles.submitButton]}
|
style={[
|
||||||
|
styles.submitButton,
|
||||||
|
themedStyles.submitButton,
|
||||||
|
isSubmitting && styles.submitButtonDisabled
|
||||||
|
]}
|
||||||
onPress={handleSubmit}
|
onPress={handleSubmit}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<Text style={styles.submitButtonText}>
|
{isSubmitting ? (
|
||||||
{t("diary.createTrip")}
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||||
</Text>
|
) : (
|
||||||
|
<Text style={styles.submitButtonText}>
|
||||||
|
{t("diary.createTrip")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -373,6 +400,9 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
submitButtonDisabled: {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
submitButtonText: {
|
submitButtonText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const API_GET_ALL_BANZONES = "/api/sgw/banzones";
|
|||||||
export const API_GET_SHIP_TYPES = "/api/sgw/ships/types";
|
export const API_GET_SHIP_TYPES = "/api/sgw/ships/types";
|
||||||
export const API_GET_SHIP_GROUPS = "/api/sgw/shipsgroup";
|
export const API_GET_SHIP_GROUPS = "/api/sgw/shipsgroup";
|
||||||
export const API_GET_LAST_TRIP = "/api/sgw/trips/last";
|
export const API_GET_LAST_TRIP = "/api/sgw/trips/last";
|
||||||
|
export const API_POST_TRIP = "/api/sgw/trips";
|
||||||
export const API_GET_ALARM = "/api/alarms";
|
export const API_GET_ALARM = "/api/alarms";
|
||||||
export const API_MANAGER_ALARM = "/api/alarms/confirm";
|
export const API_MANAGER_ALARM = "/api/alarms/confirm";
|
||||||
export const API_GET_ALL_SHIP = "/api/sgw/ships";
|
export const API_GET_ALL_SHIP = "/api/sgw/ships";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
API_UPDATE_FISHING_LOGS,
|
API_UPDATE_FISHING_LOGS,
|
||||||
API_UPDATE_TRIP_STATUS,
|
API_UPDATE_TRIP_STATUS,
|
||||||
API_GET_LAST_TRIP,
|
API_GET_LAST_TRIP,
|
||||||
|
API_POST_TRIP,
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
|
|
||||||
export async function queryTrip() {
|
export async function queryTrip() {
|
||||||
@@ -31,3 +32,7 @@ export async function queryUpdateFishingLogs(body: Model.FishingLog) {
|
|||||||
export async function queryTripsList(body: Model.TripListBody) {
|
export async function queryTripsList(body: Model.TripListBody) {
|
||||||
return api.post(API_POST_TRIPSLIST, body);
|
return api.post(API_POST_TRIPSLIST, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createTrip(thingId: string, body: Model.TripAPIBody) {
|
||||||
|
return api.post<Model.Trip>(`${API_POST_TRIP}/${thingId}`, body);
|
||||||
|
}
|
||||||
|
|||||||
22
controller/typings.d.ts
vendored
22
controller/typings.d.ts
vendored
@@ -200,6 +200,28 @@ declare namespace Model {
|
|||||||
status: number;
|
status: number;
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API body interface for creating a new trip
|
||||||
|
interface TripAPIBody {
|
||||||
|
thing_id?: string;
|
||||||
|
name: string;
|
||||||
|
departure_time: string; // ISO string
|
||||||
|
departure_port_id: number;
|
||||||
|
arrival_time: string; // ISO string
|
||||||
|
arrival_port_id: number;
|
||||||
|
fishing_ground_codes: number[];
|
||||||
|
fishing_gears: Array<{
|
||||||
|
name: string;
|
||||||
|
number: string;
|
||||||
|
}>;
|
||||||
|
trip_cost: Array<{
|
||||||
|
type: string;
|
||||||
|
amount: number;
|
||||||
|
unit: string;
|
||||||
|
cost_per_unit: number;
|
||||||
|
total_cost: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
//Fish
|
//Fish
|
||||||
interface FishSpeciesResponse {
|
interface FishSpeciesResponse {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -165,7 +165,14 @@
|
|||||||
"success": "Data filled from last trip",
|
"success": "Data filled from last trip",
|
||||||
"error": "Unable to fetch trip data",
|
"error": "Unable to fetch trip data",
|
||||||
"noData": "No previous trip data available"
|
"noData": "No previous trip data available"
|
||||||
}
|
},
|
||||||
|
"validation": {
|
||||||
|
"shipRequired": "Please select a ship before creating the trip",
|
||||||
|
"datesRequired": "Please select departure and arrival dates",
|
||||||
|
"tripNameRequired": "Please enter a trip name"
|
||||||
|
},
|
||||||
|
"createTripSuccess": "Trip created successfully!",
|
||||||
|
"createTripError": "Unable to create trip. Please try again."
|
||||||
},
|
},
|
||||||
"trip": {
|
"trip": {
|
||||||
"infoTrip": "Trip Information",
|
"infoTrip": "Trip Information",
|
||||||
|
|||||||
@@ -165,7 +165,14 @@
|
|||||||
"success": "Đã điền dữ liệu từ chuyến đi cuối cùng",
|
"success": "Đã điền dữ liệu từ chuyến đi cuối cùng",
|
||||||
"error": "Không thể lấy dữ liệu chuyến đi",
|
"error": "Không thể lấy dữ liệu chuyến đi",
|
||||||
"noData": "Không có dữ liệu chuyến đi trước đó"
|
"noData": "Không có dữ liệu chuyến đi trước đó"
|
||||||
}
|
},
|
||||||
|
"validation": {
|
||||||
|
"shipRequired": "Vui lòng chọn tàu trước khi tạo chuyến đi",
|
||||||
|
"datesRequired": "Vui lòng chọn ngày khởi hành và ngày kết thúc",
|
||||||
|
"tripNameRequired": "Vui lòng nhập tên chuyến đi"
|
||||||
|
},
|
||||||
|
"createTripSuccess": "Tạo chuyến đi thành công!",
|
||||||
|
"createTripError": "Không thể tạo chuyến đi. Vui lòng thử lại."
|
||||||
},
|
},
|
||||||
"trip": {
|
"trip": {
|
||||||
"infoTrip": "Thông Tin Chuyến Đi",
|
"infoTrip": "Thông Tin Chuyến Đi",
|
||||||
|
|||||||
Reference in New Issue
Block a user