cập nhật animation hiển thị modal, call API edit

This commit is contained in:
2025-12-22 22:47:08 +07:00
parent 67e9fc22a3
commit afc6acbfe2
16 changed files with 530 additions and 216 deletions

View File

@@ -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>
); );

View File

@@ -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 = () => {
onApply({ status, startDate, endDate, selectedShip }); // Close animation then apply
onClose(); 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 });
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}
> >
<TouchableOpacity <Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
style={styles.overlay}
activeOpacity={1}
onPress={onClose}
>
<TouchableOpacity <TouchableOpacity
style={[styles.modalContainer, themedStyles.modalContainer]} style={styles.overlayTouchable}
activeOpacity={1} activeOpacity={1}
onPress={(e) => e.stopPropagation()} onPress={handleClose}
/>
<Animated.View
style={[
styles.modalContainer,
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,

View File

@@ -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]} />

View File

@@ -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>
); );

View File

@@ -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,38 +111,43 @@ 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 */}
<View style={styles.actionButtons}> {!disabled && (
<TouchableOpacity <View style={styles.actionButtons}>
onPress={() => handleDuplicateGear(gear)} <TouchableOpacity
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} onPress={() => handleDuplicateGear(gear)}
> hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
<Ionicons name="copy-outline" size={20} color={colors.text} /> >
</TouchableOpacity> <Ionicons name="copy-outline" size={20} color={colors.text} />
<TouchableOpacity </TouchableOpacity>
onPress={() => handleRemoveGear(gear.id)} <TouchableOpacity
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} onPress={() => handleRemoveGear(gear.id)}
> hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
<Ionicons name="trash-outline" size={20} color={colors.error} /> >
</TouchableOpacity> <Ionicons name="trash-outline" size={20} color={colors.error} />
</View> </TouchableOpacity>
</View>
)}
</View> </View>
))} ))}
{/* Add Button */} {/* Add Button - hide when disabled */}
<TouchableOpacity {!disabled && (
style={[styles.addButton, themedStyles.addButton]} <TouchableOpacity
onPress={handleAddGear} style={[styles.addButton, themedStyles.addButton]}
activeOpacity={0.7} onPress={handleAddGear}
> activeOpacity={0.7}
<Ionicons name="add" size={20} color={colors.primary} /> >
<Text style={[styles.addButtonText, themedStyles.addButtonText]}> <Ionicons name="add" size={20} color={colors.primary} />
{t("diary.addFishingGear")} <Text style={[styles.addButtonText, themedStyles.addButtonText]}>
</Text> {t("diary.addFishingGear")}
</TouchableOpacity> </Text>
</TouchableOpacity>
)}
</View> </View>
); );
} }

View File

@@ -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>
<Ionicons {!disabled && (
name="chevron-down" <Ionicons
size={16} name="chevron-down"
color={colors.textSecondary} size={16}
/> 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,40 +284,44 @@ export default function MaterialCostList({
</View> </View>
</View> </View>
{/* Action Buttons */} {/* Action Buttons - hide when disabled */}
<View style={styles.actionButtons}> {!disabled && (
<TouchableOpacity <View style={styles.actionButtons}>
onPress={() => handleDuplicateMaterial(cost)} <TouchableOpacity
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} onPress={() => handleDuplicateMaterial(cost)}
> hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
<Ionicons name="copy-outline" size={20} color={colors.text} /> >
</TouchableOpacity> <Ionicons name="copy-outline" size={20} color={colors.text} />
<TouchableOpacity </TouchableOpacity>
onPress={() => handleRemoveMaterial(cost.id)} <TouchableOpacity
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} onPress={() => handleRemoveMaterial(cost.id)}
> hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
<Ionicons >
name="trash-outline" <Ionicons
size={20} name="trash-outline"
color={colors.error} size={20}
/> color={colors.error}
</TouchableOpacity> />
</View> </TouchableOpacity>
</View>
)}
</View> </View>
</View> </View>
))} ))}
{/* Add Button */} {/* Add Button - hide when disabled */}
<TouchableOpacity {!disabled && (
style={[styles.addButton, themedStyles.addButton]} <TouchableOpacity
onPress={handleAddMaterial} style={[styles.addButton, themedStyles.addButton]}
activeOpacity={0.7} onPress={handleAddMaterial}
> activeOpacity={0.7}
<Ionicons name="add" size={20} color={colors.primary} /> >
<Text style={[styles.addButtonText, themedStyles.addButtonText]}> <Ionicons name="add" size={20} color={colors.primary} />
{t("diary.addMaterialCost")} <Text style={[styles.addButtonText, themedStyles.addButtonText]}>
</Text> {t("diary.addMaterialCost")}
</TouchableOpacity> </Text>
</TouchableOpacity>
)}
</View> </View>
); );
} }

