thêm quét QR đăng nhập, sửa lại logic để gọi api bằng ip thiết bị

This commit is contained in:
Tran Anh Tuan
2025-11-10 16:12:52 +07:00
parent c26de5aefc
commit 1a534eccb0
10 changed files with 224 additions and 218 deletions

View File

@@ -7,16 +7,7 @@ export default function Warning() {
const [isShowModal, setIsShowModal] = useState(false);
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<CreateOrUpdateHaulModal
fishingLog={fishingLogData}
isVisible={isShowModal}
onClose={function (): void {
setIsShowModal(false);
}}
/>
<Button title="Thêm thu hoạch" onPress={() => setIsShowModal(true)} />
</View>
</SafeAreaView>
);
}
@@ -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,
};

View File

@@ -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<PolylineWithLabelProps | null>(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 (
<Circle
key={index}
key={`circle-${index}`}
center={{
latitude: point.lat,
longitude: point.lon,
@@ -315,27 +325,27 @@ export default function HomeScreen() {
/>
);
})}
{polylineCoordinates && (
<PolylineWithLabel
key={`polyline-${gpsData?.lat || 0}-${gpsData?.lon || 0}`}
coordinates={polylineCoordinates.coordinates}
label={polylineCoordinates.label}
content={polylineCoordinates.content}
strokeColor="#FF5733"
strokeWidth={4}
showDistance={false}
// zIndex={50}
/>
{polylineCoordinates.length > 0 && (
<>
{polylineCoordinates.map((polyline, index) => (
<PolylineWithLabel
key={`polyline-${index}-${gpsData?.lat || 0}-${
gpsData?.lon || 0
}`}
coordinates={polyline.coordinates}
label={polyline.label}
content={polyline.content}
strokeColor="#FF5733"
strokeWidth={4}
showDistance={false}
// zIndex={50}
/>
))}
</>
)}
{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 (
<PolygonWithLabel
key={`polygon-${index}-${gpsData?.lat || 0}-${
@@ -365,11 +375,6 @@ export default function HomeScreen() {
latitude: gpsData.lat,
longitude: gpsData.lon,
}}
title={
platform === IOS_PLATFORM
? "Tàu của mình - iOS"
: "Tàu của mình - Android"
}
zIndex={20}
anchor={
platform === IOS_PLATFORM

View File

@@ -4,7 +4,7 @@ import { StyleSheet } from "react-native";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { api } from "@/config";
import { TOKEN } from "@/constants";
import { DOMAIN, TOKEN } from "@/constants";
import { removeStorageItem } from "@/utils/storage";
import { useState } from "react";
@@ -37,9 +37,10 @@ export default function SettingScreen() {
<ThemedText type="title">Settings</ThemedText>
<ThemedView
style={styles.button}
onTouchEnd={() => {
removeStorageItem(TOKEN);
router.replace("/auth/login");
onTouchEnd={async () => {
await removeStorageItem(TOKEN);
await removeStorageItem(DOMAIN);
router.navigate("/auth/login");
}}
>
<ThemedText type="defaultSemiBold">Đăng xuất</ThemedText>

View File

@@ -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 */}
<View style={styles.inputGroup}>
<ThemedText style={styles.label}>Mật khẩu</ThemedText>
<TextInput
style={styles.input}
placeholder="Nhập mật khẩu"
placeholderTextColor="#999"
value={password}
onChangeText={setPassword}
secureTextEntry
editable={!loading}
autoCapitalize="none"
/>
<View className="relative">
<TextInput
style={styles.input}
placeholder="Nhập mật khẩu"
placeholderTextColor="#999"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
editable={!loading}
autoCapitalize="none"
/>
{/* Position absolute with top:0 and bottom:0 and justifyContent:center
ensures the icon remains vertically centered inside the input */}
<TouchableOpacity
onPress={() => 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 }}
>
<Ionicons
name={showPassword ? "eye-off" : "eye"}
size={22}
color="#666"
/>
</TouchableOpacity>
</View>
</View>
{/* Login Button */}
<TouchableOpacity
style={[
styles.loginButton,
loading && styles.loginButtonDisabled,
]}
onPress={handleLogin}
disabled={loading}
{/* Login Button (3/4) + QR Scan (1/4) */}
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
}}
>
{loading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={styles.loginButtonText}>Đăng nhập</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={[
styles.loginButton,
loading && styles.loginButtonDisabled,
{ flex: 5, marginRight: 12, marginTop: 0 },
]}
onPress={() => handleLogin()}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={styles.loginButtonText}>Đăng nhập</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={{
flex: 1,
paddingVertical: 10,
marginTop: 0,
borderColor: "#ddd",
borderWidth: 1,
borderRadius: 8,
backgroundColor: "transparent",
justifyContent: "center",
alignItems: "center",
}}
onPress={() => setIsShowingQRScanner(true)}
disabled={loading}
>
<MaterialIcons
name="qr-code-scanner"
size={28}
color="#007AFF"
/>
</TouchableOpacity>
</View>
{/* Footer text */}
<View style={styles.footerContainer}>
@@ -176,6 +272,11 @@ export default function LoginScreen() {
</View>
</ThemedView>
</ScrollView>
<ScanQRCode
visible={isShowingQRScanner}
onClose={() => setIsShowingQRScanner(false)}
onScanned={handleQRCodeScanned}
/>
</KeyboardAvoidingView>
);
}