Khởi tạo ban đầu

This commit is contained in:
Tran Anh Tuan
2025-11-28 16:59:57 +07:00
parent 2911be97b2
commit 4ba46a7df2
131 changed files with 28066 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useI18n } from "@/hooks/use-i18n";
import { useTrip } from "@/state/use-trip";
import React, { useMemo, useRef, useState } from "react";
import { Animated, Text, TouchableOpacity, View } from "react-native";
import CrewDetailModal from "./modal/CrewDetailModal";
import { useAppTheme } from "@/hooks/use-app-theme";
import { useThemeContext } from "@/hooks/use-theme-context";
import { createTableStyles } from "./ThemedTable";
const CrewListTable: React.FC = () => {
const [collapsed, setCollapsed] = useState(true);
const [contentHeight, setContentHeight] = useState<number>(0);
const animatedHeight = useRef(new Animated.Value(0)).current;
const [modalVisible, setModalVisible] = useState(false);
const [selectedCrew, setSelectedCrew] = useState<Model.TripCrews | null>(
null
);
const { t } = useI18n();
const { colorScheme } = useAppTheme();
const { colors } = useThemeContext();
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
const { trip } = useTrip();
const data: Model.TripCrews[] = trip?.crews ?? [];
const tongThanhVien = data.length;
const handleToggle = () => {
const toValue = collapsed ? contentHeight : 0;
Animated.timing(animatedHeight, {
toValue,
duration: 300,
useNativeDriver: false,
}).start();
setCollapsed((prev) => !prev);
};
const handleCrewPress = (crewId: string) => {
const crew = data.find((item) => item.Person.personal_id === crewId);
if (crew) {
setSelectedCrew(crew);
setModalVisible(true);
}
};
const handleCloseModal = () => {
setModalVisible(false);
setSelectedCrew(null);
};
return (
<View style={styles.container}>
{/* Header toggle */}
<TouchableOpacity
activeOpacity={0.7}
onPress={handleToggle}
style={styles.headerRow}
>
<Text style={styles.title}>{t("trip.crewList.title")}</Text>
{collapsed && (
<Text style={styles.totalCollapsed}>{tongThanhVien}</Text>
)}
<IconSymbol
name={collapsed ? "chevron.down" : "chevron.up"}
size={16}
color={colors.icon}
/>
</TouchableOpacity>
{/* Nội dung ẩn để đo chiều cao */}
<View
style={{ position: "absolute", opacity: 0, zIndex: -1000 }}
onLayout={(event) => {
const height = event.nativeEvent.layout.height;
if (height > 0 && contentHeight === 0) {
setContentHeight(height);
}
}}
>
{/* Header */}
<View style={[styles.row, styles.tableHeader]}>
<View style={styles.cellWrapper}>
<Text style={[styles.cell, styles.headerText]}>
{t("trip.crewList.nameHeader")}
</Text>
</View>
<Text style={[styles.cell, styles.right, styles.headerText]}>
{t("trip.crewList.roleHeader")}
</Text>
</View>
{/* Body */}
{data.map((item) => (
<View key={item.Person.personal_id} style={styles.row}>
<TouchableOpacity
style={styles.cellWrapper}
onPress={() => handleCrewPress(item.Person.personal_id)}
>
<Text style={[styles.cell, styles.linkText]}>
{item.Person.name}
</Text>
</TouchableOpacity>
<Text style={[styles.cell, styles.right]}>{item.role}</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.footerText]}>
{t("trip.crewList.totalLabel")}
</Text>
<Text style={[styles.cell, styles.footerTotal]}>{tongThanhVien}</Text>
</View>
</View>
{/* Bảng hiển thị với animation */}
<Animated.View style={{ height: animatedHeight, overflow: "hidden" }}>
{/* Header */}
<View style={[styles.row, styles.tableHeader]}>
<View style={styles.cellWrapper}>
<Text style={[styles.cell, styles.headerText]}>
{t("trip.crewList.nameHeader")}
</Text>
</View>
<Text style={[styles.cell, styles.right, styles.headerText]}>
{t("trip.crewList.roleHeader")}
</Text>
</View>
{/* Body */}
{data.map((item) => (
<View key={item.Person.personal_id} style={styles.row}>
<TouchableOpacity
style={styles.cellWrapper}
onPress={() => handleCrewPress(item.Person.personal_id)}
>
<Text style={[styles.cell, styles.linkText]}>
{item.Person.name}
</Text>
</TouchableOpacity>
<Text style={[styles.cell, styles.right]}>{item.role}</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.footerText]}>
{t("trip.crewList.totalLabel")}
</Text>
<Text style={[styles.cell, styles.footerTotal]}>{tongThanhVien}</Text>
</View>
</Animated.View>
{/* Modal chi tiết thuyền viên */}
<CrewDetailModal
visible={modalVisible}
onClose={handleCloseModal}
crewData={selectedCrew}
/>
</View>
);
};
export default CrewListTable;

View File

@@ -0,0 +1,123 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useI18n } from "@/hooks/use-i18n";
import { useTrip } from "@/state/use-trip";
import React, { useMemo, useRef, useState } from "react";
import { Animated, Text, TouchableOpacity, View } from "react-native";
import { useAppTheme } from "@/hooks/use-app-theme";
import { useThemeContext } from "@/hooks/use-theme-context";
import { createTableStyles } from "./ThemedTable";
const FishingToolsTable: React.FC = () => {
const [collapsed, setCollapsed] = useState(true);
const [contentHeight, setContentHeight] = useState<number>(0);
const animatedHeight = useRef(new Animated.Value(0)).current;
const { t } = useI18n();
const { colorScheme } = useAppTheme();
const { colors } = useThemeContext();
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
const { trip } = useTrip();
const data: Model.FishingGear[] = trip?.fishing_gears ?? [];
const tongSoLuong = data.reduce((sum, item) => sum + Number(item.number), 0);
const handleToggle = () => {
const toValue = collapsed ? contentHeight : 0;
Animated.timing(animatedHeight, {
toValue,
duration: 300,
useNativeDriver: false,
}).start();
setCollapsed((prev) => !prev);
};
return (
<View style={styles.container}>
{/* Header / Toggle */}
<TouchableOpacity
activeOpacity={0.7}
onPress={handleToggle}
style={styles.headerRow}
>
<Text style={styles.title}>{t("trip.fishingTools.title")}</Text>
{collapsed && <Text style={styles.totalCollapsed}>{tongSoLuong}</Text>}
<IconSymbol
name={collapsed ? "chevron.down" : "chevron.up"}
size={16}
color={colors.icon}
/>
</TouchableOpacity>
{/* Nội dung ẩn để đo chiều cao */}
<View
style={{ position: "absolute", opacity: 0, zIndex: -1000 }}
onLayout={(event) => {
const height = event.nativeEvent.layout.height;
if (height > 0 && contentHeight === 0) {
setContentHeight(height);
}
}}
>
{/* Table Header */}
<View style={[styles.row, styles.tableHeader]}>
<Text style={[styles.cell, styles.left, styles.headerText]}>
{t("trip.fishingTools.nameHeader")}
</Text>
<Text style={[styles.cell, styles.right, styles.headerText]}>
{t("trip.fishingTools.quantityHeader")}
</Text>
</View>
{/* Body */}
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.name}</Text>
<Text style={[styles.cell, styles.right]}>{item.number}</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.left, styles.footerText]}>
{t("trip.fishingTools.totalLabel")}
</Text>
<Text style={[styles.cell, styles.right, styles.footerTotal]}>
{tongSoLuong}
</Text>
</View>
</View>
{/* Nội dung mở/đóng */}
<Animated.View style={{ height: animatedHeight, overflow: "hidden" }}>
{/* Table Header */}
<View style={[styles.row, styles.tableHeader]}>
<Text style={[styles.cell, styles.left, styles.headerText]}>
{t("trip.fishingTools.nameHeader")}
</Text>
<Text style={[styles.cell, styles.right, styles.headerText]}>
{t("trip.fishingTools.quantityHeader")}
</Text>
</View>
{/* Body */}
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.name}</Text>
<Text style={[styles.cell, styles.right]}>{item.number}</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.left, styles.footerText]}>
{t("trip.fishingTools.totalLabel")}
</Text>
<Text style={[styles.cell, styles.right, styles.footerTotal]}>
{tongSoLuong}
</Text>
</View>
</Animated.View>
</View>
);
};
export default FishingToolsTable;

