cập nhật animation hiển thị modal, call API edit
This commit is contained in:
@@ -13,7 +13,7 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import FilterButton from "@/components/diary/FilterButton";
|
import FilterButton from "@/components/diary/FilterButton";
|
||||||
import TripCard from "@/components/diary/TripCard";
|
import TripCard from "@/components/diary/TripCard";
|
||||||
import FilterModal, { FilterValues } from "@/components/diary/FilterModal";
|
import FilterModal, { FilterValues } from "@/components/diary/FilterModal";
|
||||||
import AddTripModal from "@/components/diary/addTripModal";
|
import AddTripModal from "@/components/diary/TripFormModal";
|
||||||
import { useThings } from "@/state/use-thing";
|
import { useThings } from "@/state/use-thing";
|
||||||
import { useTripsList } from "@/state/use-tripslist";
|
import { useTripsList } from "@/state/use-tripslist";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@@ -25,6 +25,8 @@ export default function diary() {
|
|||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||||
const [showAddTripModal, setShowAddTripModal] = useState(false);
|
const [showAddTripModal, setShowAddTripModal] = useState(false);
|
||||||
|
const [editingTrip, setEditingTrip] = useState<Model.Trip | null>(null);
|
||||||
|
const [viewingTrip, setViewingTrip] = useState<Model.Trip | null>(null);
|
||||||
const [filters, setFilters] = useState<FilterValues>({
|
const [filters, setFilters] = useState<FilterValues>({
|
||||||
status: null,
|
status: null,
|
||||||
startDate: null,
|
startDate: null,
|
||||||
@@ -169,19 +171,27 @@ export default function diary() {
|
|||||||
getTripsList(updatedPayload);
|
getTripsList(updatedPayload);
|
||||||
}, [isLoadingMore, hasMore, payloadTrips]);
|
}, [isLoadingMore, hasMore, payloadTrips]);
|
||||||
|
|
||||||
const handleTripPress = (tripId: string) => {
|
// const handleTripPress = (tripId: string) => {
|
||||||
// TODO: Navigate to trip detail
|
// // TODO: Navigate to trip detail
|
||||||
console.log("Trip pressed:", tripId);
|
// console.log("Trip pressed:", tripId);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleViewTrip = (tripId: string) => {
|
const handleViewTrip = (tripId: string) => {
|
||||||
console.log("View trip:", tripId);
|
// Find the trip from allTrips and open modal in view mode
|
||||||
// TODO: Navigate to trip detail view
|
const tripToView = allTrips.find((trip) => trip.id === tripId);
|
||||||
|
if (tripToView) {
|
||||||
|
setViewingTrip(tripToView);
|
||||||
|
setShowAddTripModal(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditTrip = (tripId: string) => {
|
const handleEditTrip = (tripId: string) => {
|
||||||
console.log("Edit trip:", tripId);
|
// Find the trip from allTrips
|
||||||
// TODO: Navigate to trip edit screen
|
const tripToEdit = allTrips.find((trip) => trip.id === tripId);
|
||||||
|
if (tripToEdit) {
|
||||||
|
setEditingTrip(tripToEdit);
|
||||||
|
setShowAddTripModal(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewTeam = (tripId: string) => {
|
const handleViewTeam = (tripId: string) => {
|
||||||
@@ -242,7 +252,7 @@ export default function diary() {
|
|||||||
({ item }: { item: any }) => (
|
({ item }: { item: any }) => (
|
||||||
<TripCard
|
<TripCard
|
||||||
trip={item}
|
trip={item}
|
||||||
onPress={() => handleTripPress(item.id)}
|
// onPress={() => handleTripPress(item.id)}
|
||||||
onView={() => handleViewTrip(item.id)}
|
onView={() => handleViewTrip(item.id)}
|
||||||
onEdit={() => handleEditTrip(item.id)}
|
onEdit={() => handleEditTrip(item.id)}
|
||||||
onTeam={() => handleViewTeam(item.id)}
|
onTeam={() => handleViewTeam(item.id)}
|
||||||
@@ -250,7 +260,7 @@ export default function diary() {
|
|||||||
onDelete={() => handleDeleteTrip(item.id)}
|
onDelete={() => handleDeleteTrip(item.id)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[]
|
[handleViewTrip, handleEditTrip, handleViewTeam, handleSendTrip, handleDeleteTrip]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Key extractor cho FlatList
|
// Key extractor cho FlatList
|
||||||
@@ -348,11 +358,17 @@ export default function diary() {
|
|||||||
onApply={handleApplyFilters}
|
onApply={handleApplyFilters}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Add Trip Modal */}
|
{/* Add/Edit/View Trip Modal */}
|
||||||
<AddTripModal
|
<AddTripModal
|
||||||
visible={showAddTripModal}
|
visible={showAddTripModal}
|
||||||
onClose={() => setShowAddTripModal(false)}
|
onClose={() => {
|
||||||
|
setShowAddTripModal(false);
|
||||||
|
setEditingTrip(null);
|
||||||
|
setViewingTrip(null);
|
||||||
|
}}
|
||||||
onSuccess={handleTripAddSuccess}
|
onSuccess={handleTripAddSuccess}
|
||||||
|
mode={viewingTrip ? 'view' : editingTrip ? 'edit' : 'add'}
|
||||||
|
tripData={viewingTrip || editingTrip || undefined}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
Animated,
|
||||||
|
Dimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import StatusDropdown from "./StatusDropdown";
|
import StatusDropdown from "./StatusDropdown";
|
||||||
@@ -71,6 +73,47 @@ export default function FilterModal({
|
|||||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||||
const [selectedShip, setSelectedShip] = useState<ShipOption | null>(null);
|
const [selectedShip, setSelectedShip] = useState<ShipOption | null>(null);
|
||||||
|
|
||||||
|
// Animation values
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
const slideAnim = useRef(new Animated.Value(Dimensions.get('window').height)).current;
|
||||||
|
|
||||||
|
// Handle animation when modal visibility changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
// Open animation: fade overlay + slide content up
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(slideAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start();
|
||||||
|
}
|
||||||
|
}, [visible, fadeAnim, slideAnim]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// Close animation: fade overlay + slide content down
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(slideAnim, {
|
||||||
|
toValue: Dimensions.get('window').height,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start(() => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setStatus(null);
|
setStatus(null);
|
||||||
setStartDate(null);
|
setStartDate(null);
|
||||||
@@ -79,8 +122,22 @@ export default function FilterModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
|
// Close animation then apply
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(slideAnim, {
|
||||||
|
toValue: Dimensions.get('window').height,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start(() => {
|
||||||
onApply({ status, startDate, endDate, selectedShip });
|
onApply({ status, startDate, endDate, selectedShip });
|
||||||
onClose();
|
onClose();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFilters =
|
const hasFilters =
|
||||||
@@ -128,23 +185,26 @@ export default function FilterModal({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
animationType="fade"
|
animationType="none"
|
||||||
transparent
|
transparent
|
||||||
onRequestClose={onClose}
|
onRequestClose={handleClose}
|
||||||
>
|
>
|
||||||
|
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.overlay}
|
style={styles.overlayTouchable}
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
onPress={onClose}
|
onPress={handleClose}
|
||||||
>
|
/>
|
||||||
<TouchableOpacity
|
<Animated.View
|
||||||
style={[styles.modalContainer, themedStyles.modalContainer]}
|
style={[
|
||||||
activeOpacity={1}
|
styles.modalContainer,
|
||||||
onPress={(e) => e.stopPropagation()}
|
themedStyles.modalContainer,
|
||||||
|
{ transform: [{ translateY: slideAnim }] }
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={[styles.header, themedStyles.header]}>
|
<View style={[styles.header, themedStyles.header]}>
|
||||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
|
||||||
<Ionicons name="close" size={24} color={colors.text} />
|
<Ionicons name="close" size={24} color={colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={[styles.title, themedStyles.title]}>{t("diary.filter")}</Text>
|
<Text style={[styles.title, themedStyles.title]}>{t("diary.filter")}</Text>
|
||||||
@@ -218,8 +278,8 @@ export default function FilterModal({
|
|||||||
<Text style={styles.applyButtonText}>{t("diary.apply")}</Text>
|
<Text style={styles.applyButtonText}>{t("diary.apply")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</Animated.View>
|
||||||
</TouchableOpacity>
|
</Animated.View>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -231,6 +291,13 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
},
|
},
|
||||||
|
overlayTouchable: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
modalContainer: {
|
modalContainer: {
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default function TripCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.card, themedStyles.card]}>
|
<View style={[styles.card, themedStyles.card]}>
|
||||||
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
<View>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerLeft}>
|
<View style={styles.headerLeft}>
|
||||||
@@ -142,7 +142,7 @@ export default function TripCard({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={[styles.divider, themedStyles.divider]} />
|
<View style={[styles.divider, themedStyles.divider]} />
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import { useThemeContext } from "@/hooks/use-theme-context";
|
|||||||
interface BasicInfoInputProps {
|
interface BasicInfoInputProps {
|
||||||
fishingGroundCodes: string;
|
fishingGroundCodes: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BasicInfoInput({
|
export default function BasicInfoInput({
|
||||||
fishingGroundCodes,
|
fishingGroundCodes,
|
||||||
onChange,
|
onChange,
|
||||||
|
disabled = false,
|
||||||
}: BasicInfoInputProps) {
|
}: BasicInfoInputProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
@@ -25,7 +27,7 @@ export default function BasicInfoInput({
|
|||||||
label: { color: colors.text },
|
label: { color: colors.text },
|
||||||
subLabel: { color: colors.textSecondary },
|
subLabel: { color: colors.textSecondary },
|
||||||
input: {
|
input: {
|
||||||
backgroundColor: colors.card,
|
backgroundColor: disabled ? colors.backgroundSecondary : colors.card,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
@@ -46,6 +48,7 @@ export default function BasicInfoInput({
|
|||||||
placeholder={t("diary.fishingGroundCodesPlaceholder")}
|
placeholder={t("diary.fishingGroundCodesPlaceholder")}
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
|
editable={!disabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -20,11 +20,13 @@ interface FishingGear {
|
|||||||
interface FishingGearListProps {
|
interface FishingGearListProps {
|
||||||
items: FishingGear[];
|
items: FishingGear[];
|
||||||
onChange: (items: FishingGear[]) => void;
|
onChange: (items: FishingGear[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FishingGearList({
|
export default function FishingGearList({
|
||||||
items,
|
items,
|
||||||
onChange,
|
onChange,
|
||||||
|
disabled = false,
|
||||||
}: FishingGearListProps) {
|
}: FishingGearListProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
@@ -93,6 +95,7 @@ export default function FishingGearList({
|
|||||||
onChangeText={(value) => handleUpdateGear(gear.id, "name", value)}
|
onChangeText={(value) => handleUpdateGear(gear.id, "name", value)}
|
||||||
placeholder={t("diary.gearNamePlaceholder")}
|
placeholder={t("diary.gearNamePlaceholder")}
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
editable={!disabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -108,10 +111,12 @@ export default function FishingGearList({
|
|||||||
placeholder={t("diary.gearNumberPlaceholder")}
|
placeholder={t("diary.gearNumberPlaceholder")}
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
|
editable={!disabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons - hide when disabled */}
|
||||||
|
{!disabled && (
|
||||||
<View style={styles.actionButtons}>
|
<View style={styles.actionButtons}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleDuplicateGear(gear)}
|
onPress={() => handleDuplicateGear(gear)}
|
||||||
@@ -126,10 +131,12 @@ export default function FishingGearList({
|
|||||||
<Ionicons name="trash-outline" size={20} color={colors.error} />
|
<Ionicons name="trash-outline" size={20} color={colors.error} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Add Button */}
|
{/* Add Button - hide when disabled */}
|
||||||
|
{!disabled && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.addButton, themedStyles.addButton]}
|
style={[styles.addButton, themedStyles.addButton]}
|
||||||
onPress={handleAddGear}
|
onPress={handleAddGear}
|
||||||
@@ -140,6 +147,7 @@ export default function FishingGearList({
|
|||||||
{t("diary.addFishingGear")}
|
{t("diary.addFishingGear")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ interface TripCost {
|
|||||||
interface MaterialCostListProps {
|
interface MaterialCostListProps {
|
||||||
items: TripCost[];
|
items: TripCost[];
|
||||||
onChange: (items: TripCost[]) => void;
|
onChange: (items: TripCost[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Predefined cost types
|
// Predefined cost types
|
||||||
@@ -39,6 +40,7 @@ const COST_TYPES = [
|
|||||||
export default function MaterialCostList({
|
export default function MaterialCostList({
|
||||||
items,
|
items,
|
||||||
onChange,
|
onChange,
|
||||||
|
disabled = false,
|
||||||
}: MaterialCostListProps) {
|
}: MaterialCostListProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
@@ -146,8 +148,9 @@ export default function MaterialCostList({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.dropdown, themedStyles.dropdown]}
|
style={[styles.dropdown, themedStyles.dropdown]}
|
||||||
onPress={() => setTypeDropdownVisible(cost.id)}
|
onPress={disabled ? undefined : () => setTypeDropdownVisible(cost.id)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={disabled ? 1 : 0.7}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -158,11 +161,13 @@ export default function MaterialCostList({
|
|||||||
>
|
>
|
||||||
{getTypeLabel(cost.type)}
|
{getTypeLabel(cost.type)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!disabled && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-down"
|
name="chevron-down"
|
||||||
size={16}
|
size={16}
|
||||||
color={colors.textSecondary}
|
color={colors.textSecondary}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Type Dropdown Modal */}
|
{/* Type Dropdown Modal */}
|
||||||
@@ -222,6 +227,7 @@ export default function MaterialCostList({
|
|||||||
placeholder="0"
|
placeholder="0"
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
|
editable={!disabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -238,6 +244,7 @@ export default function MaterialCostList({
|
|||||||
}
|
}
|
||||||
placeholder={t("diary.unitPlaceholder")}
|
placeholder={t("diary.unitPlaceholder")}
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
editable={!disabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -261,6 +268,7 @@ export default function MaterialCostList({
|
|||||||
placeholder="0"
|
placeholder="0"
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
|
editable={!disabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -276,7 +284,8 @@ export default function MaterialCostList({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons - hide when disabled */}
|
||||||
|
{!disabled && (
|
||||||
<View style={styles.actionButtons}>
|
<View style={styles.actionButtons}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleDuplicateMaterial(cost)}
|
onPress={() => handleDuplicateMaterial(cost)}
|
||||||
@@ -295,11 +304,13 @@ export default function MaterialCostList({
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Add Button */}
|
{/* Add Button - hide when disabled */}
|
||||||
|
{!disabled && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.addButton, themedStyles.addButton]}
|
style={[styles.addButton, themedStyles.addButton]}
|
||||||
onPress={handleAddMaterial}
|
onPress={handleAddMaterial}
|
||||||
@@ -310,6 +321,7 @@ export default function MaterialCostList({
|
|||||||
{t("diary.addMaterialCost")}
|
{t("diary.addMaterialCost")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ interface PortSelectorProps {
|
|||||||
arrivalPortId: number;
|
arrivalPortId: number;
|
||||||
onDeparturePortChange: (portId: number) => void;
|
onDeparturePortChange: (portId: number) => void;
|
||||||
onArrivalPortChange: (portId: number) => void;
|
onArrivalPortChange: (portId: number) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PortSelector({
|
export default function PortSelector({
|
||||||
@@ -22,6 +23,7 @@ export default function PortSelector({
|
|||||||
arrivalPortId,
|
arrivalPortId,
|
||||||
onDeparturePortChange,
|
onDeparturePortChange,
|
||||||
onArrivalPortChange,
|
onArrivalPortChange,
|
||||||
|
disabled = false,
|
||||||
}: PortSelectorProps) {
|
}: PortSelectorProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
@@ -69,8 +71,9 @@ export default function PortSelector({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.portSelector, themedStyles.portSelector]}
|
style={[styles.portSelector, themedStyles.portSelector]}
|
||||||
onPress={handleSelectDeparturePort}
|
onPress={disabled ? undefined : handleSelectDeparturePort}
|
||||||
activeOpacity={0.7}
|
activeOpacity={disabled ? 1 : 0.7}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -81,11 +84,13 @@ export default function PortSelector({
|
|||||||
>
|
>
|
||||||
{getPortDisplayName(departurePortId)}
|
{getPortDisplayName(departurePortId)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!disabled && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="ellipsis-horizontal"
|
name="ellipsis-horizontal"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.textSecondary}
|
color={colors.textSecondary}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -96,8 +101,9 @@ export default function PortSelector({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.portSelector, themedStyles.portSelector]}
|
style={[styles.portSelector, themedStyles.portSelector]}
|
||||||
onPress={handleSelectArrivalPort}
|
onPress={disabled ? undefined : handleSelectArrivalPort}
|
||||||
activeOpacity={0.7}
|
activeOpacity={disabled ? 1 : 0.7}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -108,11 +114,13 @@ export default function PortSelector({
|
|||||||
>
|
>
|
||||||
{getPortDisplayName(arrivalPortId)}
|
{getPortDisplayName(arrivalPortId)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!disabled && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="ellipsis-horizontal"
|
name="ellipsis-horizontal"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.textSecondary}
|
color={colors.textSecondary}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -17,11 +17,13 @@ import { useThemeContext } from "@/hooks/use-theme-context";
|
|||||||
interface ShipSelectorProps {
|
interface ShipSelectorProps {
|
||||||
selectedShipId: string;
|
selectedShipId: string;
|
||||||
onChange: (shipId: string) => void;
|
onChange: (shipId: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ShipSelector({
|
export default function ShipSelector({
|
||||||
selectedShipId,
|
selectedShipId,
|
||||||
onChange,
|
onChange,
|
||||||
|
disabled = false,
|
||||||
}: ShipSelectorProps) {
|
}: ShipSelectorProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
@@ -58,7 +60,10 @@ export default function ShipSelector({
|
|||||||
|
|
||||||
const themedStyles = {
|
const themedStyles = {
|
||||||
label: { color: colors.text },
|
label: { color: colors.text },
|
||||||
selector: { backgroundColor: colors.card, borderColor: colors.border },
|
selector: {
|
||||||
|
backgroundColor: disabled ? colors.backgroundSecondary : colors.card,
|
||||||
|
borderColor: colors.border,
|
||||||
|
},
|
||||||
selectorText: { color: colors.text },
|
selectorText: { color: colors.text },
|
||||||
placeholder: { color: colors.textSecondary },
|
placeholder: { color: colors.textSecondary },
|
||||||
modalContent: { backgroundColor: colors.card },
|
modalContent: { backgroundColor: colors.card },
|
||||||
@@ -81,8 +86,9 @@ export default function ShipSelector({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.selector, themedStyles.selector]}
|
style={[styles.selector, themedStyles.selector]}
|
||||||
onPress={() => setIsOpen(true)}
|
onPress={() => !disabled && setIsOpen(true)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={disabled ? 1 : 0.7}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -93,11 +99,13 @@ export default function ShipSelector({
|
|||||||
>
|
>
|
||||||
{displayValue}
|
{displayValue}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!disabled && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="ellipsis-horizontal"
|
name="ellipsis-horizontal"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.textSecondary}
|
color={colors.textSecondary}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@@ -17,6 +17,7 @@ interface TripDurationPickerProps {
|
|||||||
endDate: Date | null;
|
endDate: Date | null;
|
||||||
onStartDateChange: (date: Date | null) => void;
|
onStartDateChange: (date: Date | null) => void;
|
||||||
onEndDateChange: (date: Date | null) => void;
|
onEndDateChange: (date: Date | null) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TripDurationPicker({
|
export default function TripDurationPicker({
|
||||||
@@ -24,6 +25,7 @@ export default function TripDurationPicker({
|
|||||||
endDate,
|
endDate,
|
||||||
onStartDateChange,
|
onStartDateChange,
|
||||||
onEndDateChange,
|
onEndDateChange,
|
||||||
|
disabled = false,
|
||||||
}: TripDurationPickerProps) {
|
}: TripDurationPickerProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors, colorScheme } = useThemeContext();
|
const { colors, colorScheme } = useThemeContext();
|
||||||
@@ -127,8 +129,9 @@ export default function TripDurationPicker({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.dateInput, themedStyles.dateInput]}
|
style={[styles.dateInput, themedStyles.dateInput]}
|
||||||
onPress={handleOpenStartPicker}
|
onPress={disabled ? undefined : handleOpenStartPicker}
|
||||||
activeOpacity={0.7}
|
activeOpacity={disabled ? 1 : 0.7}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -139,11 +142,13 @@ export default function TripDurationPicker({
|
|||||||
>
|
>
|
||||||
{startDate ? formatDate(startDate) : t("diary.selectDate")}
|
{startDate ? formatDate(startDate) : t("diary.selectDate")}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!disabled && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="calendar-outline"
|
name="calendar-outline"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.textSecondary}
|
color={colors.textSecondary}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -154,8 +159,9 @@ export default function TripDurationPicker({
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.dateInput, themedStyles.dateInput]}
|
style={[styles.dateInput, themedStyles.dateInput]}
|
||||||
onPress={handleOpenEndPicker}
|
onPress={disabled ? undefined : handleOpenEndPicker}
|
||||||
activeOpacity={0.7}
|
activeOpacity={disabled ? 1 : 0.7}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -166,11 +172,13 @@ export default function TripDurationPicker({
|
|||||||
>
|
>
|
||||||
{endDate ? formatDate(endDate) : t("diary.selectDate")}
|
{endDate ? formatDate(endDate) : t("diary.selectDate")}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!disabled && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="calendar-outline"
|
name="calendar-outline"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.textSecondary}
|
color={colors.textSecondary}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -12,16 +12,17 @@ import { useThemeContext } from "@/hooks/use-theme-context";
|
|||||||
interface TripNameInputProps {
|
interface TripNameInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TripNameInput({ value, onChange }: TripNameInputProps) {
|
export default function TripNameInput({ value, onChange, disabled = false }: TripNameInputProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
|
|
||||||
const themedStyles = {
|
const themedStyles = {
|
||||||
label: { color: colors.text },
|
label: { color: colors.text },
|
||||||
input: {
|
input: {
|
||||||
backgroundColor: colors.card,
|
backgroundColor: disabled ? colors.backgroundSecondary : colors.card,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
@@ -38,6 +39,7 @@ export default function TripNameInput({ value, onChange }: TripNameInputProps) {
|
|||||||
onChangeText={onChange}
|
onChangeText={onChange}
|
||||||
placeholder={t("diary.tripNamePlaceholder")}
|
placeholder={t("diary.tripNamePlaceholder")}
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
editable={!disabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -9,19 +9,21 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
Alert,
|
Alert,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Animated,
|
||||||
|
Dimensions,
|
||||||
} 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";
|
||||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||||
import FishingGearList from "@/components/diary/addTripModal/FishingGearList";
|
import FishingGearList from "@/components/diary/TripFormModal/FishingGearList";
|
||||||
import MaterialCostList from "@/components/diary/addTripModal/MaterialCostList";
|
import MaterialCostList from "@/components/diary/TripFormModal/MaterialCostList";
|
||||||
import TripNameInput from "@/components/diary/addTripModal/TripNameInput";
|
import TripNameInput from "@/components/diary/TripFormModal/TripNameInput";
|
||||||
import TripDurationPicker from "@/components/diary/addTripModal/TripDurationPicker";
|
import TripDurationPicker from "@/components/diary/TripFormModal/TripDurationPicker";
|
||||||
import PortSelector from "@/components/diary/addTripModal/PortSelector";
|
import PortSelector from "@/components/diary/TripFormModal/PortSelector";
|
||||||
import BasicInfoInput from "@/components/diary/addTripModal/BasicInfoInput";
|
import BasicInfoInput from "@/components/diary/TripFormModal/BasicInfoInput";
|
||||||
import ShipSelector from "./ShipSelector";
|
import ShipSelector from "./ShipSelector";
|
||||||
import AutoFillSection from "./AutoFillSection";
|
import AutoFillSection from "./AutoFillSection";
|
||||||
import { createTrip } from "@/controller/TripController";
|
import { createTrip, updateTrip } from "@/controller/TripController";
|
||||||
|
|
||||||
// Internal component interfaces - extend from Model with local id for state management
|
// Internal component interfaces - extend from Model with local id for state management
|
||||||
export interface FishingGear extends Model.FishingGear {
|
export interface FishingGear extends Model.FishingGear {
|
||||||
@@ -32,16 +34,51 @@ export interface TripCost extends Model.TripCost {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddTripModalProps {
|
interface TripFormModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSuccess?: () => void; // Callback khi thêm chuyến đi thành công
|
onSuccess?: () => void;
|
||||||
|
mode?: 'add' | 'edit' | 'view';
|
||||||
|
tripData?: Model.Trip;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddTripModal({ visible, onClose, onSuccess }: AddTripModalProps) {
|
export default function TripFormModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
mode = 'add',
|
||||||
|
tripData,
|
||||||
|
}: TripFormModalProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { colors } = useThemeContext();
|
const { colors } = useThemeContext();
|
||||||
|
|
||||||
|
const isEditMode = mode === 'edit';
|
||||||
|
const isViewMode = mode === 'view';
|
||||||
|
const isReadOnly = isViewMode; // View mode is read-only
|
||||||
|
|
||||||
|
// Animation values
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
const slideAnim = useRef(new Animated.Value(Dimensions.get('window').height)).current;
|
||||||
|
|
||||||
|
// Handle animation when modal visibility changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
// Open animation: fade overlay + slide content up
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(slideAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start();
|
||||||
|
}
|
||||||
|
}, [visible, fadeAnim, slideAnim]);
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [selectedShipId, setSelectedShipId] = useState<string>("");
|
const [selectedShipId, setSelectedShipId] = useState<string>("");
|
||||||
const [tripName, setTripName] = useState("");
|
const [tripName, setTripName] = useState("");
|
||||||
@@ -51,11 +88,85 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||||
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>("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Pre-fill form when in edit or view mode
|
||||||
|
useEffect(() => {
|
||||||
|
if ((isEditMode || isViewMode) && tripData && visible) {
|
||||||
|
// Fill ship ID (use vms_id as thingId)
|
||||||
|
setSelectedShipId(tripData.vms_id || "");
|
||||||
|
|
||||||
|
// Fill trip 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: `${mode}-${Date.now()}-${index}`,
|
||||||
|
name: gear.name || "",
|
||||||
|
number: gear.number?.toString() || "",
|
||||||
|
}));
|
||||||
|
setFishingGears(gears);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill trip costs
|
||||||
|
if (tripData.trip_cost && Array.isArray(tripData.trip_cost)) {
|
||||||
|
const costs: TripCost[] = tripData.trip_cost.map((cost, index) => ({
|
||||||
|
id: `${mode}-${Date.now()}-${index}`,
|
||||||
|
type: cost.type || "",
|
||||||
|
amount: cost.amount || 0,
|
||||||
|
unit: cost.unit || "",
|
||||||
|
cost_per_unit: cost.cost_per_unit || 0,
|
||||||
|
total_cost: cost.total_cost || 0,
|
||||||
|
}));
|
||||||
|
setTripCosts(costs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill dates
|
||||||
|
if (tripData.departure_time) {
|
||||||
|
setStartDate(new Date(tripData.departure_time));
|
||||||
|
}
|
||||||
|
if (tripData.arrival_time) {
|
||||||
|
setEndDate(new Date(tripData.arrival_time));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill ports
|
||||||
|
if (tripData.departure_port_id) {
|
||||||
|
setDeparturePortId(tripData.departure_port_id);
|
||||||
|
}
|
||||||
|
if (tripData.arrival_port_id) {
|
||||||
|
setArrivalPortId(tripData.arrival_port_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill fishing ground codes
|
||||||
|
if (tripData.fishing_ground_codes && Array.isArray(tripData.fishing_ground_codes)) {
|
||||||
|
setFishingGroundCodes(tripData.fishing_ground_codes.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isEditMode, isViewMode, tripData, visible, mode]);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
// Reset form
|
// Close animation: fade overlay + slide content down
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(slideAnim, {
|
||||||
|
toValue: Dimensions.get('window').height,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start(() => {
|
||||||
|
// Close modal after animation completes
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Only reset form in add/edit mode, not needed for view mode
|
||||||
|
if (!isViewMode) {
|
||||||
|
// Reset form after closing
|
||||||
|
setTimeout(() => {
|
||||||
setSelectedShipId("");
|
setSelectedShipId("");
|
||||||
setTripName("");
|
setTripName("");
|
||||||
setFishingGears([]);
|
setFishingGears([]);
|
||||||
@@ -65,7 +176,32 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
setDeparturePortId(1);
|
setDeparturePortId(1);
|
||||||
setArrivalPortId(1);
|
setArrivalPortId(1);
|
||||||
setFishingGroundCodes("");
|
setFishingGroundCodes("");
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle close modal after successful submit - always resets form
|
||||||
|
const handleSuccessClose = () => {
|
||||||
|
// Reset animation values for next open
|
||||||
|
fadeAnim.setValue(0);
|
||||||
|
slideAnim.setValue(Dimensions.get('window').height);
|
||||||
|
|
||||||
|
// Close modal immediately
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
|
// Reset form after closing
|
||||||
|
setTimeout(() => {
|
||||||
|
setSelectedShipId("");
|
||||||
|
setTripName("");
|
||||||
|
setFishingGears([]);
|
||||||
|
setTripCosts([]);
|
||||||
|
setStartDate(null);
|
||||||
|
setEndDate(null);
|
||||||
|
setDeparturePortId(1);
|
||||||
|
setArrivalPortId(1);
|
||||||
|
setFishingGroundCodes("");
|
||||||
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle auto-fill from last trip data
|
// Handle auto-fill from last trip data
|
||||||
@@ -169,32 +305,45 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const response = await createTrip(selectedShipId, apiBody);
|
let response;
|
||||||
|
|
||||||
if (response.data) {
|
if (isEditMode && tripData) {
|
||||||
// Show success alert
|
// Edit mode: call updateTrip
|
||||||
Alert.alert(
|
response = await updateTrip(tripData.id, apiBody);
|
||||||
t("common.success"),
|
} else {
|
||||||
t("diary.createTripSuccess")
|
// Add mode: call createTrip
|
||||||
);
|
response = await createTrip(selectedShipId, apiBody);
|
||||||
|
}
|
||||||
|
|
||||||
// Call onSuccess callback
|
// Check if response is successful (response exists)
|
||||||
|
|
||||||
|
// Call onSuccess callback first (to refresh data)
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess();
|
onSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show success alert
|
||||||
|
Alert.alert(
|
||||||
|
t("common.success"),
|
||||||
|
isEditMode ? t("diary.updateTripSuccess") : t("diary.createTripSuccess")
|
||||||
|
);
|
||||||
|
|
||||||
// Reset form and close modal
|
// Reset form and close modal
|
||||||
handleCancel();
|
console.log("Calling handleSuccessClose");
|
||||||
}
|
handleSuccessClose();
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error creating trip:", error);
|
console.error(isEditMode ? "Error updating trip:" : "Error creating trip:", error);
|
||||||
// Log detailed error information for debugging
|
// Log detailed error information for debugging
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
console.error("Response status:", error.response.status);
|
console.error("Response status:", error.response.status);
|
||||||
console.error("Response data:", JSON.stringify(error.response.data, null, 2));
|
console.error("Response data:", JSON.stringify(error.response.data, null, 2));
|
||||||
}
|
}
|
||||||
console.log("Request body was:", JSON.stringify(apiBody, null, 2));
|
console.log("Request body was:", JSON.stringify(apiBody, null, 2));
|
||||||
Alert.alert(t("common.error"), t("diary.createTripError"));
|
Alert.alert(
|
||||||
|
t("common.error"),
|
||||||
|
isEditMode ? t("diary.updateTripError") : t("diary.createTripError")
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -227,19 +376,29 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
animationType="fade"
|
animationType="none"
|
||||||
transparent
|
transparent
|
||||||
onRequestClose={onClose}
|
onRequestClose={handleCancel}
|
||||||
|
>
|
||||||
|
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.modalContainer,
|
||||||
|
themedStyles.modalContainer,
|
||||||
|
{ transform: [{ translateY: slideAnim }] }
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<View style={styles.overlay}>
|
|
||||||
<View style={[styles.modalContainer, themedStyles.modalContainer]}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={[styles.header, themedStyles.header]}>
|
<View style={[styles.header, themedStyles.header]}>
|
||||||
<TouchableOpacity onPress={handleCancel} style={styles.closeButton}>
|
<TouchableOpacity onPress={handleCancel} style={styles.closeButton}>
|
||||||
<Ionicons name="close" size={24} color={colors.text} />
|
<Ionicons name="close" size={24} color={colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={[styles.title, themedStyles.title]}>
|
<Text style={[styles.title, themedStyles.title]}>
|
||||||
{t("diary.addTrip")}
|
{isViewMode
|
||||||
|
? t("diary.viewTrip")
|
||||||
|
: isEditMode
|
||||||
|
? t("diary.editTrip")
|
||||||
|
: t("diary.addTrip")}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.placeholder} />
|
<View style={styles.placeholder} />
|
||||||
</View>
|
</View>
|
||||||
@@ -248,24 +407,26 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={isViewMode ? { paddingBottom: 40 } : undefined}
|
||||||
>
|
>
|
||||||
{/* Auto Fill Section */}
|
{/* Auto Fill Section - only show in add mode */}
|
||||||
<AutoFillSection onAutoFill={handleAutoFill} />
|
{!isEditMode && !isViewMode && <AutoFillSection onAutoFill={handleAutoFill} />}
|
||||||
|
|
||||||
{/* Ship Selector */}
|
{/* Ship Selector - disabled in edit and view mode */}
|
||||||
<ShipSelector
|
<ShipSelector
|
||||||
selectedShipId={selectedShipId}
|
selectedShipId={selectedShipId}
|
||||||
onChange={setSelectedShipId}
|
onChange={setSelectedShipId}
|
||||||
|
disabled={isEditMode || isViewMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Trip Name */}
|
{/* Trip Name */}
|
||||||
<TripNameInput value={tripName} onChange={setTripName} />
|
<TripNameInput value={tripName} onChange={setTripName} disabled={isReadOnly} />
|
||||||
|
|
||||||
{/* Fishing Gear List */}
|
{/* Fishing Gear List */}
|
||||||
<FishingGearList items={fishingGears} onChange={setFishingGears} />
|
<FishingGearList items={fishingGears} onChange={setFishingGears} disabled={isReadOnly} />
|
||||||
|
|
||||||
{/* Trip Cost List */}
|
{/* Trip Cost List */}
|
||||||
<MaterialCostList items={tripCosts} onChange={setTripCosts} />
|
<MaterialCostList items={tripCosts} onChange={setTripCosts} disabled={isReadOnly} />
|
||||||
|
|
||||||
{/* Trip Duration */}
|
{/* Trip Duration */}
|
||||||
<TripDurationPicker
|
<TripDurationPicker
|
||||||
@@ -273,6 +434,7 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
onStartDateChange={setStartDate}
|
onStartDateChange={setStartDate}
|
||||||
onEndDateChange={setEndDate}
|
onEndDateChange={setEndDate}
|
||||||
|
disabled={isReadOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Port Selector */}
|
{/* Port Selector */}
|
||||||
@@ -281,16 +443,19 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
arrivalPortId={arrivalPortId}
|
arrivalPortId={arrivalPortId}
|
||||||
onDeparturePortChange={setDeparturePortId}
|
onDeparturePortChange={setDeparturePortId}
|
||||||
onArrivalPortChange={setArrivalPortId}
|
onArrivalPortChange={setArrivalPortId}
|
||||||
|
disabled={isReadOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Fishing Ground Codes */}
|
{/* Fishing Ground Codes */}
|
||||||
<BasicInfoInput
|
<BasicInfoInput
|
||||||
fishingGroundCodes={fishingGroundCodes}
|
fishingGroundCodes={fishingGroundCodes}
|
||||||
onChange={setFishingGroundCodes}
|
onChange={setFishingGroundCodes}
|
||||||
|
disabled={isReadOnly}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer - hide in view mode */}
|
||||||
|
{!isViewMode && (
|
||||||
<View style={[styles.footer, themedStyles.footer]}>
|
<View style={[styles.footer, themedStyles.footer]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.cancelButton, themedStyles.cancelButton]}
|
style={[styles.cancelButton, themedStyles.cancelButton]}
|
||||||
@@ -317,13 +482,14 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod
|
|||||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.submitButtonText}>
|
<Text style={styles.submitButtonText}>
|
||||||
{t("diary.createTrip")}
|
{isEditMode ? t("diary.saveChanges") : t("diary.createTrip")}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)}
|
||||||
</View>
|
</Animated.View>
|
||||||
|
</Animated.View>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,7 @@ 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_POST_TRIP = "/api/sgw/trips";
|
||||||
|
export const API_PUT_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";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
API_UPDATE_TRIP_STATUS,
|
API_UPDATE_TRIP_STATUS,
|
||||||
API_GET_LAST_TRIP,
|
API_GET_LAST_TRIP,
|
||||||
API_POST_TRIP,
|
API_POST_TRIP,
|
||||||
|
API_PUT_TRIP,
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
|
|
||||||
export async function queryTrip() {
|
export async function queryTrip() {
|
||||||
@@ -36,3 +37,7 @@ export async function queryTripsList(body: Model.TripListBody) {
|
|||||||
export async function createTrip(thingId: string, body: Model.TripAPIBody) {
|
export async function createTrip(thingId: string, body: Model.TripAPIBody) {
|
||||||
return api.post<Model.Trip>(`${API_POST_TRIP}/${thingId}`, body);
|
return api.post<Model.Trip>(`${API_POST_TRIP}/${thingId}`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateTrip(tripId: string, body: Model.TripAPIBody) {
|
||||||
|
return api.put<Model.Trip>(`${API_PUT_TRIP}/${tripId}`, body);
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,7 +172,12 @@
|
|||||||
"tripNameRequired": "Please enter a trip name"
|
"tripNameRequired": "Please enter a trip name"
|
||||||
},
|
},
|
||||||
"createTripSuccess": "Trip created successfully!",
|
"createTripSuccess": "Trip created successfully!",
|
||||||
"createTripError": "Unable to create trip. Please try again."
|
"createTripError": "Unable to create trip. Please try again.",
|
||||||
|
"editTrip": "Edit Trip",
|
||||||
|
"viewTrip": "Trip Details",
|
||||||
|
"saveChanges": "Save Changes",
|
||||||
|
"updateTripSuccess": "Trip updated successfully!",
|
||||||
|
"updateTripError": "Unable to update trip. Please try again."
|
||||||
},
|
},
|
||||||
"trip": {
|
"trip": {
|
||||||
"infoTrip": "Trip Information",
|
"infoTrip": "Trip Information",
|
||||||
|
|||||||
@@ -172,7 +172,12 @@
|
|||||||
"tripNameRequired": "Vui lòng nhập tên chuyến đi"
|
"tripNameRequired": "Vui lòng nhập tên chuyến đi"
|
||||||
},
|
},
|
||||||
"createTripSuccess": "Tạo chuyến đi thành công!",
|
"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."
|
"createTripError": "Không thể tạo chuyến đi. Vui lòng thử lại.",
|
||||||
|
"editTrip": "Chỉnh sửa chuyến đi",
|
||||||
|
"viewTrip": "Chi tiết chuyến đi",
|
||||||
|
"saveChanges": "Lưu thay đổi",
|
||||||
|
"updateTripSuccess": "Cập nhật chuyến đi thành công!",
|
||||||
|
"updateTripError": "Không thể cập nhật 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