391 lines
10 KiB
TypeScript
391 lines
10 KiB
TypeScript
import React, { useState, useEffect, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
StyleSheet,
|
|
Platform,
|
|
Modal,
|
|
ScrollView,
|
|
TextInput,
|
|
ActivityIndicator,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useI18n } from "@/hooks/use-i18n";
|
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
import { useGroup } from "@/state/use-group";
|
|
import { usePort } from "@/state/use-ports";
|
|
import { filterPortsByProvinceCode } from "@/utils/tripDataConverters";
|
|
|
|
interface PortSelectorProps {
|
|
departurePortId: number;
|
|
arrivalPortId: number;
|
|
onDeparturePortChange: (portId: number) => void;
|
|
onArrivalPortChange: (portId: number) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
type PortType = "departure" | "arrival";
|
|
|
|
export default function PortSelector({
|
|
departurePortId,
|
|
arrivalPortId,
|
|
onDeparturePortChange,
|
|
onArrivalPortChange,
|
|
disabled = false,
|
|
}: PortSelectorProps) {
|
|
const { t } = useI18n();
|
|
const { colors } = useThemeContext();
|
|
|
|
// State from zustand stores
|
|
const { groups, getUserGroups, loading: groupsLoading } = useGroup();
|
|
const { ports, getPorts, loading: portsLoading } = usePort();
|
|
|
|
// Local state - which dropdown is open
|
|
const [activeDropdown, setActiveDropdown] = useState<PortType | null>(null);
|
|
const [searchText, setSearchText] = useState("");
|
|
|
|
// Fetch groups and ports if not available
|
|
useEffect(() => {
|
|
if (!groups) {
|
|
getUserGroups();
|
|
}
|
|
}, [groups, getUserGroups]);
|
|
|
|
useEffect(() => {
|
|
if (!ports) {
|
|
getPorts();
|
|
}
|
|
}, [ports, getPorts]);
|
|
|
|
// Filter ports by province codes from groups
|
|
const filteredPorts = useMemo(() => {
|
|
return filterPortsByProvinceCode(ports, groups);
|
|
}, [ports, groups]);
|
|
|
|
// Filter by search text
|
|
const searchFilteredPorts = useMemo(() => {
|
|
if (!searchText) return filteredPorts;
|
|
return filteredPorts.filter((port) =>
|
|
port.name?.toLowerCase().includes(searchText.toLowerCase())
|
|
);
|
|
}, [filteredPorts, searchText]);
|
|
|
|
const isLoading = groupsLoading || portsLoading;
|
|
|
|
const handleSelectPort = (portId: number) => {
|
|
if (activeDropdown === "departure") {
|
|
onDeparturePortChange(portId);
|
|
} else {
|
|
onArrivalPortChange(portId);
|
|
}
|
|
setActiveDropdown(null);
|
|
setSearchText("");
|
|
};
|
|
|
|
const openDropdown = (type: PortType) => {
|
|
setActiveDropdown(type);
|
|
setSearchText("");
|
|
};
|
|
|
|
const closeDropdown = () => {
|
|
setActiveDropdown(null);
|
|
setSearchText("");
|
|
};
|
|
|
|
// Get port name by ID
|
|
const getPortDisplayName = (portId: number): string => {
|
|
const port = filteredPorts.find((p) => p.id === portId);
|
|
return port?.name || t("diary.selectPort");
|
|
};
|
|
|
|
const currentSelectedId = activeDropdown === "departure" ? departurePortId : arrivalPortId;
|
|
|
|
const themedStyles = {
|
|
label: { color: colors.text },
|
|
portSelector: {
|
|
backgroundColor: colors.card,
|
|
borderColor: colors.border,
|
|
},
|
|
portText: { color: colors.text },
|
|
placeholder: { color: colors.textSecondary },
|
|
modalOverlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
|
modalContent: { backgroundColor: colors.card },
|
|
searchInput: {
|
|
backgroundColor: colors.backgroundSecondary,
|
|
color: colors.text,
|
|
borderColor: colors.border,
|
|
},
|
|
option: { borderBottomColor: colors.separator },
|
|
optionText: { color: colors.text },
|
|
};
|
|
|
|
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.dropdown, themedStyles.portSelector]}
|
|
onPress={disabled ? undefined : () => openDropdown("departure")}
|
|
activeOpacity={disabled ? 1 : 0.7}
|
|
disabled={disabled}
|
|
>
|
|
{isLoading ? (
|
|
<ActivityIndicator size="small" color={colors.primary} />
|
|
) : (
|
|
<>
|
|
<Text
|
|
style={[
|
|
styles.dropdownText,
|
|
themedStyles.portText,
|
|
!departurePortId && themedStyles.placeholder,
|
|
]}
|
|
>
|
|
{getPortDisplayName(departurePortId)}
|
|
</Text>
|
|
{!disabled && (
|
|
<Ionicons
|
|
name="chevron-down"
|
|
size={16}
|
|
color={colors.textSecondary}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Arrival Port */}
|
|
<View style={styles.portSection}>
|
|
<Text style={[styles.subLabel, themedStyles.placeholder]}>
|
|
{t("diary.arrivalPort")}
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={[styles.dropdown, themedStyles.portSelector]}
|
|
onPress={disabled ? undefined : () => openDropdown("arrival")}
|
|
activeOpacity={disabled ? 1 : 0.7}
|
|
disabled={disabled}
|
|
>
|
|
{isLoading ? (
|
|
<ActivityIndicator size="small" color={colors.primary} />
|
|
) : (
|
|
<>
|
|
<Text
|
|
style={[
|
|
styles.dropdownText,
|
|
themedStyles.portText,
|
|
!arrivalPortId && themedStyles.placeholder,
|
|
]}
|
|
>
|
|
{getPortDisplayName(arrivalPortId)}
|
|
</Text>
|
|
{!disabled && (
|
|
<Ionicons
|
|
name="chevron-down"
|
|
size={16}
|
|
color={colors.textSecondary}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Port Dropdown Modal - Fade style like MaterialCostList */}
|
|
<Modal
|
|
visible={activeDropdown !== null}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={closeDropdown}
|
|
>
|
|
<TouchableOpacity
|
|
style={[styles.modalOverlay, themedStyles.modalOverlay]}
|
|
activeOpacity={1}
|
|
onPress={closeDropdown}
|
|
>
|
|
<View style={[styles.modalContent, themedStyles.modalContent]}>
|
|
{/* Search Input */}
|
|
<View style={styles.searchContainer}>
|
|
<Ionicons
|
|
name="search"
|
|
size={18}
|
|
color={colors.textSecondary}
|
|
style={styles.searchIcon}
|
|
/>
|
|
<TextInput
|
|
style={[styles.searchInput, themedStyles.searchInput]}
|
|
placeholder={t("diary.searchPort")}
|
|
placeholderTextColor={colors.textSecondary}
|
|
value={searchText}
|
|
onChangeText={setSearchText}
|
|
/>
|
|
</View>
|
|
|
|
{/* Port List */}
|
|
<ScrollView style={styles.optionsList}>
|
|
{isLoading ? (
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="small" color={colors.primary} />
|
|
</View>
|
|
) : searchFilteredPorts.length === 0 ? (
|
|
<View style={styles.emptyContainer}>
|
|
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
|
|
{t("diary.noPortsFound")}
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
searchFilteredPorts.map((port) => (
|
|
<TouchableOpacity
|
|
key={port.id}
|
|
style={[styles.option, themedStyles.option]}
|
|
onPress={() => handleSelectPort(port.id || 0)}
|
|
>
|
|
<Text style={[styles.optionText, themedStyles.optionText]}>
|
|
{port.name}
|
|
</Text>
|
|
{currentSelectedId === port.id && (
|
|
<Ionicons name="checkmark" size={20} color={colors.primary} />
|
|
)}
|
|
</TouchableOpacity>
|
|
))
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</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",
|
|
}),
|
|
},
|
|
portContainer: {
|
|
flexDirection: "column",
|
|
gap: 12,
|
|
},
|
|
portSection: {
|
|
flex: 1,
|
|
},
|
|
dropdown: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
borderWidth: 1,
|
|
borderRadius: 8,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
minHeight: 44,
|
|
},
|
|
dropdownText: {
|
|
fontSize: 15,
|
|
flex: 1,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
// Modal styles - same as MaterialCostList
|
|
modalOverlay: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
modalContent: {
|
|
borderRadius: 12,
|
|
width: "85%",
|
|
maxHeight: "60%",
|
|
overflow: "hidden",
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 8,
|
|
elevation: 8,
|
|
},
|
|
searchContainer: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: "#E5E5E7",
|
|
},
|
|
searchIcon: {
|
|
marginRight: 8,
|
|
},
|
|
searchInput: {
|
|
flex: 1,
|
|
fontSize: 15,
|
|
paddingVertical: 6,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
optionsList: {
|
|
maxHeight: 300,
|
|
},
|
|
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",
|
|
}),
|
|
},
|
|
loadingContainer: {
|
|
padding: 20,
|
|
alignItems: "center",
|
|
},
|
|
emptyContainer: {
|
|
padding: 20,
|
|
alignItems: "center",
|
|
},
|
|
emptyText: {
|
|
fontSize: 14,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
});
|