View File

@@ -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>
<Ionicons {!disabled && (
name="ellipsis-horizontal" <Ionicons
size={20} name="ellipsis-horizontal"
color={colors.textSecondary} size={20}
/> 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>
<Ionicons {!disabled && (
name="ellipsis-horizontal" <Ionicons
size={20} name="ellipsis-horizontal"
color={colors.textSecondary} size={20}
/> color={colors.textSecondary}
/>
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>

View File

@@ -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>
<Ionicons {!disabled && (
name="ellipsis-horizontal" <Ionicons
size={20} name="ellipsis-horizontal"
color={colors.textSecondary} size={20}
/> color={colors.textSecondary}
/>
)}
</TouchableOpacity> </TouchableOpacity>
<Modal <Modal

View File

@@ -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>
<Ionicons {!disabled && (
name="calendar-outline" <Ionicons
size={20} name="calendar-outline"
color={colors.textSecondary} size={20}
/> 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>
<Ionicons {!disabled && (
name="calendar-outline" <Ionicons
size={20} name="calendar-outline"
color={colors.textSecondary} size={20}
/> color={colors.textSecondary}
/>
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>

View File

@@ -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>
); );

View File

@@ -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,21 +88,120 @@ 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
setSelectedShipId(""); Animated.parallel([
setTripName(""); Animated.timing(fadeAnim, {
setFishingGears([]); toValue: 0,
setTripCosts([]); duration: 250,
setStartDate(null); useNativeDriver: true,
setEndDate(null); }),
setDeparturePortId(1); Animated.timing(slideAnim, {
setArrivalPortId(1); toValue: Dimensions.get('window').height,
setFishingGroundCodes(""); 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("");
setTripName("");
setFishingGears([]);
setTripCosts([]);
setStartDate(null);
setEndDate(null);
setDeparturePortId(1);
setArrivalPortId(1);
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
if (onSuccess) {
onSuccess();
}
// Reset form and close modal
handleCancel();
} }
// Check if response is successful (response exists)
// Call onSuccess callback first (to refresh data)
if (onSuccess) {
onSuccess();
}
// Show success alert
Alert.alert(
t("common.success"),
isEditMode ? t("diary.updateTripSuccess") : t("diary.createTripSuccess")
);
// Reset form and close modal
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}
> >
<View style={styles.overlay}> <Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
<View style={[styles.modalContainer, themedStyles.modalContainer]}> <Animated.View
style={[
styles.modalContainer,
themedStyles.modalContainer,
{ transform: [{ translateY: slideAnim }] }
]}
>
{/* 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,49 +443,53 @@ 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 */}
<View style={[styles.footer, themedStyles.footer]}> {!isViewMode && (
<TouchableOpacity <View style={[styles.footer, themedStyles.footer]}>
style={[styles.cancelButton, themedStyles.cancelButton]} <TouchableOpacity
onPress={handleCancel} style={[styles.cancelButton, themedStyles.cancelButton]}
activeOpacity={0.7} onPress={handleCancel}
> activeOpacity={0.7}
<Text
style={[styles.cancelButtonText, themedStyles.cancelButtonText]}
> >
{t("common.cancel")} <Text
</Text> style={[styles.cancelButtonText, themedStyles.cancelButtonText]}
</TouchableOpacity> >
<TouchableOpacity {t("common.cancel")}
style={[
styles.submitButton,
themedStyles.submitButton,
isSubmitting && styles.submitButtonDisabled
]}
onPress={handleSubmit}
activeOpacity={0.7}
disabled={isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<Text style={styles.submitButtonText}>
{t("diary.createTrip")}
</Text> </Text>
)} </TouchableOpacity>
</TouchableOpacity> <TouchableOpacity
</View> style={[
</View> styles.submitButton,
</View> themedStyles.submitButton,
isSubmitting && styles.submitButtonDisabled
]}
onPress={handleSubmit}
activeOpacity={0.7}
disabled={isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<Text style={styles.submitButtonText}>
{isEditMode ? t("diary.saveChanges") : t("diary.createTrip")}
</Text>
)}
</TouchableOpacity>
</View>
)}
</Animated.View>
</Animated.View>
</Modal> </Modal>
); );
} }

View File

@@ -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";

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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",