diff --git a/app/(tabs)/diary.tsx b/app/(tabs)/diary.tsx
index de69db8..93d63d1 100644
--- a/app/(tabs)/diary.tsx
+++ b/app/(tabs)/diary.tsx
@@ -7,16 +7,7 @@ export default function Warning() {
const [isShowModal, setIsShowModal] = useState(false);
return (
-
-
-
+
);
}
@@ -54,118 +45,3 @@ const styles = StyleSheet.create({
},
});
-const fishingLogData:Model.FishingLog = {
- fishing_log_id: "124b2701-a5d6-4eb0-ba3b-6789473c14a9",
- trip_id: "d84caab6-ebb0-4cf7-abf9-31e5617d23b9",
- start_at: "2025-11-07T10:50:01.693193764Z",
- end_at: "2025-11-07T10:50:31.693027729Z",
- start_lat: 11.59141,
- start_lon: 109.0489,
- haul_lat: 11.590274,
- haul_lon: 109.049284,
- status: 1,
- weather_description: "Stormy",
- info: [
- {
- fish_species_id: 8,
- fish_name: "Cá hồng phớn",
- catch_number: 1309,
- catch_unit: "kg",
- fish_size: 173,
- fish_rarity: 3,
- fish_condition: "Còn sống",
- gear_usage: "Câu vàng",
- },
- {
- fish_species_id: 18,
- fish_name: "Cá đuối quạt",
- catch_number: 731,
- catch_unit: "kg",
- fish_size: 16,
- fish_rarity: 4,
- fish_condition: "Chết",
- gear_usage: "Bẫy lưới",
- },
- {
- fish_species_id: 7,
- fish_name: "Cá bơn vàng",
- catch_number: 1224,
- catch_unit: "kg",
- fish_size: 12,
- fish_rarity: 1,
- fish_condition: "Chết",
- gear_usage: "",
- },
- {
- fish_species_id: 16,
- fish_name: "Cá rồng biển",
- catch_number: 838,
- catch_unit: "kg",
- fish_size: 164,
- fish_rarity: 3,
- fish_condition: "Chết",
- gear_usage: "Lưới rê",
- },
- {
- fish_species_id: 9,
- fish_name: "Cá hổ Napoleon",
- catch_number: 1410,
- catch_unit: "kg",
- fish_size: 104,
- fish_rarity: 4,
- fish_condition: "Bị thương",
- gear_usage: "Câu vàng",
- },
- {
- fish_species_id: 3,
- fish_name: "Cá chim trắng",
- catch_number: 1184,
- catch_unit: "kg",
- fish_size: 104,
- fish_rarity: 2,
- fish_condition: "Còn sống",
- gear_usage: "",
- },
- {
- fish_species_id: 5,
- fish_name: "Cá mú đỏ",
- catch_number: 987,
- catch_unit: "kg",
- fish_size: 171,
- fish_rarity: 2,
- fish_condition: "Bị thương",
- gear_usage: "",
- },
- {
- fish_species_id: 13,
- fish_name: "Cá song đỏ",
- catch_number: 1676,
- catch_unit: "kg",
- fish_size: 99,
- fish_rarity: 2,
- fish_condition: "Bị thương",
- gear_usage: "",
- },
- {
- fish_species_id: 11,
- fish_name: "Cá ngừ đại dương",
- catch_number: 462,
- catch_unit: "kg",
- fish_size: 11,
- fish_rarity: 1,
- fish_condition: "Bị thương",
- gear_usage: "",
- },
- {
- fish_species_id: 2,
- fish_name: "Cá nục",
- catch_number: 496,
- catch_unit: "kg",
- fish_size: 125,
- fish_rarity: 1,
- fish_condition: "Còn sống",
- gear_usage: "",
- },
- ],
- sync: true,
-};
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 8d13633..c3343d7 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -46,8 +46,9 @@ export default function HomeScreen() {
const [circleRadius, setCircleRadius] = useState(100);
const [zoomLevel, setZoomLevel] = useState(10);
const [isFirstLoad, setIsFirstLoad] = useState(true);
- const [polylineCoordinates, setPolylineCoordinates] =
- useState(null);
+ const [polylineCoordinates, setPolylineCoordinates] = useState<
+ PolylineWithLabelProps[]
+ >([]);
const [polygonCoordinates, setPolygonCoordinates] = useState<
PolygonWithLabelProps[]
>([]);
@@ -69,7 +70,7 @@ export default function HomeScreen() {
} else {
setGpsData(null);
setPolygonCoordinates([]);
- setPolylineCoordinates(null);
+ setPolylineCoordinates([]);
}
};
const queryAlarmData = (alarmData: Model.AlarmResponse) => {
@@ -121,7 +122,7 @@ export default function HomeScreen() {
}, []);
useEffect(() => {
- setPolylineCoordinates(null);
+ setPolylineCoordinates([]);
setPolygonCoordinates([]);
if (!entityData) return;
if (!banzoneData) return;
@@ -139,11 +140,14 @@ export default function HomeScreen() {
}
// Nếu danh sách zone rỗng, clear tất cả
if (zones.length === 0) {
- setPolylineCoordinates(null);
+ setPolylineCoordinates([]);
setPolygonCoordinates([]);
return;
}
+ let polylines: PolylineWithLabelProps[] = [];
+ let polygons: PolygonWithLabelProps[] = [];
+
for (const zone of zones) {
// console.log("Zone Data: ", zone);
const geom = banzoneData.find((b) => b.id === zone.zone_id);
@@ -161,7 +165,7 @@ export default function HomeScreen() {
geom_lines || ""
);
if (coordinates.length > 0) {
- setPolylineCoordinates({
+ polylines.push({
coordinates: coordinates.map((coord) => ({
latitude: coord[0],
longitude: coord[1],
@@ -169,25 +173,31 @@ export default function HomeScreen() {
label: zone?.zone_name ?? "",
content: zone?.message ?? "",
});
+ } else {
+ console.log("Không tìm thấy polyline trong alarm");
}
} else if (geom_type === 1) {
// foundPolygon = true;
const coordinates = convertWKTtoLatLngString(geom_poly || "");
if (coordinates.length > 0) {
// console.log("Polygon Coordinate: ", coordinates);
- setPolygonCoordinates(
- coordinates.map((polygon) => ({
- coordinates: polygon.map((coord) => ({
- latitude: coord[0],
- longitude: coord[1],
- })),
- label: zone?.zone_name ?? "",
- content: zone?.message ?? "",
- }))
- );
+ const zonePolygons = coordinates.map((polygon) => ({
+ coordinates: polygon.map((coord) => ({
+ latitude: coord[0],
+ longitude: coord[1],
+ })),
+ label: zone?.zone_name ?? "",
+ content: zone?.message ?? "",
+ }));
+ polygons.push(...zonePolygons);
+ } else {
+ console.log("Không tìm thấy polygon trong alarm");
}
}
}
+
+ setPolylineCoordinates(polylines);
+ setPolygonCoordinates(polygons);
}
}, [banzoneData, entityData]);
@@ -301,7 +311,7 @@ export default function HomeScreen() {
// console.log(`Rendering circle ${index}:`, point);
return (
);
})}
- {polylineCoordinates && (
-
+ {polylineCoordinates.length > 0 && (
+ <>
+ {polylineCoordinates.map((polyline, index) => (
+
+ ))}
+ >
)}
{polygonCoordinates.length > 0 && (
<>
{polygonCoordinates.map((polygon, index) => {
- // Tạo key ổn định từ tọa độ đầu tiên của polygon
- const polygonKey =
- polygon.coordinates.length > 0
- ? `polygon-${polygon.coordinates[0].latitude}-${polygon.coordinates[0].longitude}-${index}`
- : `polygon-${index}`;
-
return (
Settings
{
- removeStorageItem(TOKEN);
- router.replace("/auth/login");
+ onTouchEnd={async () => {
+ await removeStorageItem(TOKEN);
+ await removeStorageItem(DOMAIN);
+ router.navigate("/auth/login");
}}
>
Đăng xuất
diff --git a/app/auth/login.tsx b/app/auth/login.tsx
index 0bea752..77ca28d 100644
--- a/app/auth/login.tsx
+++ b/app/auth/login.tsx
@@ -1,14 +1,16 @@
+import ScanQRCode from "@/components/ScanQRCode";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
-import { TOKEN } from "@/constants";
+import { DOMAIN, TOKEN } from "@/constants";
import { login } from "@/controller/AuthController";
-import { showErrorToast } from "@/services/toast_service";
+import { showErrorToast, showWarningToast } from "@/services/toast_service";
import {
getStorageItem,
removeStorageItem,
setStorageItem,
} from "@/utils/storage";
import { parseJwtToken } from "@/utils/token";
+import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import {
@@ -29,22 +31,30 @@ export default function LoginScreen() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
const checkLogin = useCallback(async () => {
const token = await getStorageItem(TOKEN);
- console.log("Token:", token);
-
+ const domain = await getStorageItem(DOMAIN);
+ // console.log("Token:", token);
+ // removeStorageItem(DOMAIN);
+ console.log("Domain:", domain);
if (!token) {
return;
}
+ if (!domain) {
+ return;
+ }
const parsed = parseJwtToken(token);
- console.log("Parse Token: ", parsed);
+ // console.log("Parse Token: ", parsed);
const { exp } = parsed;
const now = Math.floor(Date.now() / 1000);
const oneHour = 60 * 60;
if (exp - now < oneHour) {
await removeStorageItem(TOKEN);
+ await removeStorageItem(DOMAIN);
} else {
router.replace("/(tabs)");
}
@@ -54,9 +64,41 @@ export default function LoginScreen() {
checkLogin();
}, [checkLogin]);
- const handleLogin = async () => {
+ const handleQRCodeScanned = async (data: string) => {
+ console.log("QR Code Scanned Data:", data);
+ try {
+ const parsed = JSON.parse(data);
+ if (parsed.username && parsed.password) {
+ // update UI fields
+ setUsername(parsed.username);
+ setPassword(parsed.password);
+ console.log("Domain: ", parsed.device_ip);
+
+ // close scanner so user sees the filled form
+ await setStorageItem(DOMAIN, parsed.device_ip);
+
+ // // call login directly with scanned credentials to avoid waiting for state to update
+ await handleLogin({
+ username: parsed.username,
+ password: parsed.password,
+ });
+ } else {
+ showWarningToast("Mã QR không hợp lệ");
+ }
+ } catch (error) {
+ showWarningToast("Mã QR không hợp lệ");
+ }
+ };
+
+ const handleLogin = async (creds?: {
+ username: string;
+ password: string;
+ }) => {
+ const user = creds?.username ?? username;
+ const pass = creds?.password ?? password;
+
// Validate input
- if (!username.trim() || !password.trim()) {
+ if (!user?.trim() || !pass?.trim()) {
showErrorToast("Vui lòng nhập tài khoản và mật khẩu");
return;
}
@@ -64,8 +106,8 @@ export default function LoginScreen() {
setLoading(true);
try {
const body: Model.LoginRequestBody = {
- username: username,
- password: password,
+ username: user,
+ password: pass,
};
const response = await login(body);
@@ -131,33 +173,87 @@ export default function LoginScreen() {
{/* Password Input */}
Mật khẩu
-
+
+
+ {/* Position absolute with top:0 and bottom:0 and justifyContent:center
+ ensures the icon remains vertically centered inside the input */}
+ setShowPassword(!showPassword)}
+ style={{
+ position: "absolute",
+ right: 12,
+ top: 0,
+ bottom: 0,
+ justifyContent: "center",
+ alignItems: "center",
+ padding: 4,
+ }}
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
+ >
+
+
+
- {/* Login Button */}
-
- {loading ? (
-
- ) : (
- Đăng nhập
- )}
-
+ handleLogin()}
+ disabled={loading}
+ >
+ {loading ? (
+
+ ) : (
+ Đăng nhập
+ )}
+
+
+ setIsShowingQRScanner(true)}
+ disabled={loading}
+ >
+
+
+
{/* Footer text */}
@@ -176,6 +272,11 @@ export default function LoginScreen() {
+ setIsShowingQRScanner(false)}
+ onScanned={handleQRCodeScanned}
+ />
);
}
diff --git a/components/ScanQRCode.tsx b/components/ScanQRCode.tsx
index fa3cdf4..a2846fc 100644
--- a/components/ScanQRCode.tsx
+++ b/components/ScanQRCode.tsx
@@ -23,6 +23,8 @@ export default function ScanQRCode({
const [permission, requestPermission] = useCameraPermissions();
const [scanned, setScanned] = useState(false);
const cameraRef = useRef(null);
+ // Dùng ref để chặn quét nhiều lần trong cùng một frame/event loop
+ const hasScannedRef = useRef(false);
// Request camera permission when component mounts or when visible changes to true
useEffect(() => {
@@ -38,6 +40,13 @@ export default function ScanQRCode({
}
}, [visible]);
+ // Mỗi khi reset scanned state thì reset luôn ref guard
+ useEffect(() => {
+ if (!scanned) {
+ hasScannedRef.current = false;
+ }
+ }, [scanned]);
+
const handleBarCodeScanned = ({
type,
data,
@@ -45,11 +54,12 @@ export default function ScanQRCode({
type: string;
data: string;
}) => {
- if (!scanned) {
- setScanned(true);
- onScanned(data);
- onClose();
- }
+ // Nếu đã scan rồi, bỏ qua
+ if (hasScannedRef.current || scanned) return;
+ hasScannedRef.current = true;
+ setScanned(true);
+ onScanned(data);
+ onClose();
};
if (!permission) {
@@ -102,7 +112,8 @@ export default function ScanQRCode({
{
}
} catch (error) {
console.error("Error when send sos: ", error);
- showToastError("Không thể gửi tín hiệu SOS", "Lỗi");
+ showErrorToast("Không thể gửi tín hiệu SOS");
}
};
diff --git a/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx b/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx
index 3d105ec..1074ef0 100644
--- a/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx
+++ b/components/tripInfo/modal/NetDetailModal/components/InfoSection.tsx
@@ -30,7 +30,7 @@ export const InfoSection: React.FC = ({
{
label: "Thời gian kết thúc",
value:
- fishingLog.end_at !== "0001-01-01T00:00:00Z"
+ fishingLog.end_at.toString() !== "0001-01-01T00:00:00Z"
? new Date(fishingLog.end_at).toLocaleString()
: "-",
},
diff --git a/config/auth.ts b/config/auth.ts
index 71770c4..56030e2 100644
--- a/config/auth.ts
+++ b/config/auth.ts
@@ -1,4 +1,4 @@
-import { TOKEN } from "@/constants";
+import { DOMAIN, TOKEN } from "@/constants";
import { removeStorageItem } from "@/utils/storage";
import { Router } from "expo-router";
@@ -17,7 +17,12 @@ export const setRouterInstance = (router: Router) => {
export const handle401 = () => {
if (routerInstance) {
removeStorageItem(TOKEN);
- (routerInstance as any).replace("/auth/login");
+ removeStorageItem(DOMAIN);
+ // Cancel all pending requests to prevent further API calls
+ if (typeof window !== "undefined" && (window as any).axiosAbortController) {
+ (window as any).axiosAbortController.abort();
+ }
+ routerInstance.navigate("/auth/login");
} else {
console.warn("Router instance not set, cannot redirect to login");
}
diff --git a/config/axios.ts b/config/axios.ts
index bea7b47..7a2b3ab 100644
--- a/config/axios.ts
+++ b/config/axios.ts
@@ -1,8 +1,8 @@
-import { TOKEN } from "@/constants";
+import { DOMAIN, TOKEN } from "@/constants";
+import { showErrorToast } from "@/services/toast_service";
import { getStorageItem } from "@/utils/storage";
import axios, { AxiosInstance } from "axios";
import { handle401 } from "./auth";
-import { showToastError } from "./toast";
const codeMessage = {
200: "The server successfully returned the requested data。",
@@ -38,9 +38,15 @@ api.interceptors.request.use(
async (config) => {
// Thêm auth token nếu có
const token = await getStorageItem(TOKEN);
+ const domain = await getStorageItem(DOMAIN);
+ if (domain) {
+ config.baseURL = `http://${domain}`;
+ }
if (token) {
config.headers.Authorization = `${token}`;
}
+ // console.log("Domain Request: ", config.baseURL);
+
return config;
},
(error) => {
@@ -57,7 +63,9 @@ api.interceptors.response.use(
if (!error.response) {
const networkErrorMsg =
error.message || "Network error - please check connection";
- showToastError("Lỗi kết nối", networkErrorMsg);
+ showErrorToast("Lỗi kết nối");
+ console.error("Response Network Error: ", networkErrorMsg);
+
return Promise.reject(error);
}
@@ -70,8 +78,7 @@ api.interceptors.response.use(
statusText ||
"Unknown error";
- showToastError(`Lỗi ${status}`, errMsg);
-
+ showErrorToast(`Lỗi ${status}: ${errMsg}`);
if (status === 401) {
handle401();
}
diff --git a/constants/index.ts b/constants/index.ts
index 0d30192..8da46f6 100644
--- a/constants/index.ts
+++ b/constants/index.ts
@@ -1,5 +1,5 @@
export const TOKEN = "token";
-export const BASE_URL = "https://sgw-device.gms.vn";
+export const DOMAIN = "domain";
export const MAP_TRACKPOINTS_ID = "ship-trackpoints";
export const MAP_POLYLINE_BAN = "ban-polyline";
export const MAP_POLYGON_BAN = "ban-polygon";