Cập nhật API thêm trip (validate)

This commit is contained in:
2025-12-22 15:22:06 +07:00
parent 12fb7c48ed
commit 67e9fc22a3
9 changed files with 228 additions and 86 deletions

View File

@@ -7,6 +7,8 @@ import {
StyleSheet,
Platform,
ScrollView,
Alert,
ActivityIndicator,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
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 ShipSelector from "./ShipSelector";
import AutoFillSection from "./AutoFillSection";
import { createTrip } from "@/controller/TripController";
// Internal component interfaces
export interface FishingGear {
// Internal component interfaces - extend from Model with local id for state management
export interface FishingGear extends Model.FishingGear {
id: string;
name: string;
number: string; // Changed from quantity to number (string)
}
export interface TripCost {
export interface TripCost extends Model.TripCost {
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 {
visible: boolean;
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 { colors } = useThemeContext();
@@ -78,6 +52,7 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
const [departurePortId, setDeparturePortId] = useState<number>(1);
const [arrivalPortId, setArrivalPortId] = useState<number>(1);
const [fishingGroundCodes, setFishingGroundCodes] = useState<string>(""); // Input as string, convert to array
const [isSubmitting, setIsSubmitting] = useState(false);
const handleCancel = () => {
// Reset form
@@ -99,17 +74,19 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
setSelectedShipId(selectedThingId);
// Fill trip name
if (tripData.name) {
setTripName(tripData.name);
}
// if (tripData.name) {
// setTripName(tripData.name);
// }
// Fill fishing gears
if (tripData.fishing_gears && Array.isArray(tripData.fishing_gears)) {
const gears: FishingGear[] = tripData.fishing_gears.map((gear, index) => ({
id: `auto-${Date.now()}-${index}`,
name: gear.name || "",
number: gear.number?.toString() || "",
}));
const gears: FishingGear[] = tripData.fishing_gears.map(
(gear, index) => ({
id: `auto-${Date.now()}-${index}`,
name: gear.name || "",
number: gear.number?.toString() || "",
})
);
setFishingGears(gears);
}
@@ -135,12 +112,33 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
}
// 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(", "));
}
};
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
const fishingGroundCodesArray = fishingGroundCodes
.split(",")
@@ -148,8 +146,8 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
.filter((code) => !isNaN(code));
// Format API body
const apiBody: TripAPIBody = {
thing_id: selectedShipId || undefined,
const apiBody: Model.TripAPIBody = {
thing_id: selectedShipId,
name: tripName,
departure_time: startDate ? startDate.toISOString() : "",
departure_port_id: departurePortId,
@@ -169,13 +167,37 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
})),
};
// Simulate API call - log the formatted data
console.log("=== Submitting Trip Data (API Format) ===");
console.log(JSON.stringify(apiBody, null, 2));
console.log("=== End Trip Data ===");
// Reset form and close modal
handleCancel();
setIsSubmitting(true);
try {
const response = await createTrip(selectedShipId, apiBody);
if (response.data) {
// Show success alert
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 = {
@@ -240,16 +262,10 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
<TripNameInput value={tripName} onChange={setTripName} />
{/* Fishing Gear List */}
<FishingGearList
items={fishingGears}
onChange={setFishingGears}
/>
<FishingGearList items={fishingGears} onChange={setFishingGears} />
{/* Trip Cost List */}
<MaterialCostList
items={tripCosts}
onChange={setTripCosts}
/>
<MaterialCostList items={tripCosts} onChange={setTripCosts} />
{/* Trip Duration */}
<TripDurationPicker
@@ -281,18 +297,29 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
onPress={handleCancel}
activeOpacity={0.7}
>
<Text style={[styles.cancelButtonText, themedStyles.cancelButtonText]}>
<Text
style={[styles.cancelButtonText, themedStyles.cancelButtonText]}
>
{t("common.cancel")}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.submitButton, themedStyles.submitButton]}
style={[
styles.submitButton,
themedStyles.submitButton,
isSubmitting && styles.submitButtonDisabled
]}
onPress={handleSubmit}
activeOpacity={0.7}
disabled={isSubmitting}
>
<Text style={styles.submitButtonText}>
{t("diary.createTrip")}
</Text>
{isSubmitting ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<Text style={styles.submitButtonText}>
{t("diary.createTrip")}
</Text>
)}
</TouchableOpacity>
</View>
</View>
@@ -373,6 +400,9 @@ const styles = StyleSheet.create({
borderRadius: 12,
alignItems: "center",
},
submitButtonDisabled: {
opacity: 0.7,
},
submitButtonText: {
fontSize: 16,
fontWeight: "600",