cập nhật phần modal thêm chuyến đi mới
This commit is contained in:
@@ -117,9 +117,9 @@ export default function TripCard({
|
||||
{/* Info Grid */}
|
||||
<View style={styles.infoGrid}>
|
||||
<View style={styles.infoRow}>
|
||||
<Text style={[styles.label, themedStyles.label]}>{t("diary.tripCard.shipCode")}</Text>
|
||||
<Text style={[styles.label, themedStyles.label]}>{t("diary.tripCard.shipName")}</Text>
|
||||
<Text style={[styles.value, themedStyles.value]}>
|
||||
{thingOfTrip?.metadata?.ship_reg_number /* hoặc trip.ship_id */}
|
||||
{thingOfTrip?.metadata?.ship_name /* hoặc trip.ship_id */}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
388
components/diary/addTripModal/AutoFillSection.tsx
Normal file
388
components/diary/addTripModal/AutoFillSection.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThings } from "@/state/use-thing";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { queryLastTrip } from "@/controller/TripController";
|
||||
import { showErrorToast } from "@/services/toast_service";
|
||||
|
||||
interface AutoFillSectionProps {
|
||||
onAutoFill: (tripData: Model.Trip, selectedShipId: string) => void;
|
||||
}
|
||||
|
||||
export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { things } = useThings();
|
||||
|
||||
// Convert things to ship options
|
||||
const shipOptions =
|
||||
things
|
||||
?.filter((thing) => thing.id != null)
|
||||
.map((thing) => ({
|
||||
id: thing.id as string,
|
||||
shipName: thing.metadata?.ship_name || "",
|
||||
})) || [];
|
||||
|
||||
// Filter ships based on search text
|
||||
const filteredShips = shipOptions.filter((ship) => {
|
||||
const searchLower = searchText.toLowerCase();
|
||||
return ship.shipName.toLowerCase().includes(searchLower);
|
||||
});
|
||||
|
||||
const handleSelectShip = async (shipId: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await queryLastTrip(shipId);
|
||||
if (response.data) {
|
||||
// Close the modal first before showing alert
|
||||
setIsOpen(false);
|
||||
setSearchText("");
|
||||
|
||||
// Pass shipId (thingId) along with trip data for filling ShipSelector
|
||||
onAutoFill(response.data, shipId);
|
||||
|
||||
// Use Alert instead of Toast so it appears above all modals
|
||||
Alert.alert(
|
||||
t("common.success"),
|
||||
t("diary.autoFill.success")
|
||||
);
|
||||
} else {
|
||||
showErrorToast(t("diary.autoFill.noData"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching last trip:", error);
|
||||
showErrorToast(t("diary.autoFill.error"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
container: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
label: { color: colors.text },
|
||||
description: { color: colors.textSecondary },
|
||||
button: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
modalContent: { backgroundColor: colors.card },
|
||||
searchContainer: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
searchInput: { color: colors.text },
|
||||
option: { borderBottomColor: colors.separator },
|
||||
optionText: { color: colors.text },
|
||||
emptyText: { color: colors.textSecondary },
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, themedStyles.container]}>
|
||||
<View style={styles.contentWrapper}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="flash" size={24} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.autoFill.title")}
|
||||
</Text>
|
||||
<Text style={[styles.description, themedStyles.description]}>
|
||||
{t("diary.autoFill.description")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, themedStyles.button]}
|
||||
onPress={() => setIsOpen(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.buttonText}>{t("diary.autoFill.selectShip")}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
visible={isOpen}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setIsOpen(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setIsOpen(false)}
|
||||
>
|
||||
<View
|
||||
style={[styles.modalContent, themedStyles.modalContent]}
|
||||
onStartShouldSetResponder={() => true}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={[styles.modalTitle, { color: colors.text }]}>
|
||||
{t("diary.autoFill.modalTitle")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Search Input */}
|
||||
<View
|
||||
style={[styles.searchContainer, themedStyles.searchContainer]}
|
||||
>
|
||||
<Ionicons
|
||||
name="search"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.searchInput, themedStyles.searchInput]}
|
||||
placeholder={t("diary.searchShip")}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
{searchText.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchText("")}>
|
||||
<Ionicons
|
||||
name="close-circle"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>
|
||||
{t("diary.autoFill.loading")}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView style={styles.optionsList}>
|
||||
{/* Filtered ship options */}
|
||||
{filteredShips.length > 0 ? (
|
||||
filteredShips.map((ship) => (
|
||||
<TouchableOpacity
|
||||
key={ship.id}
|
||||
style={[styles.option, themedStyles.option]}
|
||||
onPress={() => handleSelectShip(ship.id)}
|
||||
>
|
||||
<View style={styles.optionContent}>
|
||||
<Ionicons
|
||||
name="boat"
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
style={styles.optionIcon}
|
||||
/>
|
||||
<Text style={[styles.optionText, themedStyles.optionText]}>
|
||||
{ship.shipName}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={[styles.emptyText, themedStyles.emptyText]}>
|
||||
{t("diary.noShipsFound")}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderStyle: "dashed",
|
||||
},
|
||||
contentWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 4,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
description: {
|
||||
fontSize: 13,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#FFFFFF",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
modalContent: {
|
||||
borderRadius: 12,
|
||||
width: "85%",
|
||||
maxHeight: "70%",
|
||||
overflow: "hidden",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
modalHeader: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
padding: 0,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
optionsList: {
|
||||
maxHeight: 350,
|
||||
},
|
||||
option: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
optionContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
},
|
||||
optionIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
emptyContainer: {
|
||||
paddingVertical: 24,
|
||||
alignItems: "center",
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
loadingContainer: {
|
||||
paddingVertical: 40,
|
||||
alignItems: "center",
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 14,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
89
components/diary/addTripModal/BasicInfoInput.tsx
Normal file
89
components/diary/addTripModal/BasicInfoInput.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface BasicInfoInputProps {
|
||||
fishingGroundCodes: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function BasicInfoInput({
|
||||
fishingGroundCodes,
|
||||
onChange,
|
||||
}: BasicInfoInputProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const themedStyles = {
|
||||
label: { color: colors.text },
|
||||
subLabel: { color: colors.textSecondary },
|
||||
input: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.fishingGroundCodes")}
|
||||
</Text>
|
||||
<Text style={[styles.subLabel, themedStyles.subLabel]}>
|
||||
{t("diary.fishingGroundCodesHint")}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, themedStyles.input]}
|
||||
value={fishingGroundCodes}
|
||||
onChangeText={onChange}
|
||||
placeholder={t("diary.fishingGroundCodesPlaceholder")}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 12,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
subLabel: {
|
||||
fontSize: 14,
|
||||
marginBottom: 8,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
225
components/diary/addTripModal/FishingGearList.tsx
Normal file
225
components/diary/addTripModal/FishingGearList.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
TextInput,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface FishingGear {
|
||||
id: string;
|
||||
name: string;
|
||||
number: string;
|
||||
}
|
||||
|
||||
interface FishingGearListProps {
|
||||
items: FishingGear[];
|
||||
onChange: (items: FishingGear[]) => void;
|
||||
}
|
||||
|
||||
export default function FishingGearList({
|
||||
items,
|
||||
onChange,
|
||||
}: FishingGearListProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const handleAddGear = () => {
|
||||
const newGear: FishingGear = {
|
||||
id: Date.now().toString(),
|
||||
name: "",
|
||||
number: "",
|
||||
};
|
||||
onChange([...items, newGear]);
|
||||
};
|
||||
|
||||
const handleRemoveGear = (id: string) => {
|
||||
onChange(items.filter((item) => item.id !== id));
|
||||
};
|
||||
|
||||
const handleDuplicateGear = (gear: FishingGear) => {
|
||||
const duplicatedGear: FishingGear = {
|
||||
id: Date.now().toString(),
|
||||
name: gear.name,
|
||||
number: gear.number,
|
||||
};
|
||||
onChange([...items, duplicatedGear]);
|
||||
};
|
||||
|
||||
const handleUpdateGear = (id: string, field: keyof FishingGear, value: string) => {
|
||||
onChange(
|
||||
items.map((item) =>
|
||||
item.id === id ? { ...item, [field]: value } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
sectionTitle: { color: colors.text },
|
||||
fieldLabel: { color: colors.text },
|
||||
input: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
},
|
||||
addButton: {
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
addButtonText: { color: colors.primary },
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.sectionTitle, themedStyles.sectionTitle]}>
|
||||
{t("diary.fishingGearList")}
|
||||
</Text>
|
||||
|
||||
{/* Gear Items List */}
|
||||
{items.map((gear, index) => (
|
||||
<View key={gear.id} style={styles.gearRow}>
|
||||
{/* Name Input */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
||||
{t("diary.gearName")}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.nameInput, themedStyles.input]}
|
||||
value={gear.name}
|
||||
onChangeText={(value) => handleUpdateGear(gear.id, "name", value)}
|
||||
placeholder={t("diary.gearNamePlaceholder")}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Number Input */}
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
||||
{t("diary.gearNumber")}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.numberInput, themedStyles.input]}
|
||||
value={gear.number}
|
||||
onChangeText={(value) => handleUpdateGear(gear.id, "number", value)}
|
||||
placeholder={t("diary.gearNumberPlaceholder")}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.actionButtons}>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleDuplicateGear(gear)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="copy-outline" size={20} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleRemoveGear(gear.id)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Add Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, themedStyles.addButton]}
|
||||
onPress={handleAddGear}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="add" size={20} color={colors.primary} />
|
||||
<Text style={[styles.addButtonText, themedStyles.addButtonText]}>
|
||||
{t("diary.addFishingGear")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
marginBottom: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
gearRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputGroup: {
|
||||
flex: 1,
|
||||
},
|
||||
fieldLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
marginBottom: 6,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 15,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
nameInput: {
|
||||
flex: 1,
|
||||
},
|
||||
numberInput: {
|
||||
flex: 1,
|
||||
},
|
||||
actionButtons: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
paddingBottom: 10,
|
||||
},
|
||||
addButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 8,
|
||||
borderStyle: "dashed",
|
||||
marginTop: 4,
|
||||
},
|
||||
addButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
472
components/diary/addTripModal/MaterialCostList.tsx
Normal file
472
components/diary/addTripModal/MaterialCostList.tsx
Normal file
@@ -0,0 +1,472 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
TextInput,
|
||||
Modal,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface TripCost {
|
||||
id: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
unit: string;
|
||||
cost_per_unit: number;
|
||||
total_cost: number;
|
||||
}
|
||||
|
||||
interface MaterialCostListProps {
|
||||
items: TripCost[];
|
||||
onChange: (items: TripCost[]) => void;
|
||||
}
|
||||
|
||||
// Predefined cost types
|
||||
const COST_TYPES = [
|
||||
{ value: "fuel", label: "Nhiên liệu" },
|
||||
{ value: "food", label: "Thực phẩm" },
|
||||
{ value: "crew_salary", label: "Lương thuyền viên" },
|
||||
{ value: "ice_salt_cost", label: "Muối đá" },
|
||||
{ value: "other", label: "Khác" },
|
||||
];
|
||||
|
||||
export default function MaterialCostList({
|
||||
items,
|
||||
onChange,
|
||||
}: MaterialCostListProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
const [typeDropdownVisible, setTypeDropdownVisible] = useState<string | null>(null);
|
||||
|
||||
const handleAddMaterial = () => {
|
||||
const newMaterial: TripCost = {
|
||||
id: Date.now().toString(),
|
||||
type: "",
|
||||
amount: 0,
|
||||
unit: "",
|
||||
cost_per_unit: 0,
|
||||
total_cost: 0,
|
||||
};
|
||||
onChange([...items, newMaterial]);
|
||||
};
|
||||
|
||||
const handleRemoveMaterial = (id: string) => {
|
||||
onChange(items.filter((item) => item.id !== id));
|
||||
};
|
||||
|
||||
const handleDuplicateMaterial = (material: TripCost) => {
|
||||
const duplicatedMaterial: TripCost = {
|
||||
id: Date.now().toString(),
|
||||
type: material.type,
|
||||
amount: material.amount,
|
||||
unit: material.unit,
|
||||
cost_per_unit: material.cost_per_unit,
|
||||
total_cost: material.total_cost,
|
||||
};
|
||||
onChange([...items, duplicatedMaterial]);
|
||||
};
|
||||
|
||||
const handleUpdateMaterial = (id: string, field: keyof TripCost, value: string | number) => {
|
||||
onChange(
|
||||
items.map((item) => {
|
||||
if (item.id === id) {
|
||||
const updatedItem = { ...item, [field]: value };
|
||||
// Auto-calculate total_cost when amount or cost_per_unit changes
|
||||
if (field === "amount" || field === "cost_per_unit") {
|
||||
const amount = field === "amount" ? Number(value) : item.amount;
|
||||
const costPerUnit = field === "cost_per_unit" ? Number(value) : item.cost_per_unit;
|
||||
updatedItem.total_cost = amount * costPerUnit;
|
||||
}
|
||||
return updatedItem;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeLabel = (value: string) => {
|
||||
const type = COST_TYPES.find((t) => t.value === value);
|
||||
return type ? type.label : value || t("diary.selectType");
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("vi-VN").format(amount);
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
sectionTitle: { color: colors.text },
|
||||
fieldLabel: { color: colors.text },
|
||||
input: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
},
|
||||
dropdown: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
dropdownText: { color: colors.text },
|
||||
placeholder: { color: colors.textSecondary },
|
||||
addButton: {
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
addButtonText: { color: colors.primary },
|
||||
modalOverlay: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.card,
|
||||
},
|
||||
option: {
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
optionText: { color: colors.text },
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.sectionTitle, themedStyles.sectionTitle]}>
|
||||
{t("diary.materialCostList")}
|
||||
</Text>
|
||||
|
||||
{/* Cost Items List */}
|
||||
{items.map((cost) => (
|
||||
<View key={cost.id} style={styles.costRow}>
|
||||
<View style={styles.formRow}>
|
||||
{/* Type Dropdown */}
|
||||
<View style={[styles.inputGroup, styles.typeGroup]}>
|
||||
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
||||
{t("diary.costType")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.dropdown, themedStyles.dropdown]}
|
||||
onPress={() => setTypeDropdownVisible(cost.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dropdownText,
|
||||
themedStyles.dropdownText,
|
||||
!cost.type && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{getTypeLabel(cost.type)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-down"
|
||||
size={16}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Type Dropdown Modal */}
|
||||
<Modal
|
||||
visible={typeDropdownVisible === cost.id}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setTypeDropdownVisible(null)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalOverlay, themedStyles.modalOverlay]}
|
||||
activeOpacity={1}
|
||||
onPress={() => setTypeDropdownVisible(null)}
|
||||
>
|
||||
<View style={[styles.modalContent, themedStyles.modalContent]}>
|
||||
<ScrollView>
|
||||
{COST_TYPES.map((type) => (
|
||||
<TouchableOpacity
|
||||
key={type.value}
|
||||
style={[styles.option, themedStyles.option]}
|
||||
onPress={() => {
|
||||
handleUpdateMaterial(cost.id, "type", type.value);
|
||||
setTypeDropdownVisible(null);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[styles.optionText, themedStyles.optionText]}
|
||||
>
|
||||
{type.label}
|
||||
</Text>
|
||||
{cost.type === type.value && (
|
||||
<Ionicons
|
||||
name="checkmark"
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
|
||||
{/* Amount Input */}
|
||||
<View style={[styles.inputGroup, styles.smallGroup]}>
|
||||
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
||||
{t("diary.amount")}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.smallInput, themedStyles.input]}
|
||||
value={cost.amount.toString()}
|
||||
onChangeText={(value) =>
|
||||
handleUpdateMaterial(cost.id, "amount", Number(value) || 0)
|
||||
}
|
||||
placeholder="0"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Unit Input */}
|
||||
<View style={[styles.inputGroup, styles.smallGroup]}>
|
||||
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
||||
{t("diary.unit")}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.smallInput, themedStyles.input]}
|
||||
value={cost.unit}
|
||||
onChangeText={(value) =>
|
||||
handleUpdateMaterial(cost.id, "unit", value)
|
||||
}
|
||||
placeholder={t("diary.unitPlaceholder")}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.formRow}>
|
||||
{/* Cost Per Unit Input */}
|
||||
<View style={[styles.inputGroup, styles.mediumGroup]}>
|
||||
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
||||
{t("diary.costPerUnit")}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.mediumInput, themedStyles.input]}
|
||||
value={cost.cost_per_unit.toString()}
|
||||
onChangeText={(value) =>
|
||||
handleUpdateMaterial(
|
||||
cost.id,
|
||||
"cost_per_unit",
|
||||
Number(value) || 0
|
||||
)
|
||||
}
|
||||
placeholder="0"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Total Cost (Read-only, auto-calculated) */}
|
||||
<View style={[styles.inputGroup, styles.mediumGroup]}>
|
||||
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
||||
{t("diary.totalCost")}
|
||||
</Text>
|
||||
<View style={[styles.input, styles.mediumInput, themedStyles.input, styles.readOnlyInput]}>
|
||||
<Text style={[styles.readOnlyText, themedStyles.dropdownText]}>
|
||||
{formatCurrency(cost.total_cost)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.actionButtons}>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleDuplicateMaterial(cost)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="copy-outline" size={20} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleRemoveMaterial(cost.id)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons
|
||||
name="trash-outline"
|
||||
size={20}
|
||||
color={colors.error}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Add Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, themedStyles.addButton]}
|
||||
onPress={handleAddMaterial}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="add" size={20} color={colors.primary} />
|
||||
<Text style={[styles.addButtonText, themedStyles.addButtonText]}>
|
||||
{t("diary.addMaterialCost")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
marginBottom: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
costRow: {
|
||||
marginBottom: 20,
|
||||
paddingBottom: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5E5",
|
||||
},
|
||||
formRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
gap: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
inputGroup: {
|
||||
flex: 1,
|
||||
},
|
||||
typeGroup: {
|
||||
flex: 1.5,
|
||||
},
|
||||
smallGroup: {
|
||||
flex: 0.8,
|
||||
},
|
||||
mediumGroup: {
|
||||
flex: 1,
|
||||
},
|
||||
fieldLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
marginBottom: 6,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 15,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
smallInput: {},
|
||||
mediumInput: {},
|
||||
dropdown: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
dropdownText: {
|
||||
fontSize: 15,
|
||||
flex: 1,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
readOnlyInput: {
|
||||
justifyContent: "center",
|
||||
opacity: 0.7,
|
||||
},
|
||||
readOnlyText: {
|
||||
fontSize: 15,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
actionButtons: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
paddingBottom: 10,
|
||||
},
|
||||
addButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 8,
|
||||
borderStyle: "dashed",
|
||||
marginTop: 4,
|
||||
},
|
||||
addButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
// Modal styles
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
modalContent: {
|
||||
borderRadius: 12,
|
||||
width: "80%",
|
||||
maxHeight: "50%",
|
||||
overflow: "hidden",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
option: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
171
components/diary/addTripModal/PortSelector.tsx
Normal file
171
components/diary/addTripModal/PortSelector.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface PortSelectorProps {
|
||||
departurePortId: number;
|
||||
arrivalPortId: number;
|
||||
onDeparturePortChange: (portId: number) => void;
|
||||
onArrivalPortChange: (portId: number) => void;
|
||||
}
|
||||
|
||||
export default function PortSelector({
|
||||
departurePortId,
|
||||
arrivalPortId,
|
||||
onDeparturePortChange,
|
||||
onArrivalPortChange,
|
||||
}: PortSelectorProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const handleSelectDeparturePort = () => {
|
||||
console.log("Select departure port pressed");
|
||||
// TODO: Implement port selection modal/dropdown
|
||||
// For now, just set a dummy ID
|
||||
onDeparturePortChange(1);
|
||||
};
|
||||
|
||||
const handleSelectArrivalPort = () => {
|
||||
console.log("Select arrival port pressed");
|
||||
// TODO: Implement port selection modal/dropdown
|
||||
// For now, just set a dummy ID
|
||||
onArrivalPortChange(1);
|
||||
};
|
||||
|
||||
// Helper to display port name (in production, fetch from port list by ID)
|
||||
const getPortDisplayName = (portId: number): string => {
|
||||
// TODO: Fetch actual port name by ID from port list
|
||||
return portId ? `Cảng (ID: ${portId})` : t("diary.selectPort");
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
label: { color: colors.text },
|
||||
portSelector: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
portText: { color: colors.text },
|
||||
placeholder: { color: colors.textSecondary },
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.portLabel")}
|
||||
</Text>
|
||||
<View style={styles.portContainer}>
|
||||
{/* Departure Port */}
|
||||
<View style={styles.portSection}>
|
||||
<Text style={[styles.subLabel, themedStyles.placeholder]}>
|
||||
{t("diary.departurePort")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.portSelector, themedStyles.portSelector]}
|
||||
onPress={handleSelectDeparturePort}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.portText,
|
||||
themedStyles.portText,
|
||||
!departurePortId && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{getPortDisplayName(departurePortId)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="ellipsis-horizontal"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Arrival Port */}
|
||||
<View style={styles.portSection}>
|
||||
<Text style={[styles.subLabel, themedStyles.placeholder]}>
|
||||
{t("diary.arrivalPort")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.portSelector, themedStyles.portSelector]}
|
||||
onPress={handleSelectArrivalPort}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.portText,
|
||||
themedStyles.portText,
|
||||
!arrivalPortId && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{getPortDisplayName(arrivalPortId)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="ellipsis-horizontal"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 12,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
subLabel: {
|
||||
fontSize: 14,
|
||||
marginBottom: 6,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
portContainer: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
portSection: {
|
||||
flex: 1,
|
||||
},
|
||||
portSelector: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
portText: {
|
||||
fontSize: 15,
|
||||
flex: 1,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
293
components/diary/addTripModal/ShipSelector.tsx
Normal file
293
components/diary/addTripModal/ShipSelector.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThings } from "@/state/use-thing";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface ShipSelectorProps {
|
||||
selectedShipId: string;
|
||||
onChange: (shipId: string) => void;
|
||||
}
|
||||
|
||||
export default function ShipSelector({
|
||||
selectedShipId,
|
||||
onChange,
|
||||
}: ShipSelectorProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const { things } = useThings();
|
||||
|
||||
// Convert things to ship options
|
||||
const shipOptions =
|
||||
things
|
||||
?.filter((thing) => thing.id != null)
|
||||
.map((thing) => ({
|
||||
id: thing.id as string,
|
||||
shipName: thing.metadata?.ship_name || "",
|
||||
})) || [];
|
||||
|
||||
// Filter ships based on search text
|
||||
const filteredShips = shipOptions.filter((ship) => {
|
||||
const searchLower = searchText.toLowerCase();
|
||||
return ship.shipName.toLowerCase().includes(searchLower);
|
||||
});
|
||||
|
||||
const handleSelect = (shipId: string) => {
|
||||
onChange(shipId);
|
||||
setIsOpen(false);
|
||||
setSearchText("");
|
||||
};
|
||||
|
||||
const selectedShip = shipOptions.find((ship) => ship.id === selectedShipId);
|
||||
const displayValue = selectedShip
|
||||
? selectedShip.shipName
|
||||
: t("diary.selectShip");
|
||||
|
||||
const themedStyles = {
|
||||
label: { color: colors.text },
|
||||
selector: { backgroundColor: colors.card, borderColor: colors.border },
|
||||
selectorText: { color: colors.text },
|
||||
placeholder: { color: colors.textSecondary },
|
||||
modalContent: { backgroundColor: colors.card },
|
||||
searchContainer: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
searchInput: { color: colors.text },
|
||||
option: { borderBottomColor: colors.separator },
|
||||
selectedOption: { backgroundColor: colors.backgroundSecondary },
|
||||
optionText: { color: colors.text },
|
||||
emptyText: { color: colors.textSecondary },
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.shipSelector")}
|
||||
<Text style={styles.required}> *</Text>
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.selector, themedStyles.selector]}
|
||||
onPress={() => setIsOpen(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.selectorText,
|
||||
themedStyles.selectorText,
|
||||
!selectedShipId && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{displayValue}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="ellipsis-horizontal"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
visible={isOpen}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setIsOpen(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setIsOpen(false)}
|
||||
>
|
||||
<View
|
||||
style={[styles.modalContent, themedStyles.modalContent]}
|
||||
onStartShouldSetResponder={() => true}
|
||||
>
|
||||
{/* Search Input */}
|
||||
<View
|
||||
style={[styles.searchContainer, themedStyles.searchContainer]}
|
||||
>
|
||||
<Ionicons
|
||||
name="search"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.searchInput, themedStyles.searchInput]}
|
||||
placeholder={t("diary.searchShip")}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
{searchText.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchText("")}>
|
||||
<Ionicons
|
||||
name="close-circle"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.optionsList}>
|
||||
{/* Filtered ship options */}
|
||||
{filteredShips.length > 0 ? (
|
||||
filteredShips.map((ship) => (
|
||||
<TouchableOpacity
|
||||
key={ship.id}
|
||||
style={[
|
||||
styles.option,
|
||||
themedStyles.option,
|
||||
selectedShipId === ship.id && themedStyles.selectedOption,
|
||||
]}
|
||||
onPress={() => handleSelect(ship.id)}
|
||||
>
|
||||
<Text style={[styles.optionText, themedStyles.optionText]}>
|
||||
{ship.shipName}
|
||||
</Text>
|
||||
{selectedShipId === ship.id && (
|
||||
<Ionicons
|
||||
name="checkmark"
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={[styles.emptyText, themedStyles.emptyText]}>
|
||||
{t("diary.noShipsFound")}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 8,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
required: {
|
||||
color: "#EF4444",
|
||||
},
|
||||
selector: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
selectorText: {
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
modalContent: {
|
||||
borderRadius: 12,
|
||||
width: "85%",
|
||||
maxHeight: "70%",
|
||||
overflow: "hidden",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
padding: 0,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
optionsList: {
|
||||
maxHeight: 350,
|
||||
},
|
||||
option: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
emptyContainer: {
|
||||
paddingVertical: 24,
|
||||
alignItems: "center",
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
289
components/diary/addTripModal/TripDurationPicker.tsx
Normal file
289
components/diary/addTripModal/TripDurationPicker.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
Modal,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import DateTimePicker from "@react-native-community/datetimepicker";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface TripDurationPickerProps {
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
onStartDateChange: (date: Date | null) => void;
|
||||
onEndDateChange: (date: Date | null) => void;
|
||||
}
|
||||
|
||||
export default function TripDurationPicker({
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange,
|
||||
onEndDateChange,
|
||||
}: TripDurationPickerProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors, colorScheme } = useThemeContext();
|
||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||
|
||||
const formatDate = (date: Date | null) => {
|
||||
if (!date) return "";
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const handleStartDateChange = (event: any, selectedDate?: Date) => {
|
||||
setShowStartPicker(Platform.OS === "ios");
|
||||
if (selectedDate) {
|
||||
onStartDateChange(selectedDate);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndDateChange = (event: any, selectedDate?: Date) => {
|
||||
setShowEndPicker(Platform.OS === "ios");
|
||||
if (selectedDate) {
|
||||
onEndDateChange(selectedDate);
|
||||
}
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
label: { color: colors.text },
|
||||
dateInput: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
dateText: { color: colors.text },
|
||||
placeholder: { color: colors.textSecondary },
|
||||
pickerContainer: { backgroundColor: colors.card },
|
||||
pickerHeader: { borderBottomColor: colors.border },
|
||||
pickerTitle: { color: colors.text },
|
||||
cancelButton: { color: colors.textSecondary },
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.tripDuration")}
|
||||
</Text>
|
||||
<View style={styles.dateRangeContainer}>
|
||||
{/* Start Date */}
|
||||
<View style={styles.dateSection}>
|
||||
<Text style={[styles.subLabel, themedStyles.placeholder]}>
|
||||
{t("diary.startDate")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.dateInput, themedStyles.dateInput]}
|
||||
onPress={() => setShowStartPicker(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dateText,
|
||||
themedStyles.dateText,
|
||||
!startDate && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{startDate ? formatDate(startDate) : t("diary.selectDate")}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="calendar-outline"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* End Date */}
|
||||
<View style={styles.dateSection}>
|
||||
<Text style={[styles.subLabel, themedStyles.placeholder]}>
|
||||
{t("diary.endDate")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.dateInput, themedStyles.dateInput]}
|
||||
onPress={() => setShowEndPicker(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dateText,
|
||||
themedStyles.dateText,
|
||||
!endDate && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{endDate ? formatDate(endDate) : t("diary.selectDate")}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="calendar-outline"
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Start Date Picker */}
|
||||
{showStartPicker && (
|
||||
<Modal transparent animationType="fade" visible={showStartPicker}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.pickerContainer, themedStyles.pickerContainer]}>
|
||||
<View style={[styles.pickerHeader, themedStyles.pickerHeader]}>
|
||||
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||
<Text style={[styles.cancelButton, themedStyles.cancelButton]}>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
||||
{t("diary.selectStartDate")}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
value={startDate || new Date()}
|
||||
mode="date"
|
||||
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||
onChange={handleStartDateChange}
|
||||
maximumDate={endDate || undefined}
|
||||
themeVariant={colorScheme}
|
||||
textColor={colors.text}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* End Date Picker */}
|
||||
{showEndPicker && (
|
||||
<Modal transparent animationType="fade" visible={showEndPicker}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.pickerContainer, themedStyles.pickerContainer]}>
|
||||
<View style={[styles.pickerHeader, themedStyles.pickerHeader]}>
|
||||
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||
<Text style={[styles.cancelButton, themedStyles.cancelButton]}>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
||||
{t("diary.selectEndDate")}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
value={endDate || new Date()}
|
||||
mode="date"
|
||||
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||
onChange={handleEndDateChange}
|
||||
minimumDate={startDate || undefined}
|
||||
themeVariant={colorScheme}
|
||||
textColor={colors.text}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 12,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
subLabel: {
|
||||
fontSize: 14,
|
||||
marginBottom: 6,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
dateRangeContainer: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
dateSection: {
|
||||
flex: 1,
|
||||
},
|
||||
dateInput: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 15,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
pickerContainer: {
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
pickerHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
pickerTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
cancelButton: {
|
||||
fontSize: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
doneButton: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#3B82F6",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
72
components/diary/addTripModal/TripNameInput.tsx
Normal file
72
components/diary/addTripModal/TripNameInput.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
interface TripNameInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function TripNameInput({ value, onChange }: TripNameInputProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const themedStyles = {
|
||||
label: { color: colors.text },
|
||||
input: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.tripNameLabel")}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, themedStyles.input]}
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
placeholder={t("diary.tripNamePlaceholder")}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 8,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
386
components/diary/addTripModal/index.tsx
Normal file
386
components/diary/addTripModal/index.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Modal,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import FishingGearList from "@/components/diary/addTripModal/FishingGearList";
|
||||
import MaterialCostList from "@/components/diary/addTripModal/MaterialCostList";
|
||||
import TripNameInput from "@/components/diary/addTripModal/TripNameInput";
|
||||
import TripDurationPicker from "@/components/diary/addTripModal/TripDurationPicker";
|
||||
import PortSelector from "@/components/diary/addTripModal/PortSelector";
|
||||
import BasicInfoInput from "@/components/diary/addTripModal/BasicInfoInput";
|
||||
import ShipSelector from "./ShipSelector";
|
||||
import AutoFillSection from "./AutoFillSection";
|
||||
|
||||
|
||||
// Internal component interfaces
|
||||
export interface FishingGear {
|
||||
id: string;
|
||||
name: string;
|
||||
number: string; // Changed from quantity to number (string)
|
||||
}
|
||||
|
||||
export interface TripCost {
|
||||
id: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
unit: string;
|
||||
cost_per_unit: number;
|
||||
total_cost: number;
|
||||
}
|
||||
|
||||
// API body interface
|
||||
export interface TripAPIBody {
|
||||
thing_id?: string; // Ship ID
|
||||
name: string;
|
||||
departure_time: string; // ISO string
|
||||
departure_port_id: number;
|
||||
arrival_time: string; // ISO string
|
||||
arrival_port_id: number;
|
||||
fishing_ground_codes: number[]; // Array of numbers
|
||||
fishing_gears: Array<{
|
||||
name: string;
|
||||
number: string;
|
||||
}>;
|
||||
trip_cost: Array<{
|
||||
type: string;
|
||||
amount: number;
|
||||
unit: string;
|
||||
cost_per_unit: number;
|
||||
total_cost: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface AddTripModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function AddTripModal({ visible, onClose }: AddTripModalProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
// Form state
|
||||
const [selectedShipId, setSelectedShipId] = useState<string>("");
|
||||
const [tripName, setTripName] = useState("");
|
||||
const [fishingGears, setFishingGears] = useState<FishingGear[]>([]);
|
||||
const [tripCosts, setTripCosts] = useState<TripCost[]>([]);
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [departurePortId, setDeparturePortId] = useState<number>(1);
|
||||
const [arrivalPortId, setArrivalPortId] = useState<number>(1);
|
||||
const [fishingGroundCodes, setFishingGroundCodes] = useState<string>(""); // Input as string, convert to array
|
||||
|
||||
const handleCancel = () => {
|
||||
// Reset form
|
||||
setSelectedShipId("");
|
||||
setTripName("");
|
||||
setFishingGears([]);
|
||||
setTripCosts([]);
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
setDeparturePortId(1);
|
||||
setArrivalPortId(1);
|
||||
setFishingGroundCodes("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Handle auto-fill from last trip data
|
||||
const handleAutoFill = (tripData: Model.Trip, selectedThingId: string) => {
|
||||
// Fill ship ID (use the thingId from the selected ship for ShipSelector)
|
||||
setSelectedShipId(selectedThingId);
|
||||
|
||||
// Fill trip name
|
||||
if (tripData.name) {
|
||||
setTripName(tripData.name);
|
||||
}
|
||||
|
||||
// Fill fishing gears
|
||||
if (tripData.fishing_gears && Array.isArray(tripData.fishing_gears)) {
|
||||
const gears: FishingGear[] = tripData.fishing_gears.map((gear, index) => ({
|
||||
id: `auto-${Date.now()}-${index}`,
|
||||
name: gear.name || "",
|
||||
number: gear.number?.toString() || "",
|
||||
}));
|
||||
setFishingGears(gears);
|
||||
}
|
||||
|
||||
// Fill trip costs
|
||||
if (tripData.trip_cost && Array.isArray(tripData.trip_cost)) {
|
||||
const costs: TripCost[] = tripData.trip_cost.map((cost, index) => ({
|
||||
id: `auto-${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 departure and arrival 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(", "));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Parse fishing ground codes from comma-separated string to array of numbers
|
||||
const fishingGroundCodesArray = fishingGroundCodes
|
||||
.split(",")
|
||||
.map((code) => parseInt(code.trim()))
|
||||
.filter((code) => !isNaN(code));
|
||||
|
||||
// Format API body
|
||||
const apiBody: TripAPIBody = {
|
||||
thing_id: selectedShipId || undefined,
|
||||
name: tripName,
|
||||
departure_time: startDate ? startDate.toISOString() : "",
|
||||
departure_port_id: departurePortId,
|
||||
arrival_time: endDate ? endDate.toISOString() : "",
|
||||
arrival_port_id: arrivalPortId,
|
||||
fishing_ground_codes: fishingGroundCodesArray,
|
||||
fishing_gears: fishingGears.map((gear) => ({
|
||||
name: gear.name,
|
||||
number: gear.number,
|
||||
})),
|
||||
trip_cost: tripCosts.map((cost) => ({
|
||||
type: cost.type,
|
||||
amount: cost.amount,
|
||||
unit: cost.unit,
|
||||
cost_per_unit: cost.cost_per_unit,
|
||||
total_cost: cost.total_cost,
|
||||
})),
|
||||
};
|
||||
|
||||
// Simulate API call - log the formatted data
|
||||
console.log("=== Submitting Trip Data (API Format) ===");
|
||||
console.log(JSON.stringify(apiBody, null, 2));
|
||||
console.log("=== End Trip Data ===");
|
||||
|
||||
// Reset form and close modal
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
modalContainer: {
|
||||
backgroundColor: colors.card,
|
||||
},
|
||||
header: {
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
title: {
|
||||
color: colors.text,
|
||||
},
|
||||
footer: {
|
||||
borderTopColor: colors.separator,
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<View style={[styles.modalContainer, themedStyles.modalContainer]}>
|
||||
{/* Header */}
|
||||
<View style={[styles.header, themedStyles.header]}>
|
||||
<TouchableOpacity onPress={handleCancel} style={styles.closeButton}>
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, themedStyles.title]}>
|
||||
{t("diary.addTrip")}
|
||||
</Text>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Auto Fill Section */}
|
||||
<AutoFillSection onAutoFill={handleAutoFill} />
|
||||
|
||||
{/* Ship Selector */}
|
||||
<ShipSelector
|
||||
selectedShipId={selectedShipId}
|
||||
onChange={setSelectedShipId}
|
||||
/>
|
||||
|
||||
{/* Trip Name */}
|
||||
<TripNameInput value={tripName} onChange={setTripName} />
|
||||
|
||||
{/* Fishing Gear List */}
|
||||
<FishingGearList
|
||||
items={fishingGears}
|
||||
onChange={setFishingGears}
|
||||
/>
|
||||
|
||||
{/* Trip Cost List */}
|
||||
<MaterialCostList
|
||||
items={tripCosts}
|
||||
onChange={setTripCosts}
|
||||
/>
|
||||
|
||||
{/* Trip Duration */}
|
||||
<TripDurationPicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
|
||||
{/* Port Selector */}
|
||||
<PortSelector
|
||||
departurePortId={departurePortId}
|
||||
arrivalPortId={arrivalPortId}
|
||||
onDeparturePortChange={setDeparturePortId}
|
||||
onArrivalPortChange={setArrivalPortId}
|
||||
/>
|
||||
|
||||
{/* Fishing Ground Codes */}
|
||||
<BasicInfoInput
|
||||
fishingGroundCodes={fishingGroundCodes}
|
||||
onChange={setFishingGroundCodes}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={[styles.footer, themedStyles.footer]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.cancelButton, themedStyles.cancelButton]}
|
||||
onPress={handleCancel}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.cancelButtonText, themedStyles.cancelButtonText]}>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.submitButton, themedStyles.submitButton]}
|
||||
onPress={handleSubmit}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.submitButtonText}>
|
||||
{t("diary.createTrip")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
modalContainer: {
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
maxHeight: "90%",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: -4,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
placeholder: {
|
||||
width: 32,
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
padding: 20,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
cancelButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
},
|
||||
cancelButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
submitButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
},
|
||||
submitButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#FFFFFF",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user