View File

@@ -0,0 +1,197 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useI18n } from "@/hooks/use-i18n";
import { useFishes } from "@/state/use-fish";
import { useTrip } from "@/state/use-trip";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Animated, Text, TouchableOpacity, View } from "react-native";
import CreateOrUpdateHaulModal from "./modal/CreateOrUpdateHaulModal";
import { useAppTheme } from "@/hooks/use-app-theme";
import { useThemeContext } from "@/hooks/use-theme-context";
import { createTableStyles } from "./ThemedTable";
const NetListTable: React.FC = () => {
const [collapsed, setCollapsed] = useState(true);
const [contentHeight, setContentHeight] = useState<number>(0);
const animatedHeight = useRef(new Animated.Value(0)).current;
const [modalVisible, setModalVisible] = useState(false);
const [selectedNet, setSelectedNet] = useState<Model.FishingLog | null>(null);
const { t } = useI18n();
const { colorScheme } = useAppTheme();
const { colors } = useThemeContext();
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
const { trip } = useTrip();
const { fishSpecies, getFishSpecies } = useFishes();
useEffect(() => {
getFishSpecies();
}, []);
const handleToggle = () => {
const toValue = collapsed ? contentHeight : 0;
Animated.timing(animatedHeight, {
toValue,
duration: 300,
useNativeDriver: false,
}).start();
setCollapsed((prev) => !prev);
};
const handleStatusPress = (id: string) => {
const net = trip?.fishing_logs?.find((item) => item.fishing_log_id === id);
if (net) {
setSelectedNet(net);
setModalVisible(true);
}
};
return (
<View style={styles.container}>
{/* Header toggle */}
<TouchableOpacity
activeOpacity={0.7}
onPress={handleToggle}
style={styles.headerRow}
>
<Text style={styles.title}>{t("trip.netList.title")}</Text>
{collapsed && (
<Text style={styles.totalCollapsed}>
{trip?.fishing_logs?.length}
</Text>
)}
<IconSymbol
name={collapsed ? "chevron.down" : "chevron.up"}
size={16}
color={colors.icon}
/>
</TouchableOpacity>
{/* Nội dung ẩn để đo chiều cao */}
<View
style={{ position: "absolute", opacity: 0, zIndex: -1000 }}
onLayout={(event) => {
const height = event.nativeEvent.layout.height;
// Update measured content height whenever it actually changes.
if (height > 0 && height !== contentHeight) {
setContentHeight(height);
// If the panel is currently expanded, animate to the new height so
// newly added/removed rows become visible immediately.
if (!collapsed) {
Animated.timing(animatedHeight, {
toValue: height,
duration: 200,
useNativeDriver: false,
}).start();
}
}
}}
>
{/* Header */}
<View style={[styles.row, styles.tableHeader]}>
<Text style={[styles.sttCell, styles.headerText]}>
{t("trip.netList.sttHeader")}
</Text>
<Text style={[styles.cell, styles.headerText]}>
{t("trip.netList.statusHeader")}
</Text>
</View>
{/* Body */}
{trip?.fishing_logs?.map((item, index) => (
<View key={item.fishing_log_id} style={styles.row}>
{/* Cột STT */}
<Text style={styles.sttCell}>
{t("trip.netList.haulPrefix")} {index + 1}
</Text>
{/* Cột Trạng thái */}
<View style={[styles.cell, styles.statusContainer]}>
<View
style={[
styles.statusDot,
{ backgroundColor: item.status ? "#2ecc71" : "#FFD600" },
]}
/>
<TouchableOpacity
onPress={() => handleStatusPress(item.fishing_log_id)}
>
<Text style={styles.statusText}>
{item.status
? t("trip.netList.completed")
: t("trip.netList.pending")}
</Text>
</TouchableOpacity>
</View>
</View>
))}
</View>
{/* Bảng hiển thị với animation */}
<Animated.View style={{ height: animatedHeight, overflow: "hidden" }}>
{/* Header */}
<View style={[styles.row, styles.tableHeader]}>
<Text style={[styles.sttCell, styles.headerText]}>
{t("trip.netList.sttHeader")}
</Text>
<Text style={[styles.cell, styles.headerText]}>
{t("trip.netList.statusHeader")}
</Text>
</View>
{/* Body */}
{trip?.fishing_logs?.map((item, index) => (
<View key={item.fishing_log_id} style={styles.row}>
{/* Cột STT */}
<Text style={styles.sttCell}>
{t("trip.netList.haulPrefix")} {index + 1}
</Text>
{/* Cột Trạng thái */}
<View style={[styles.cell, styles.statusContainer]}>
<View
style={[
styles.statusDot,
{ backgroundColor: item.status ? "#2ecc71" : "#FFD600" },
]}
/>
<TouchableOpacity
onPress={() => handleStatusPress(item.fishing_log_id)}
>
<Text style={styles.statusText}>
{item.status
? t("trip.netList.completed")
: t("trip.netList.pending")}
</Text>
</TouchableOpacity>
</View>
</View>
))}
</Animated.View>
<CreateOrUpdateHaulModal
isVisible={modalVisible}
onClose={() => {
console.log("OnCLose");
setModalVisible(false);
}}
fishingLog={selectedNet}
fishingLogIndex={
selectedNet
? trip!.fishing_logs!.findIndex(
(item) => item.fishing_log_id === selectedNet.fishing_log_id
) + 1
: undefined
}
/>
{/* Modal chi tiết */}
{/* <NetDetailModal
visible={modalVisible}
onClose={() => {
console.log("OnCLose");
setModalVisible(false);
}}
netData={selectedNet}
/> */}
</View>
);
};
export default NetListTable;

View File

@@ -0,0 +1,29 @@
/**
* Wrapper component to easily apply theme-aware table styles
*/
import React, { useMemo } from "react";
import { View, ViewProps } from "react-native";
import { useAppTheme } from "@/hooks/use-app-theme";
import { createTableStyles } from "./style/createTableStyles";
interface ThemedTableProps extends ViewProps {
children: React.ReactNode;
}
export function ThemedTable({ style, children, ...props }: ThemedTableProps) {
const { colorScheme } = useAppTheme();
const tableStyles = useMemo(
() => createTableStyles(colorScheme),
[colorScheme]
);
return (
<View style={[tableStyles.container, style]} {...props}>
{children}
</View>
);
}
export { createTableStyles };
export type { TableStyles } from "./style/createTableStyles";

