Files
sgw-owner-app/components/diary/TripFormModal/PortSelector.tsx

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",
}),
},
});