Files
sgw-owner-app/components/ShipSearchForm.tsx
2025-12-10 19:49:54 +07:00

528 lines
16 KiB
TypeScript

import { Colors } from "@/config";
import { ColorScheme, useTheme } from "@/hooks/use-theme-context";
import { useShipGroups } from "@/state/use-ship-groups";
import { useShipTypes } from "@/state/use-ship-types";
import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
Animated,
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import Select from "./Select";
import Slider from "./Slider";
interface ShipSearchFormProps {
initialValues?: Partial<SearchShipResponse>;
isOpen: boolean;
onClose: () => void;
onSubmit?: (data: SearchShipResponse) => void;
}
export interface SearchShipResponse {
ship_name: string;
ship_length: [number, number];
reg_number: string;
ship_power: [number, number];
ship_type: string | number;
alarm_list: string;
ship_group_id: string;
group_id: string;
}
const ShipSearchForm = (props: ShipSearchFormProps) => {
const { colors, colorScheme } = useTheme();
const styles = useMemo(
() => createStyles(colors, colorScheme),
[colors, colorScheme]
);
const { shipTypes, getShipTypes } = useShipTypes();
const [slideAnim] = useState(new Animated.Value(0));
const { shipGroups, getShipGroups } = useShipGroups();
const { control, handleSubmit, reset, watch } = useForm<SearchShipResponse>({
defaultValues: {
ship_name: props.initialValues?.ship_name || "",
reg_number: props.initialValues?.reg_number || "",
ship_length: [0, 100],
ship_power: [0, 100000],
ship_type: props.initialValues?.ship_type || "",
alarm_list: props.initialValues?.alarm_list || "",
ship_group_id: props.initialValues?.ship_group_id || "",
group_id: props.initialValues?.group_id || "",
},
});
const shipLengthValue = watch("ship_length");
const shipPowerValue = watch("ship_power");
useEffect(() => {
if (shipTypes.length === 0) {
getShipTypes();
}
}, [shipTypes]);
useEffect(() => {
if (shipGroups === null) {
getShipGroups();
}
}, [props.isOpen]);
useEffect(() => {
if (props.isOpen) {
Animated.spring(slideAnim, {
toValue: 1,
useNativeDriver: true,
tension: 50,
friction: 8,
}).start();
} else {
slideAnim.setValue(0);
}
}, [props.isOpen]);
useEffect(() => {
if (props.initialValues) {
reset({
ship_name: props.initialValues.ship_name || "",
reg_number: props.initialValues.reg_number || "",
ship_length: [
props.initialValues.ship_length?.[0] || 0,
props.initialValues.ship_length?.[1] || 100,
],
ship_power: [
props.initialValues.ship_power?.[0] || 0,
props.initialValues.ship_power?.[1] || 100000,
],
ship_type: props.initialValues.ship_type || "",
alarm_list: props.initialValues.alarm_list || "",
ship_group_id: props.initialValues.ship_group_id || "",
group_id: props.initialValues.group_id || "",
});
}
}, [props.initialValues]);
const alarmListLabel = [
{
label: "Tiếp cận vùng hạn chế",
value: "50:10",
},
{
label: "Đã ra (vào) vùng hạn chế)",
value: "50:11",
},
{
label: "Đang đánh bắt trong vùng hạn chế",
value: "50:12",
},
];
const onSubmitForm = (data: SearchShipResponse) => {
props.onSubmit?.(data);
console.log("Data: ", data);
// props.onClose();
};
const onReset = () => {
reset({
ship_name: "",
reg_number: "",
ship_length: [0, 100],
ship_power: [0, 100000],
ship_type: "",
alarm_list: "",
ship_group_id: "",
group_id: "",
});
};
const translateY = slideAnim.interpolate({
inputRange: [0, 1],
outputRange: [600, 0],
});
return (
<Modal
animationType="fade"
transparent={true}
visible={props.isOpen}
onRequestClose={props.onClose}
>
<GestureHandlerRootView style={{ flex: 1 }}>
<Pressable style={styles.backdrop} onPress={props.onClose}>
<Animated.View
style={[styles.modalContent, { transform: [{ translateY }] }]}
onStartShouldSetResponder={() => true}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.dragIndicator} />
<Text style={[styles.headerTitle, { color: colors.text }]}>
Tìm kiếm tàu
</Text>
<TouchableOpacity
onPress={props.onClose}
style={styles.closeButton}
>
<Text style={[styles.closeButtonText, { color: colors.text }]}>
</Text>
</TouchableOpacity>
</View>
{/* Form Content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.formSection}>
{/* Tên tàu */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.text }]}>
Tên tàu
</Text>
<Controller
control={control}
name="ship_name"
render={({ field: { onChange, value } }) => (
<TextInput
placeholder="Hoàng Sa 001"
placeholderTextColor={colors.textSecondary}
style={[
styles.input,
{
borderColor: colors.border,
backgroundColor: colors.surface,
color: colors.text,
},
]}
value={value}
onChangeText={onChange}
/>
)}
/>
</View>
{/* Số đăng ký */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.text }]}>
Số đăng
</Text>
<Controller
control={control}
name="reg_number"
render={({ field: { onChange, value } }) => (
<TextInput
placeholder="VN-00001"
placeholderTextColor={colors.textSecondary}
style={[
styles.input,
{
borderColor: colors.border,
backgroundColor: colors.surface,
color: colors.text,
},
]}
value={value}
onChangeText={onChange}
/>
)}
/>
</View>
{/* Chiều dài */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.text }]}>
Chiều dài ({shipLengthValue[0]}m - {shipLengthValue[1]}m)
</Text>
<Controller
control={control}
name="ship_length"
render={({ field: { onChange, value } }) => (
<View style={styles.sliderContainer}>
<Slider
range={true}
min={0}
max={100}
step={1}
value={value}
marks={{
0: "0m",
50: "50m",
100: "100m",
}}
onValueChange={(val) =>
onChange(val as [number, number])
}
activeColor={colors.primary}
/>
</View>
)}
/>
</View>
{/* Công suất */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.text }]}>
Công suất ({shipPowerValue[0]}kW - {shipPowerValue[1]}kW)
</Text>
<Controller
control={control}
name="ship_power"
render={({ field: { onChange, value } }) => (
<View style={styles.sliderContainer}>
<Slider
range={true}
min={0}
max={100000}
step={1000}
value={value}
marks={{
0: "0kW",
50000: "50,000kW",
100000: "100,000kW",
}}
onValueChange={(val) =>
onChange(val as [number, number])
}
activeColor={colors.primary}
/>
</View>
)}
/>
</View>
{/* Loại tàu */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.text }]}>
Loại tàu
</Text>
<Controller
control={control}
name="ship_type"
render={({ field: { onChange, value } }) => (
<Select
options={shipTypes.map((type) => ({
label: type.name || "",
value: type.id || 0,
}))}
placeholder="Chọn loại tàu"
mode="multiple"
value={value}
onChange={onChange}
/>
)}
/>
</View>
{/* Cảnh báo */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.text }]}>
Cảnh báo
</Text>
<Controller
control={control}
name="alarm_list"
render={({ field: { onChange, value } }) => (
<Select
options={alarmListLabel.map((type) => ({
label: type.label || "",
value: type.value || "",
}))}
placeholder="Chọn loại cảnh báo"
mode="multiple"
value={value}
onChange={onChange}
/>
)}
/>
</View>
{/* Đội tàu */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.text }]}>
Đi tàu
</Text>
<Controller
control={control}
name="ship_group_id"
render={({ field: { onChange, value } }) => (
<Select
options={
shipGroups?.map((group) => ({
label: group.name || "",
value: group.id || "",
})) || []
}
placeholder="Chọn đội tàu"
mode="multiple"
value={value}
onChange={onChange}
/>
)}
/>
</View>
<View className="h-12"></View>
</View>
</ScrollView>
{/* Action Buttons */}
<View
style={[styles.actionButtons, { borderTopColor: colors.border }]}
>
<TouchableOpacity
style={[styles.resetButton, { borderColor: colors.border }]}
onPress={onReset}
>
<Text style={[styles.resetButtonText, { color: colors.text }]}>
Đt lại
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.submitButton,
{ backgroundColor: colors.primary },
]}
onPress={handleSubmit(onSubmitForm)}
>
<Text style={styles.submitButtonText}>Tìm kiếm</Text>
</TouchableOpacity>
</View>
</Animated.View>
</Pressable>
</GestureHandlerRootView>
</Modal>
);
};
const createStyles = (colors: typeof Colors.light, scheme: ColorScheme) =>
StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
modalContent: {
height: "85%",
backgroundColor: colors.background,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: -4,
},
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 10,
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 16,
paddingHorizontal: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
position: "relative",
},
dragIndicator: {
position: "absolute",
top: 8,
width: 40,
height: 4,
backgroundColor: colors.border,
borderRadius: 2,
},
headerTitle: {
fontSize: 18,
fontWeight: "700",
textAlign: "center",
},
closeButton: {
position: "absolute",
right: 16,
top: 16,
width: 32,
height: 32,
alignItems: "center",
justifyContent: "center",
borderRadius: 16,
},
closeButtonText: {
fontSize: 20,
fontWeight: "300",
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingBottom: 20,
},
formSection: {
paddingHorizontal: 20,
paddingTop: 20,
},
fieldGroup: {
marginBottom: 24,
},
label: {
fontSize: 15,
fontWeight: "600",
marginBottom: 10,
},
input: {
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 15,
},
sliderContainer: {
paddingHorizontal: 4,
paddingTop: 8,
},
actionButtons: {
flexDirection: "row",
paddingHorizontal: 20,
paddingVertical: 16,
gap: 12,
borderTopWidth: 1,
},
resetButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 12,
borderWidth: 1,
alignItems: "center",
justifyContent: "center",
},
resetButtonText: {
fontSize: 16,
fontWeight: "600",
},
submitButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
},
submitButtonText: {
color: "#ffffff",
fontSize: 16,
fontWeight: "600",
},
});
export default ShipSearchForm;