View File

@@ -0,0 +1,177 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useI18n } from "@/hooks/use-i18n";
import { useAppTheme } from "@/hooks/use-app-theme";
import { useTrip } from "@/state/use-trip";
import { createTableStyles } from "./style/createTableStyles";
import TripCostDetailModal from "./modal/TripCostDetailModal";
import React, { useRef, useState, useMemo } from "react";
import { Animated, Text, TouchableOpacity, View } from "react-native";
import { useThemeContext } from "@/hooks/use-theme-context";
const TripCostTable: React.FC = () => {
const [collapsed, setCollapsed] = useState(true);
const [contentHeight, setContentHeight] = useState<number>(0);
const [modalVisible, setModalVisible] = useState(false);
const animatedHeight = useRef(new Animated.Value(0)).current;
const { t } = useI18n();
const { colorScheme } = useAppTheme();
const { colors } = useThemeContext();
const { trip } = useTrip();
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
const data: Model.TripCost[] = trip?.trip_cost ?? [];
const tongCong = data.reduce((sum, item) => sum + item.total_cost, 0);
const handleToggle = () => {
const toValue = collapsed ? contentHeight : 0;
Animated.timing(animatedHeight, {
toValue,
duration: 300,
useNativeDriver: false,
}).start();
setCollapsed((prev) => !prev);
};
const handleViewDetail = () => {
setModalVisible(true);
};
const handleCloseModal = () => {
setModalVisible(false);
};
return (
<View style={styles.container}>
<TouchableOpacity
activeOpacity={0.7}
onPress={handleToggle}
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
// marginBottom: 12,
}}
>
<Text style={styles.title}>{t("trip.costTable.title")}</Text>
{collapsed && (
<Text style={[styles.totalCollapsed]}>
{tongCong.toLocaleString()}
</Text>
)}
<IconSymbol
name={collapsed ? "chevron.down" : "chevron.up"}
size={15}
color={colors.icon}
/>
</TouchableOpacity>
{/* Nội dung ẩn để đo chiều cao */}
<View
style={{ position: "absolute", opacity: 0, zIndex: -1000 }}
onLayout={(event) => {
const height = event.nativeEvent.layout.height;
if (height > 0 && contentHeight === 0) {
setContentHeight(height);
}
}}
>
{/* Header */}
<View style={[styles.row, styles.header]}>
<Text style={[styles.cell, styles.left, styles.headerText]}>
{t("trip.costTable.typeHeader")}
</Text>
<Text style={[styles.cell, styles.headerText]}>
{t("trip.costTable.totalCostHeader")}
</Text>
</View>
{/* Body */}
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.type}</Text>
<Text style={[styles.cell, styles.right]}>
{item.total_cost.toLocaleString()}
</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.left, styles.footerText]}>
{t("trip.costTable.totalLabel")}
</Text>
<Text style={[styles.cell, styles.total]}>
{tongCong.toLocaleString()}
</Text>
</View>
{/* View Detail Button */}
{data.length > 0 && (
<TouchableOpacity
style={styles.viewDetailButton}
onPress={handleViewDetail}
>
<Text style={styles.viewDetailText}>
{t("trip.costTable.viewDetail")}
</Text>
</TouchableOpacity>
)}
</View>
<Animated.View style={{ height: animatedHeight, overflow: "hidden" }}>
{/* Header */}
<View style={[styles.row, styles.header]}>
<Text style={[styles.cell, styles.left, styles.headerText]}>
{t("trip.costTable.typeHeader")}
</Text>
<Text style={[styles.cell, styles.headerText]}>
{t("trip.costTable.totalCostHeader")}
</Text>
</View>
{/* Body */}
{data.map((item, index) => (
<View key={index} style={styles.row}>
<Text style={[styles.cell, styles.left]}>{item.type}</Text>
<Text style={[styles.cell, styles.right]}>
{item.total_cost.toLocaleString()}
</Text>
</View>
))}
{/* Footer */}
<View style={[styles.row]}>
<Text style={[styles.cell, styles.left, styles.footerText]}>
{t("trip.costTable.totalLabel")}
</Text>
<Text style={[styles.cell, styles.total]}>
{tongCong.toLocaleString()}
</Text>
</View>
{/* View Detail Button */}
{data.length > 0 && (
<TouchableOpacity
style={styles.viewDetailButton}
onPress={handleViewDetail}
>
<Text style={styles.viewDetailText}>
{t("trip.costTable.viewDetail")}
</Text>
</TouchableOpacity>
)}
</Animated.View>
{/* Modal */}
<TripCostDetailModal
visible={modalVisible}
onClose={handleCloseModal}
data={data}
/>
</View>
);
};
export default TripCostTable;

View File

@@ -0,0 +1,587 @@
import Select from "@/components/Select";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { queryGpsData } from "@/controller/DeviceController";
import { queryUpdateFishingLogs } from "@/controller/TripController";
import { useI18n } from "@/hooks/use-i18n";
import { useThemeContext } from "@/hooks/use-theme-context";
import { showErrorToast, showSuccessToast } from "@/services/toast_service";
import { useFishes } from "@/state/use-fish";
import { useTrip } from "@/state/use-trip";
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import {
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { z } from "zod";
import { InfoSection } from "./components/InfoSection";
import { createStyles } from "./style/CreateOrUpdateHaulModal.styles";
interface CreateOrUpdateHaulModalProps {
isVisible: boolean;
onClose: () => void;
fishingLog?: Model.FishingLog | null;
fishingLogIndex?: number;
}
const UNITS = ["con", "kg", "tấn"] as const;
type Unit = (typeof UNITS)[number];
const UNITS_OPTIONS = UNITS.map((unit) => ({
label: unit,
value: unit.toString(),
}));
const SIZE_UNITS = ["cm", "m"] as const;
type SizeUnit = (typeof SIZE_UNITS)[number];
const SIZE_UNITS_OPTIONS = SIZE_UNITS.map((unit) => ({
label: unit,
value: unit,
}));
// Zod schema cho 1 dòng cá
const fishItemSchema = z.object({
id: z.number().min(1, ""),
quantity: z.number({ invalid_type_error: "" }).positive(""),
unit: z.enum(UNITS, { required_error: "" }),
size: z.number({ invalid_type_error: "" }).positive("").optional(),
sizeUnit: z.enum(SIZE_UNITS),
});
// Schema tổng: mảng các item
const formSchema = z.object({
fish: z.array(fishItemSchema).min(1, ""),
});
type FormValues = z.infer<typeof formSchema>;
const defaultItem = (): FormValues["fish"][number] => ({
id: -1,
quantity: 1,
unit: "con",
size: undefined,
sizeUnit: "cm",
});
const CreateOrUpdateHaulModal: React.FC<CreateOrUpdateHaulModalProps> = ({
isVisible,
onClose,
fishingLog,
fishingLogIndex,
}) => {
const { colors } = useThemeContext();
const styles = React.useMemo(() => createStyles(colors), [colors]);
const { t } = useI18n();
const [isCreateMode, setIsCreateMode] = React.useState(!fishingLog?.info);
const [isEditing, setIsEditing] = React.useState(false);
const [expandedFishIndices, setExpandedFishIndices] = React.useState<
number[]
>([]);
const { trip, getTrip } = useTrip();
const { control, handleSubmit, formState, watch, reset } =
useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
fish: [defaultItem()],
},
mode: "onSubmit",
});
const { fishSpecies, getFishSpecies } = useFishes();
const { errors } = formState;
if (!fishSpecies) {
getFishSpecies();
}
const { fields, append, remove } = useFieldArray({
control,
name: "fish",
keyName: "_id", // tránh đụng key
});
const handleToggleExpanded = (index: number) => {
setExpandedFishIndices((prev) =>
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
);
};
const onSubmit = async (values: FormValues) => {
// Ensure species list is available so we can populate name/rarity
if (!fishSpecies || fishSpecies.length === 0) {
showErrorToast(t("trip.createHaulModal.fishListNotReady"));
return;
}
// Helper to map form rows -> API info entries (single place)
const buildInfo = (rows: FormValues["fish"]) =>
rows.map((item) => {
const meta = fishSpecies.find((f) => f.id === item.id);
return {
fish_species_id: item.id,
fish_name: meta?.name ?? "",
catch_number: item.quantity,
catch_unit: item.unit,
fish_size: item.size,
fish_rarity: meta?.rarity_level ?? null,
fish_condition: "",
gear_usage: "",
} as unknown;
});
try {
const gpsResp = await queryGpsData();
if (!gpsResp.data) {
showErrorToast(t("trip.createHaulModal.gpsError"));
return;
}
const gpsData = gpsResp.data;
const info = buildInfo(values.fish) as any;
// Base payload fields shared between create and update
const base: Partial<Model.FishingLog> = {
fishing_log_id: fishingLog?.fishing_log_id || "",
trip_id: trip?.id || "",
start_at: fishingLog?.start_at!,
start_lat: fishingLog?.start_lat!,
start_lon: fishingLog?.start_lon!,
weather_description:
fishingLog?.weather_description || "Nắng đẹp, Trời nhiều mây",
info,
sync: true,
};
// Build final payload depending on create vs update
const body: Model.FishingLog =
fishingLog?.status == 0
? ({
...base,
haul_lat: gpsData.lat,
haul_lon: gpsData.lon,
end_at: new Date(),
status: 1,
} as Model.FishingLog)
: ({
...base,
haul_lat: fishingLog?.haul_lat,
haul_lon: fishingLog?.haul_lon,
end_at: fishingLog?.end_at,
status: fishingLog?.status,
} as Model.FishingLog);
// console.log("Body: ", body);
const resp = await queryUpdateFishingLogs(body);
if (resp?.status === 200) {
showSuccessToast(
fishingLog?.fishing_log_id == null
? t("trip.createHaulModal.addSuccess")
: t("trip.createHaulModal.updateSuccess")
);
getTrip();
onClose();
} else {
showErrorToast(
fishingLog?.fishing_log_id == null
? t("trip.createHaulModal.addError")
: t("trip.createHaulModal.updateError")
);
}
} catch (err) {
console.error("onSubmit error:", err);
showErrorToast(t("trip.createHaulModal.validationError"));
}
};
// Initialize / reset form when modal visibility or haulData changes
React.useEffect(() => {
if (!isVisible) {
// when modal closed, clear form to default
reset({ fish: [defaultItem()] });
setIsCreateMode(true);
setIsEditing(false);
setExpandedFishIndices([]);
return;
}
// when modal opened, populate based on fishingLog
if (fishingLog?.info === null) {
// explicit null -> start with a single default item
reset({ fish: [defaultItem()] });
setIsCreateMode(true);
setIsEditing(true); // allow editing for new haul
setExpandedFishIndices([0]); // expand first item
} else if (Array.isArray(fishingLog?.info) && fishingLog?.info.length > 0) {
// map FishingLogInfo -> form rows
const mapped = fishingLog.info.map((h) => ({
id: h.fish_species_id ?? -1,
quantity: (h.catch_number as number) ?? 1,
unit: (h.catch_unit as Unit) ?? (defaultItem().unit as Unit),
size: (h.fish_size as number) ?? undefined,
sizeUnit: "cm" as SizeUnit,
}));
reset({ fish: mapped as any });
setIsCreateMode(false);
setIsEditing(false); // view mode by default
setExpandedFishIndices([]); // all collapsed
} else {
// undefined or empty array -> default
reset({ fish: [defaultItem()] });
setIsCreateMode(true);
setIsEditing(true); // allow editing for new haul
setExpandedFishIndices([0]); // expand first item
}
}, [isVisible, fishingLog?.info, reset]);
const renderRow = (item: any, index: number) => {
const isExpanded = expandedFishIndices.includes(index);
// Give expanded card highest zIndex, others get decreasing zIndex based on position
const cardZIndex = isExpanded ? 1000 : 100 - index;
return (
<View key={item._id} style={[styles.fishCard, { zIndex: cardZIndex }]}>
{/* Delete + Chevron buttons - top right corner */}
<View
style={{
position: "absolute",
top: 0,
right: 0,
zIndex: 9999,
flexDirection: "row",
alignItems: "center",
padding: 8,
gap: 8,
}}
pointerEvents="box-none"
>
{isEditing && (
<TouchableOpacity
onPress={() => remove(index)}
style={{
backgroundColor: colors.error,
borderRadius: 8,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.08,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
elevation: 2,
}}
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
activeOpacity={0.7}
>
<IconSymbol name="trash" size={24} color="#fff" />
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => handleToggleExpanded(index)}
style={{
backgroundColor: colors.primary,
borderRadius: 8,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.08,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
elevation: 2,
}}
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
activeOpacity={0.7}
>
<IconSymbol
name={isExpanded ? "chevron.up" : "chevron.down"}
size={24}
color="#fff"
/>
</TouchableOpacity>
</View>
{/* Header - visible when collapsed */}
{!isExpanded && (
<View style={{ paddingRight: 100 }}>
{(() => {
const fishId = watch(`fish.${index}.id`);
const fishName = fishSpecies?.find((f) => f.id === fishId)?.name;
const quantity = watch(`fish.${index}.quantity`);
const unit = watch(`fish.${index}.unit`);
return (
<View style={styles.fishCardHeaderContent}>
<Text style={styles.fishCardTitle}>
{fishName || t("trip.createHaulModal.selectFish")}:
</Text>
<Text style={styles.fishCardSubtitle}>
{fishName ? `${quantity} ${unit}` : "---"}
</Text>
</View>
);
})()}
</View>
)}
{/* Form - visible when expanded */}
{isExpanded && (
<View style={{ paddingRight: 10 }}>
{/* Species dropdown */}
<Controller
control={control}
name={`fish.${index}.id`}
render={({ field: { value, onChange } }) => (
<View style={[styles.fieldGroup, { marginTop: 20 }]}>
<Text style={styles.label}>
{t("trip.createHaulModal.fishName")}
</Text>
<Select
options={fishSpecies!.map((fish) => ({
label: fish.name,
value: fish.id,
}))}
value={value}
onChange={onChange}
placeholder={t("trip.createHaulModal.selectFish")}
disabled={!isEditing}
/>
{errors.fish?.[index]?.id && (
<Text style={styles.errorText}>
{t("trip.createHaulModal.selectFish")}
</Text>
)}
</View>
)}
/>
{/* Số lượng & Đơn vị cùng hàng */}
<View style={{ flexDirection: "row", gap: 12 }}>
<View style={{ flex: 1 }}>
<Controller
control={control}
name={`fish.${index}.quantity`}
render={({ field: { value, onChange, onBlur } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>
{t("trip.createHaulModal.quantity")}
</Text>
<TextInput
keyboardType="numeric"
value={String(value ?? "")}
onBlur={onBlur}
onChangeText={(t) =>
onChange(Number(t.replace(/,/g, ".")) || 0)
}
style={[
styles.input,
!isEditing && styles.inputDisabled,
]}
editable={isEditing}
/>
{errors.fish?.[index]?.quantity && (
<Text style={styles.errorText}>
{t("trip.createHaulModal.quantity")}
</Text>
)}
</View>
)}
/>
</View>
<View style={{ flex: 1 }}>
<Controller
control={control}
name={`fish.${index}.unit`}
render={({ field: { value, onChange } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>
{t("trip.createHaulModal.unit")}
</Text>
<Select
options={UNITS_OPTIONS.map((unit) => ({
label: unit.label,
value: unit.value,
}))}
value={value}
onChange={onChange}
placeholder={t("trip.createHaulModal.unit")}
disabled={!isEditing}
listStyle={{ maxHeight: 100 }}
/>
{errors.fish?.[index]?.unit && (
<Text style={styles.errorText}>
{t("trip.createHaulModal.unit")}
</Text>
)}
</View>
)}
/>
</View>
</View>
{/* Size (optional) + Unit dropdown */}
<View style={{ flexDirection: "row", gap: 12 }}>
<View style={{ flex: 1 }}>
<Controller
control={control}
name={`fish.${index}.size`}
render={({ field: { value, onChange, onBlur } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>
{t("trip.createHaulModal.size")} (
{t("trip.createHaulModal.optional")})
</Text>
<TextInput
keyboardType="numeric"
value={value ? String(value) : ""}
onBlur={onBlur}
onChangeText={(t) =>
onChange(t ? Number(t.replace(/,/g, ".")) : undefined)
}
style={[
styles.input,
!isEditing && styles.inputDisabled,
]}
editable={isEditing}
/>
{errors.fish?.[index]?.size && (
<Text style={styles.errorText}>
{t("trip.createHaulModal.size")}
</Text>
)}
</View>
)}
/>
</View>
<View style={{ flex: 1 }}>
<Controller
control={control}
name={`fish.${index}.sizeUnit`}
render={({ field: { value, onChange } }) => (
<View style={styles.fieldGroup}>
<Text style={styles.label}>
{t("trip.createHaulModal.unit")}
</Text>
<Select
options={SIZE_UNITS_OPTIONS}
value={value}
onChange={onChange}
placeholder={t("trip.createHaulModal.unit")}
disabled={!isEditing}
listStyle={{ maxHeight: 80 }}
/>
</View>
)}
/>
</View>
</View>
</View>
)}
</View>
);
};
return (
<Modal
visible={isVisible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={60}
>
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>
{isCreateMode
? t("trip.createHaulModal.addFish")
: t("trip.createHaulModal.edit")}
</Text>
<View style={styles.headerButtons}>
{isEditing ? (
<>
{!isCreateMode && (
<TouchableOpacity
onPress={() => {
setIsEditing(false);
reset(); // reset to previous values
}}
style={[
styles.saveButton,
{ backgroundColor: "#6c757d" },
]}
>
<Text style={styles.saveButtonText}>
{t("trip.createHaulModal.cancel")}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={handleSubmit(onSubmit)}
style={styles.saveButton}
>
<Text style={styles.saveButtonText}>
{t("trip.createHaulModal.save")}
</Text>
</TouchableOpacity>
</>
) : (
!isCreateMode && (
<TouchableOpacity
onPress={() => setIsEditing(true)}
style={[styles.saveButton, { backgroundColor: "#17a2b8" }]}
>
<Text style={styles.saveButtonText}>
{t("trip.createHaulModal.edit")}
</Text>
</TouchableOpacity>
)
)}
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<View style={styles.closeIconButton}>
<IconSymbol name="xmark" size={24} color="#fff" />
</View>
</TouchableOpacity>
</View>
</View>
{/* Content */}
<ScrollView style={styles.content}>
{/* Info Section */}
<InfoSection fishingLog={fishingLog!} stt={fishingLogIndex} />
{/* Fish List */}
{fields.map((item, index) => renderRow(item, index))}
{/* Add Button - only show when editing */}
{isEditing && (
<TouchableOpacity
onPress={() => append(defaultItem())}
style={styles.addButton}
>
<Text style={styles.addButtonText}>
+ {t("trip.createHaulModal.addFish")}
</Text>
</TouchableOpacity>
)}
{/* Error Message */}
{errors.fish && (
<Text style={styles.errorText}>
{t("trip.createHaulModal.validationError")}
</Text>
)}
</ScrollView>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
export default CreateOrUpdateHaulModal;

View File

@@ -0,0 +1,105 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useI18n } from "@/hooks/use-i18n";
import { useThemeContext } from "@/hooks/use-theme-context";
import React from "react";
import { Modal, ScrollView, Text, TouchableOpacity, View } from "react-native";
import { createStyles } from "./style/CrewDetailModal.styles";
// ---------------------------
// 🧩 Interface
// ---------------------------
interface CrewDetailModalProps {
visible: boolean;
onClose: () => void;
crewData: Model.TripCrews | null;
}
// ---------------------------
// 👤 Component Modal
// ---------------------------
const CrewDetailModal: React.FC<CrewDetailModalProps> = ({
visible,
onClose,
crewData,
}) => {
const { t } = useI18n();
const { colors } = useThemeContext();
const styles = React.useMemo(() => createStyles(colors), [colors]);
if (!crewData) return null;
const infoItems = [
{
label: t("trip.crewDetailModal.personalId"),
value: crewData.Person.personal_id,
},
{ label: t("trip.crewDetailModal.fullName"), value: crewData.Person.name },
{ label: t("trip.crewDetailModal.role"), value: crewData.role },
{
label: t("trip.crewDetailModal.birthDate"),
value: crewData.Person.birth_date
? new Date(crewData.Person.birth_date).toLocaleDateString()
: t("trip.crewDetailModal.notUpdated"),
},
{
label: t("trip.crewDetailModal.phone"),
value: crewData.Person.phone || t("trip.crewDetailModal.notUpdated"),
},
{
label: t("trip.crewDetailModal.address"),
value: crewData.Person.address || t("trip.crewDetailModal.notUpdated"),
},
{
label: t("trip.crewDetailModal.joinedDate"),
value: crewData.joined_at
? new Date(crewData.joined_at).toLocaleDateString()
: t("trip.crewDetailModal.notUpdated"),
},
{
label: t("trip.crewDetailModal.note"),
value: crewData.note || t("trip.crewDetailModal.notUpdated"),
},
{
label: t("trip.crewDetailModal.status"),
value: crewData.left_at
? t("trip.crewDetailModal.resigned")
: t("trip.crewDetailModal.working"),
},
];
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>{t("trip.crewDetailModal.title")}</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<View style={styles.closeIconButton}>
<IconSymbol name="xmark" size={28} color="#fff" />
</View>
</TouchableOpacity>
</View>
{/* Content */}
<ScrollView style={styles.content}>
<View style={styles.infoCard}>
{infoItems.map((item, index) => (
<View key={index} style={styles.infoRow}>
<Text style={styles.infoLabel}>{item.label}</Text>
<Text style={styles.infoValue}>{item.value}</Text>
</View>
))}
</View>
</ScrollView>
</View>
</Modal>
);
};
export default CrewDetailModal;

View File

@@ -0,0 +1,245 @@
import { IconSymbol } from "@/components/ui/icon-symbol";
import { useI18n } from "@/hooks/use-i18n";
import { useThemeContext } from "@/hooks/use-theme-context";
import React, { useEffect, useState } from "react";
import {
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { createStyles } from "./style/TripCostDetailModal.styles";
// ---------------------------
// 🧩 Interface
// ---------------------------
interface TripCostDetailModalProps {
visible: boolean;
onClose: () => void;
data: Model.TripCost[];
}
// ---------------------------
// 💰 Component Modal
// ---------------------------
const TripCostDetailModal: React.FC<TripCostDetailModalProps> = ({
visible,
onClose,
data,
}) => {
const { t } = useI18n();
const { colors } = useThemeContext();
const styles = React.useMemo(() => createStyles(colors), [colors]);
const [isEditing, setIsEditing] = useState(false);
const [editableData, setEditableData] = useState<Model.TripCost[]>(data);
// Cập nhật editableData khi props data thay đổi (API fetch xong)
useEffect(() => {
setEditableData(data);
}, [data]);
const tongCong = editableData.reduce((sum, item) => sum + item.total_cost, 0);
const handleEdit = () => {
setIsEditing(!isEditing);
};
const handleSave = () => {
setIsEditing(false);
// TODO: Save data to backend
console.log("Saved data:", editableData);
};
const handleCancel = () => {
setIsEditing(false);
setEditableData(data); // Reset to original data
};
const updateItem = (
index: number,
field: keyof Model.TripCost,
value: string
) => {
setEditableData((prev) =>
prev.map((item, idx) => {
if (idx === index) {
const updated = { ...item, [field]: value };
// Recalculate total_cost
if (field === "amount" || field === "cost_per_unit") {
const amount =
Number(field === "amount" ? value : item.amount) || 0;
const costPerUnit =
Number(field === "cost_per_unit" ? value : item.cost_per_unit) ||
0;
updated.total_cost = amount * costPerUnit;
}
return updated;
}
return item;
})
);
};
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={60}
>
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>{t("trip.costDetailModal.title")}</Text>
<View style={styles.headerButtons}>
{isEditing ? (
<>
<TouchableOpacity
onPress={handleCancel}
style={styles.cancelButton}
>
<Text style={styles.cancelButtonText}>
{t("trip.costDetailModal.cancel")}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={handleSave}
style={styles.saveButton}
>
<Text style={styles.saveButtonText}>
{t("trip.costDetailModal.save")}
</Text>
</TouchableOpacity>
</>
) : (
<TouchableOpacity
onPress={handleEdit}
style={styles.editButton}
>
<View style={styles.editIconButton}>
<IconSymbol
name="pencil"
size={28}
color="#fff"
weight="heavy"
/>
</View>
</TouchableOpacity>
)}
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<View style={styles.closeIconButton}>
<IconSymbol name="xmark" size={28} color="#fff" />
</View>
</TouchableOpacity>
</View>
</View>
{/* Content */}
<ScrollView style={styles.content}>
{editableData.map((item, index) => (
<View key={index} style={styles.itemCard}>
{/* Loại */}
<View style={styles.fieldGroup}>
<Text style={styles.label}>
{t("trip.costDetailModal.costType")}
</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={item.type}
onChangeText={(value) => updateItem(index, "type", value)}
editable={isEditing}
placeholder={t("trip.costDetailModal.enterCostType")}
/>
</View>
{/* Số lượng & Đơn vị */}
<View style={styles.rowGroup}>
<View
style={[styles.fieldGroup, { flex: 1, marginRight: 8 }]}
>
<Text style={styles.label}>
{t("trip.costDetailModal.quantity")}
</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={String(item.amount ?? "")}
onChangeText={(value) =>
updateItem(index, "amount", value)
}
editable={isEditing}
keyboardType="numeric"
placeholder="0"
/>
</View>
<View style={[styles.fieldGroup, { flex: 1, marginLeft: 8 }]}>
<Text style={styles.label}>
{t("trip.costDetailModal.unit")}
</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={item.unit}
onChangeText={(value) => updateItem(index, "unit", value)}
editable={isEditing}
placeholder={t("trip.costDetailModal.placeholder")}
/>
</View>
</View>
{/* Chi phí/đơn vị */}
<View style={styles.fieldGroup}>
<Text style={styles.label}>
{t("trip.costDetailModal.costPerUnit")}
</Text>
<TextInput
style={[styles.input, !isEditing && styles.inputDisabled]}
value={String(item.cost_per_unit ?? "")}
onChangeText={(value) =>
updateItem(index, "cost_per_unit", value)
}
editable={isEditing}
keyboardType="numeric"
placeholder="0"
/>
</View>
{/* Tổng chi phí */}
<View style={styles.fieldGroup}>
<Text style={styles.label}>
{t("trip.costDetailModal.totalCost")}
</Text>
<View style={styles.totalContainer}>
<Text style={styles.totalText}>
{item.total_cost.toLocaleString()}{" "}
{t("trip.costDetailModal.vnd")}
</Text>
</View>
</View>
</View>
))}
{/* Footer Total */}
<View style={styles.footerTotal}>
<Text style={styles.footerLabel}>
{t("trip.costDetailModal.total")}
</Text>
<Text style={styles.footerAmount}>
{tongCong.toLocaleString()} {t("trip.costDetailModal.vnd")}
</Text>
</View>
</ScrollView>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
export default TripCostDetailModal;

View File

@@ -0,0 +1,119 @@
import { useI18n } from "@/hooks/use-i18n";
import { useThemeContext } from "@/hooks/use-theme-context";
import React from "react";
import { StyleSheet, Text, View } from "react-native";
interface InfoSectionProps {
fishingLog?: Model.FishingLog;
stt?: number;
}
export const InfoSection: React.FC<InfoSectionProps> = ({
fishingLog,
stt,
}) => {
const { t } = useI18n();
const { colors } = useThemeContext();
const styles = React.useMemo(() => createStyles(colors), [colors]);
if (!fishingLog) {
return null;
}
const infoItems = [
{
label: t("trip.infoSection.sttLabel"),
value: `${t("trip.infoSection.haulPrefix")} ${stt}`,
},
{
label: t("trip.infoSection.statusLabel"),
value:
fishingLog.status === 1
? t("trip.infoSection.statusCompleted")
: t("trip.infoSection.statusPending"),
isStatus: true,
},
{
label: t("trip.infoSection.startTimeLabel"),
value: fishingLog.start_at
? new Date(fishingLog.start_at).toLocaleString()
: t("trip.infoSection.notUpdated"),
},
{
label: t("trip.infoSection.endTimeLabel"),
value:
fishingLog.end_at.toString() !== "0001-01-01T00:00:00Z"
? new Date(fishingLog.end_at).toLocaleString()
: "-",
},
];
return (
<View style={styles.infoCard}>
{infoItems.map((item, index) => (
<View key={index} style={styles.infoRow}>
<Text style={styles.infoLabel}>{item.label}</Text>
{item.isStatus ? (
<View
style={[
styles.statusBadge,
item.value === t("trip.infoSection.statusCompleted")
? styles.statusBadgeCompleted
: styles.statusBadgeInProgress,
]}
>
<Text style={styles.statusBadgeText}>{item.value}</Text>
</View>
) : (
<Text style={styles.infoValue}>{item.value}</Text>
)}
</View>
))}
</View>
);
};
const createStyles = (colors: any) =>
StyleSheet.create({
infoCard: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
padding: 12,
marginBottom: 12,
backgroundColor: colors.surfaceSecondary,
},
infoRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
},
infoLabel: {
fontSize: 14,
fontWeight: "600",
color: colors.textSecondary,
},
infoValue: {
fontSize: 16,
color: colors.text,
paddingVertical: 8,
},
statusBadge: {
paddingVertical: 4,
paddingHorizontal: 12,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
},
statusBadgeCompleted: {
backgroundColor: colors.success,
},
statusBadgeInProgress: {
backgroundColor: colors.warning,
},
statusBadgeText: {
fontSize: 14,
fontWeight: "600",
color: "#fff",
},
});

View File

@@ -0,0 +1,179 @@
import { Colors } from "@/constants/theme";
import { StyleSheet } from "react-native";
export const createStyles = (colors: typeof Colors.light) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.backgroundSecondary,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingTop: 30,
paddingBottom: 16,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
title: {
fontSize: 22,
fontWeight: "700",
color: colors.text,
flex: 1,
},
headerButtons: {
flexDirection: "row",
gap: 12,
alignItems: "center",
},
closeButton: {
padding: 4,
},
closeIconButton: {
backgroundColor: colors.error,
borderRadius: 10,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
saveButton: {
backgroundColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 20,
paddingVertical: 10,
justifyContent: "center",
alignItems: "center",
},
saveButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
content: {
flex: 1,
padding: 16,
marginBottom: 15,
},
fishCard: {
backgroundColor: colors.surfaceSecondary,
borderRadius: 12,
padding: 16,
marginBottom: 16,
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
fishCardHeaderContent: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
fishCardTitle: {
fontSize: 16,
fontWeight: "700",
color: colors.text,
},
fishCardSubtitle: {
fontSize: 15,
color: colors.warning,
fontWeight: "500",
},
fieldGroup: {
marginBottom: 14,
},
label: {
fontSize: 14,
fontWeight: "600",
color: colors.textSecondary,
marginBottom: 6,
},
input: {
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
backgroundColor: colors.surface,
color: colors.text,
},
inputDisabled: {
backgroundColor: colors.backgroundSecondary,
color: colors.textSecondary,
borderColor: colors.border,
},
errorText: {
color: colors.error,
fontSize: 12,
marginTop: 4,
fontWeight: "500",
},
buttonRow: {
flexDirection: "row",
justifyContent: "flex-end",
gap: 8,
marginTop: 12,
},
removeButton: {
backgroundColor: colors.error,
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 8,
justifyContent: "center",
alignItems: "center",
},
removeButtonText: {
color: "#fff",
fontSize: 14,
fontWeight: "600",
},
addButton: {
backgroundColor: colors.primary,
borderRadius: 12,
padding: 16,
marginBottom: 12,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
addButtonText: {
fontSize: 16,
fontWeight: "600",
color: "#fff",
},
footerSection: {
paddingHorizontal: 16,
paddingVertical: 16,
backgroundColor: colors.surface,
borderTopWidth: 1,
borderTopColor: colors.separator,
},
saveButtonLarge: {
backgroundColor: colors.primary,
borderRadius: 8,
paddingVertical: 14,
justifyContent: "center",
alignItems: "center",
},
saveButtonLargeText: {
color: "#fff",
fontSize: 16,
fontWeight: "700",
},
emptyStateText: {
textAlign: "center",
color: colors.textSecondary,
fontSize: 14,
marginTop: 20,
},
});

View File

@@ -0,0 +1,69 @@
import { Colors } from "@/constants/theme";
import { StyleSheet } from "react-native";
export const createStyles = (colors: typeof Colors.light) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.backgroundSecondary,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingTop: 30,
paddingBottom: 16,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
title: {
fontSize: 22,
fontWeight: "700",
color: colors.text,
flex: 1,
},
closeButton: {
padding: 4,
},
closeIconButton: {
backgroundColor: colors.error,
borderRadius: 10,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
content: {
flex: 1,
padding: 16,
marginBottom: 15,
},
infoCard: {
backgroundColor: colors.card,
borderRadius: 12,
padding: 16,
marginBottom: 35,
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
infoRow: {
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
infoLabel: {
fontSize: 13,
fontWeight: "600",
color: colors.textSecondary,
marginBottom: 6,
},
infoValue: {
fontSize: 16,
color: colors.text,
fontWeight: "500",
},
});

View File

@@ -0,0 +1,293 @@
import { Colors } from "@/constants/theme";
import { StyleSheet } from "react-native";
export const createStyles = (colors: typeof Colors.light) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.backgroundSecondary,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingTop: 30,
paddingBottom: 16,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
title: {
fontSize: 22,
fontWeight: "700",
color: colors.text,
flex: 1,
},
closeButton: {
padding: 4,
},
closeIconButton: {
backgroundColor: colors.error,
borderRadius: 10,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
content: {
flex: 1,
padding: 16,
marginBottom: 15,
},
infoCard: {
backgroundColor: colors.card,
borderRadius: 12,
padding: 16,
marginBottom: 35,
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
infoRow: {
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
infoLabel: {
fontSize: 13,
fontWeight: "600",
color: colors.textSecondary,
marginBottom: 6,
},
infoValue: {
fontSize: 16,
color: colors.text,
fontWeight: "500",
},
statusBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
alignSelf: "flex-start",
},
statusBadgeCompleted: {
backgroundColor: colors.success,
},
statusBadgeInProgress: {
backgroundColor: colors.warning,
},
statusBadgeText: {
fontSize: 14,
fontWeight: "600",
color: "#fff",
},
statusBadgeTextCompleted: {
color: "#fff",
},
statusBadgeTextInProgress: {
color: "#fff",
},
headerButtons: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
editButton: {
padding: 4,
},
editIconButton: {
backgroundColor: colors.primary,
borderRadius: 10,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
cancelButton: {
paddingHorizontal: 12,
paddingVertical: 6,
},
cancelButtonText: {
color: colors.primary,
fontSize: 16,
fontWeight: "600",
},
saveButton: {
backgroundColor: colors.primary,
paddingHorizontal: 16,
paddingVertical: 6,
borderRadius: 6,
},
saveButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 16,
marginBottom: 12,
paddingHorizontal: 4,
},
sectionTitle: {
fontSize: 18,
fontWeight: "700",
color: colors.text,
},
totalCatchText: {
fontSize: 16,
fontWeight: "600",
color: colors.primary,
},
fishCard: {
position: "relative",
backgroundColor: colors.card,
borderRadius: 12,
padding: 16,
marginBottom: 12,
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
fieldGroup: {
marginBottom: 12,
marginTop: 0,
},
rowGroup: {
flexDirection: "row",
marginBottom: 12,
},
label: {
fontSize: 13,
fontWeight: "600",
color: colors.textSecondary,
marginBottom: 6,
},
input: {
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 15,
color: colors.text,
backgroundColor: colors.surface,
},
selectButton: {
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.surface,
},
selectButtonText: {
fontSize: 15,
color: colors.text,
},
optionsList: {
position: "absolute",
top: 46,
left: 0,
right: 0,
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 8,
marginTop: 4,
backgroundColor: colors.surface,
maxHeight: 100,
zIndex: 1000,
elevation: 5,
shadowColor: "#000",
shadowOpacity: 0.15,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
},
optionItem: {
paddingHorizontal: 12,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
optionText: {
fontSize: 15,
color: colors.text,
},
optionsStatusFishList: {
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 8,
marginTop: 4,
backgroundColor: colors.surface,
maxHeight: 120,
zIndex: 1000,
elevation: 5,
shadowColor: "#000",
shadowOpacity: 0.15,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
},
fishNameDropdown: {
position: "absolute",
top: 46,
left: 0,
right: 0,
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 8,
marginTop: 4,
backgroundColor: colors.surface,
maxHeight: 180,
zIndex: 1000,
elevation: 5,
shadowColor: "#000",
shadowOpacity: 0.15,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
},
fishCardHeaderContent: {
flexDirection: "row",
gap: 5,
},
fishCardTitle: {
fontSize: 16,
fontWeight: "600",
color: colors.text,
},
fishCardSubtitle: {
fontSize: 15,
color: colors.warning,
marginTop: 0,
},
addFishButton: {
backgroundColor: colors.primary,
borderRadius: 12,
padding: 16,
marginBottom: 12,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
addFishButtonContent: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
addFishButtonText: {
fontSize: 16,
fontWeight: "600",
color: "#fff",
},
});

View File

@@ -0,0 +1,153 @@
import { Colors } from "@/constants/theme";
import { StyleSheet } from "react-native";
export const createStyles = (colors: typeof Colors.light) =>
StyleSheet.create({
closeIconButton: {
backgroundColor: colors.error,
borderRadius: 10,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
container: {
flex: 1,
backgroundColor: colors.backgroundSecondary,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingTop: 30,
paddingBottom: 16,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: colors.separator,
},
title: {
fontSize: 22,
fontWeight: "700",
color: colors.text,
flex: 1,
},
headerButtons: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
editButton: {
padding: 4,
},
editIconButton: {
backgroundColor: colors.primary,
borderRadius: 10,
padding: 10,
justifyContent: "center",
alignItems: "center",
},
cancelButton: {
paddingHorizontal: 12,
paddingVertical: 6,
},
cancelButtonText: {
color: colors.primary,
fontSize: 16,
fontWeight: "600",
},
saveButton: {
backgroundColor: colors.primary,
paddingHorizontal: 16,
paddingVertical: 6,
borderRadius: 6,
},
saveButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
closeButton: {
padding: 4,
},
content: {
flex: 1,
padding: 16,
},
itemCard: {
backgroundColor: colors.surfaceSecondary,
borderRadius: 12,
padding: 16,
marginBottom: 12,
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
fieldGroup: {
marginBottom: 12,
},
rowGroup: {
flexDirection: "row",
marginBottom: 12,
},
label: {
fontSize: 13,
fontWeight: "600",
color: colors.textSecondary,
marginBottom: 6,
},
input: {
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 15,
color: colors.text,
backgroundColor: colors.surface,
},
inputDisabled: {
borderColor: colors.border,
backgroundColor: colors.backgroundSecondary,
color: colors.textSecondary,
},
totalContainer: {
backgroundColor: colors.backgroundSecondary,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
borderWidth: 1,
borderColor: colors.border,
},
totalText: {
fontSize: 16,
fontWeight: "700",
color: colors.warning,
},
footerTotal: {
backgroundColor: colors.card,
borderRadius: 12,
padding: 20,
marginTop: 8,
marginBottom: 50,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.1,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 3,
},
footerLabel: {
fontSize: 18,
fontWeight: "700",
color: colors.primary,
},
footerAmount: {
fontSize: 20,
fontWeight: "700",
color: colors.warning,
},
});

View File

@@ -0,0 +1,175 @@
import { StyleSheet } from "react-native";
import { Colors } from "@/constants/theme";
export type ColorScheme = "light" | "dark";
export function createTableStyles(colorScheme: ColorScheme) {
const colors = Colors[colorScheme];
return StyleSheet.create({
container: {
width: "100%",
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
marginVertical: 10,
borderWidth: 1,
borderColor: colors.border,
shadowColor: colors.text,
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
headerRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
title: {
fontSize: 18,
fontWeight: "700",
color: colors.text,
},
totalCollapsed: {
color: colors.warning,
fontSize: 18,
fontWeight: "700",
textAlign: "center",
},
row: {
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: colors.separator,
},
header: {
backgroundColor: colors.backgroundSecondary,
borderRadius: 6,
marginTop: 10,
},
left: {
textAlign: "left",
},
rowHorizontal: {
flexDirection: "row",
paddingVertical: 8,
borderBottomWidth: 0.5,
borderBottomColor: colors.separator,
paddingLeft: 15,
},
tableHeader: {
backgroundColor: colors.backgroundSecondary,
borderRadius: 6,
marginTop: 10,
},
headerCell: {
flex: 1,
fontSize: 15,
fontWeight: "600",
color: colors.text,
textAlign: "center",
},
headerCellLeft: {
flex: 1,
fontSize: 15,
fontWeight: "600",
color: colors.text,
textAlign: "left",
},
cell: {
flex: 1,
fontSize: 15,
color: colors.text,
textAlign: "center",
},
cellLeft: {
flex: 1,
fontSize: 15,
color: colors.text,
textAlign: "left",
},
cellRight: {
flex: 1,
fontSize: 15,
color: colors.text,
textAlign: "right",
},
cellWrapper: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
headerText: {
fontWeight: "600",
color: colors.text,
},
footerText: {
color: colors.primary,
fontWeight: "600",
},
footerTotal: {
color: colors.warning,
fontWeight: "800",
},
sttCell: {
flex: 0.3,
fontSize: 15,
color: colors.text,
textAlign: "center",
paddingLeft: 10,
},
statusContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.success,
marginRight: 6,
},
statusDotPending: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.warning,
marginRight: 6,
},
statusText: {
fontSize: 15,
color: colors.primary,
textDecorationLine: "underline",
},
linkText: {
color: colors.primary,
textDecorationLine: "underline",
},
viewDetailButton: {
marginTop: 12,
paddingVertical: 8,
alignItems: "center",
},
viewDetailText: {
color: colors.primary,
fontSize: 15,
fontWeight: "600",
textDecorationLine: "underline",
},
total: {
color: colors.warning,
fontWeight: "700",
},
right: {
color: colors.warning,
fontWeight: "600",
},
footerRow: {
marginTop: 6,
},
});
}
export type TableStyles = ReturnType<typeof createTableStyles>;