Complete user profile and socket connection
parent
b7ce82b86e
commit
f85f43f968
|
|
@ -16,7 +16,7 @@ import { Formik } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import api from "@/services/axiosClient";
|
import api from "@/services/axiosClient";
|
||||||
import { BASE_URL } from "@/constants/config";
|
import { BASE_URL } from "@/constants/config";
|
||||||
import { setUsername } from "@/store/userSlice";
|
import { setUserData } from "@/store/userSlice";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useSnackbar } from "@/contexts/Snackbar";
|
import { useSnackbar } from "@/contexts/Snackbar";
|
||||||
|
|
@ -27,7 +27,9 @@ export default function EditName() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const nameSchema = Yup.object().shape({
|
const nameSchema = Yup.object().shape({
|
||||||
name: Yup.string().required("Name is required"),
|
name: Yup.string()
|
||||||
|
.required("Name is required")
|
||||||
|
.max(57, "Name cannot exceed 57 characters"),
|
||||||
});
|
});
|
||||||
const { showSnackbar } = useSnackbar();
|
const { showSnackbar } = useSnackbar();
|
||||||
const handleSave = async (values: { name: string }) => {
|
const handleSave = async (values: { name: string }) => {
|
||||||
|
|
@ -37,7 +39,7 @@ export default function EditName() {
|
||||||
mobile: data?.mobile,
|
mobile: data?.mobile,
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(setUsername(values.name));
|
dispatch(setUserData({ name: values.name }));
|
||||||
showSnackbar("Name updated successfully", "success");
|
showSnackbar("Name updated successfully", "success");
|
||||||
router.back();
|
router.back();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -10,32 +10,139 @@ import {
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
import LanguageModal from "@/components/Profile/LangaugeModal";
|
import LanguageModal from "@/components/Profile/LangaugeModal";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "@/store";
|
import { AppDispatch, RootState } from "@/store";
|
||||||
import EditIcon from "../../assets/icons/edit.svg";
|
import EditIcon from "../../assets/icons/edit.svg";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { logout } from "@/store/authSlice";
|
||||||
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import { useSnackbar } from "@/contexts/Snackbar";
|
||||||
|
import { AWS, BASE_URL, USER_PROFILE } from "@/constants/config";
|
||||||
|
import { bytesToMB, handleUpload, updateUserProfile } from "@/utils/User";
|
||||||
|
import api from "@/services/axiosClient";
|
||||||
|
import { PresignedUrlDataItem } from "@/utils/User";
|
||||||
|
import { setUserData } from "@/store/userSlice";
|
||||||
|
import { Overlay } from "@/components/common/Overlay";
|
||||||
|
|
||||||
|
interface PresignedUrlResponse {
|
||||||
|
message: string;
|
||||||
|
data?: PresignedUrlDataItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const [isLangaugeModalVisible, setLanguageModalVisible] =
|
const [isLangaugeModalVisible, setLanguageModalVisible] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
const { showSnackbar } = useSnackbar();
|
||||||
const toggleLanguageModal = () => {
|
const toggleLanguageModal = () => {
|
||||||
setLanguageModalVisible(!isLangaugeModalVisible);
|
setLanguageModalVisible(!isLangaugeModalVisible);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isUploading, setIsUploading] = React.useState(false);
|
||||||
|
|
||||||
const { data } = useSelector((state: RootState) => state.user);
|
const { data } = useSelector((state: RootState) => state.user);
|
||||||
|
|
||||||
const userName = data?.name || "User";
|
const userName = data?.name || "User";
|
||||||
const mobileNumber = data?.mobile || "Not provided";
|
const mobileNumber = data?.mobile || "Not provided";
|
||||||
|
const userImageUrl = data?.profile_url;
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
dispatch(logout());
|
||||||
|
router.replace("/auth/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadImage = async (fileName: string, uri: string) => {
|
||||||
|
try {
|
||||||
|
setIsUploading(true);
|
||||||
|
const getPresignedUrl = `${BASE_URL}/api/v1/generate-presigned-urls`;
|
||||||
|
|
||||||
|
const response = await api.post<PresignedUrlResponse>(getPresignedUrl, {
|
||||||
|
files: [fileName],
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadData = response?.data?.data?.[0];
|
||||||
|
if (!uploadData) throw new Error("Presigned URL not received");
|
||||||
|
|
||||||
|
const { url, fields, originalFileName } = uploadData;
|
||||||
|
const presignedUrl: PresignedUrlDataItem = {
|
||||||
|
url,
|
||||||
|
fields,
|
||||||
|
originalFileName,
|
||||||
|
};
|
||||||
|
await handleUpload(uri, presignedUrl);
|
||||||
|
|
||||||
|
const region = AWS.REGION;
|
||||||
|
const bucketName = AWS.BUCKET_NAME;
|
||||||
|
const objectKey = presignedUrl.fields.key;
|
||||||
|
|
||||||
|
const uploadedImageUrl = `https://s3.${region}.amazonaws.com/${bucketName}/${encodeURIComponent(
|
||||||
|
objectKey
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
console.log("Uploaded image URL:", uploadedImageUrl);
|
||||||
|
await updateUserProfile({
|
||||||
|
profile_url: uploadedImageUrl,
|
||||||
|
mobile: mobileNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(setUserData({ profile_url: uri }));
|
||||||
|
|
||||||
|
showSnackbar("Image uploaded successfully", "success");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading image:", error);
|
||||||
|
showSnackbar("Error uploading image", "error");
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickImage = async () => {
|
||||||
|
let result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
|
quality: 1,
|
||||||
|
allowsEditing: true,
|
||||||
|
aspect: [1, 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled) {
|
||||||
|
const { uri, fileSize } = result.assets[0];
|
||||||
|
console.log(uri, "File size:", fileSize);
|
||||||
|
if (!fileSize) {
|
||||||
|
showSnackbar("Image size is not available", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const size = bytesToMB(fileSize);
|
||||||
|
if (size > USER_PROFILE.MAX_IMAGE_SIZE_IN_MB) {
|
||||||
|
showSnackbar(
|
||||||
|
`Image size exceeds ${USER_PROFILE.MAX_IMAGE_SIZE_IN_MB}MB limit`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Image size exceeds ${USER_PROFILE.MAX_IMAGE_SIZE_IN_MB}MB limit`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = uri.split("/").pop() || "image.jpg";
|
||||||
|
await uploadImage(fileName, uri);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
<View style={styles.avatarContainer}>
|
<View style={styles.avatarContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={require("../../assets/images/user_image.jpg")}
|
source={
|
||||||
|
userImageUrl
|
||||||
|
? { uri: userImageUrl }
|
||||||
|
: require("@/assets/images/user_image.jpg")
|
||||||
|
}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity style={styles.editAvatar}>
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.editAvatar}
|
||||||
|
onPress={() => handlePickImage()}
|
||||||
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -49,6 +156,7 @@ export default function ProfileScreen() {
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push("/user/edit_name");
|
router.push("/user/edit_name");
|
||||||
}}
|
}}
|
||||||
|
style={styles.edit_button}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="edit" size={20} color="#555C70" />
|
<MaterialIcons name="edit" size={20} color="#555C70" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
@ -71,13 +179,14 @@ export default function ProfileScreen() {
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
{menuItem("About App")}
|
{menuItem("About App")}
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
{menuItem("Logout")}
|
{menuItem("Logout", handleLogout)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<LanguageModal
|
<LanguageModal
|
||||||
onClose={() => setLanguageModalVisible(false)}
|
onClose={() => setLanguageModalVisible(false)}
|
||||||
visible={isLangaugeModalVisible}
|
visible={isLangaugeModalVisible}
|
||||||
/>
|
/>
|
||||||
|
<Overlay isUploading={isUploading} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +199,13 @@ const menuItem = (title: string, onPress?: () => void) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
edit_button: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { ActivityIndicator, Modal, StyleSheet, View } from "react-native";
|
||||||
|
|
||||||
|
interface OverlayProps {
|
||||||
|
isUploading: boolean;
|
||||||
|
transparent?: boolean;
|
||||||
|
animationType?: "fade" | "slide" | "none";
|
||||||
|
statusBarTranslucent?: boolean;
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Overlay = ({
|
||||||
|
isUploading,
|
||||||
|
size = 32,
|
||||||
|
color = "#fff",
|
||||||
|
transparent = true,
|
||||||
|
animationType = "fade",
|
||||||
|
statusBarTranslucent = true,
|
||||||
|
}: OverlayProps) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={isUploading}
|
||||||
|
transparent={transparent}
|
||||||
|
animationType={animationType}
|
||||||
|
statusBarTranslucent={statusBarTranslucent}
|
||||||
|
>
|
||||||
|
<View style={styles.loaderOverlay}>
|
||||||
|
<ActivityIndicator size={size} color={color} />
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
loaderOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.4)",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -3,22 +3,24 @@ import { View, Text, StyleSheet, Pressable } from "react-native";
|
||||||
import ChevronRight from "../../assets/icons/chevron_rightside.svg";
|
import ChevronRight from "../../assets/icons/chevron_rightside.svg";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
totalWarrantyYears: number; // in years
|
warrantyStartDate: string;
|
||||||
batteryPurchaseEpoch: number; // in seconds
|
warrantyEndDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BatteryWarrantyCard: React.FC<Props> = ({
|
const BatteryWarrantyCard: React.FC<Props> = ({
|
||||||
totalWarrantyYears,
|
warrantyStartDate,
|
||||||
batteryPurchaseEpoch,
|
warrantyEndDate,
|
||||||
}) => {
|
}) => {
|
||||||
const totalWarrantyMs = totalWarrantyYears * 365.25 * 24 * 60 * 60 * 1000;
|
const start = new Date(warrantyStartDate);
|
||||||
const purchaseDate = new Date(batteryPurchaseEpoch * 1000);
|
const end = new Date(warrantyEndDate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const elapsed = now.getTime() - purchaseDate.getTime();
|
|
||||||
const remaining = Math.max(totalWarrantyMs - elapsed, 0);
|
|
||||||
const progress = Math.min(elapsed / totalWarrantyMs, 1);
|
|
||||||
|
|
||||||
const remainingDate = new Date(remaining);
|
const totalDuration = end.getTime() - start.getTime();
|
||||||
|
const elapsed = now.getTime() - start.getTime();
|
||||||
|
const remaining = Math.max(end.getTime() - now.getTime(), 0);
|
||||||
|
|
||||||
|
const progress = Math.min(elapsed / totalDuration, 1);
|
||||||
|
|
||||||
const yearsLeft = Math.floor(remaining / (365.25 * 24 * 60 * 60 * 1000));
|
const yearsLeft = Math.floor(remaining / (365.25 * 24 * 60 * 60 * 1000));
|
||||||
const monthsLeft = Math.floor(
|
const monthsLeft = Math.floor(
|
||||||
(remaining % (365.25 * 24 * 60 * 60 * 1000)) / (30.44 * 24 * 60 * 60 * 1000)
|
(remaining % (365.25 * 24 * 60 * 60 * 1000)) / (30.44 * 24 * 60 * 60 * 1000)
|
||||||
|
|
@ -33,7 +35,9 @@ const BatteryWarrantyCard: React.FC<Props> = ({
|
||||||
<View style={styles.textColumn}>
|
<View style={styles.textColumn}>
|
||||||
<Text style={styles.title}>Battery Warranty Left</Text>
|
<Text style={styles.title}>Battery Warranty Left</Text>
|
||||||
<Text style={styles.time}>
|
<Text style={styles.time}>
|
||||||
{`${yearsLeft} years, ${monthsLeft} month${
|
{`${yearsLeft} year${
|
||||||
|
yearsLeft !== 1 ? "s" : ""
|
||||||
|
}, ${monthsLeft} month${
|
||||||
monthsLeft !== 1 ? "s" : ""
|
monthsLeft !== 1 ? "s" : ""
|
||||||
}, ${daysLeft} day${daysLeft !== 1 ? "s" : ""}`}
|
}, ${daysLeft} day${daysLeft !== 1 ? "s" : ""}`}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ import DangerIcon from "../assets/icons/danger.svg";
|
||||||
import type { BmsState } from "./types";
|
import type { BmsState } from "./types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const BASE_URL =
|
export const BASE_URL = "https://dev-driver-saathi-api.vecmocon.com";
|
||||||
"https://eae2-2400-80c0-2001-9c6-50f0-eda-dcd7-4167.ngrok-free.app";
|
|
||||||
|
// const SERVER_URL =
|
||||||
|
// "http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504";
|
||||||
|
export const VECTOR_BASE_URL = "https://vec-tr.ai";
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
LANGUAGE: "userLanguage",
|
LANGUAGE: "userLanguage",
|
||||||
|
|
@ -92,8 +95,17 @@ export const SUPPORT = {
|
||||||
EMAIL_BODY: "Hello,\n\nI need assistance with...",
|
EMAIL_BODY: "Hello,\n\nI need assistance with...",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const USER_PROFILE = {
|
||||||
|
MAX_IMAGE_SIZE_IN_MB: 5,
|
||||||
|
};
|
||||||
|
|
||||||
export const BMS_STATE_LABELS: Record<BmsState, string> = {
|
export const BMS_STATE_LABELS: Record<BmsState, string> = {
|
||||||
0: "Idle",
|
0: "Idle",
|
||||||
1: "Charging",
|
1: "Charging",
|
||||||
[-1]: "Discharging",
|
[-1]: "Discharging",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AWS = {
|
||||||
|
BUCKET_NAME: "battery-as-a-service",
|
||||||
|
REGION: "us-east-1",
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,82 @@ import {
|
||||||
setTelemetryError,
|
setTelemetryError,
|
||||||
} from "../store/telemetrySlice";
|
} from "../store/telemetrySlice";
|
||||||
import { BmsState } from "@/constants/types";
|
import { BmsState } from "@/constants/types";
|
||||||
|
import { BASE_URL, VECTOR_BASE_URL } from "@/constants/config";
|
||||||
|
import api from "./axiosClient";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
const SERVER_URL =
|
type TokenResponse = {
|
||||||
"http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504";
|
success: boolean;
|
||||||
const TOKEN =
|
message: string;
|
||||||
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2NCIsImFjdGlvbiI6ImF1dGgiLCJ0b2tlbi12ZXJzaW9uIjowLCJpYXQiOjE3NTE2MDgwNjIsImV4cCI6MTc1MTY5NDQ2Mn0.-46Br0jSPwOTvkcDBTI05GJ1GavaAOOli6LEgkvjj3c";
|
data: {
|
||||||
|
email: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
let socket: Socket | null = null;
|
let socket: Socket | null = null;
|
||||||
|
let token: string | null = null;
|
||||||
|
let controllingServer: string | null = null;
|
||||||
|
|
||||||
|
const fetchToken = async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const response = await api.get<TokenResponse>(
|
||||||
|
`${BASE_URL}/api/v1/vec-token`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data.data.token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching token:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchControllingServer = async (token: string): Promise<string> => {
|
||||||
|
const hardwareDeviceId = store.getState().user.data?.batteries[0]?.battery_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!hardwareDeviceId) throw new Error("Missing hardwareDeviceId");
|
||||||
|
|
||||||
|
const response = await axios.get<string>(
|
||||||
|
`${VECTOR_BASE_URL}/api/device-management/dashboard/get/hardwareDeviceId/${hardwareDeviceId.substring(
|
||||||
|
1
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data.trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching controlling server:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectSocket = () => {
|
||||||
|
const hardwareDeviceId = store.getState().user.data?.batteries[0]?.battery_id;
|
||||||
|
|
||||||
|
if (!token || !controllingServer || !hardwareDeviceId) {
|
||||||
|
store.dispatch(setTelemetryError("Missing socket auth info"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
export const initSocket = () => {
|
|
||||||
store.dispatch(setTelemetryLoading());
|
store.dispatch(setTelemetryLoading());
|
||||||
console.log("Initializing socket connection...");
|
console.log("Initializing socket connection...");
|
||||||
socket = io(SERVER_URL, {
|
|
||||||
transports: ["websocket"],
|
socket = io(
|
||||||
extraHeaders: {
|
`${VECTOR_BASE_URL}/?dashboardId=deviceDashboardSocket&assetId=${hardwareDeviceId}`,
|
||||||
Authorization: `Bearer ${TOKEN}`,
|
{
|
||||||
controllingServer: "http://dev.vec-tr.ai:8082",
|
transports: ["websocket"],
|
||||||
},
|
extraHeaders: {
|
||||||
reconnection: true,
|
Authorization: `Bearer ${token}`,
|
||||||
});
|
controllingServer: controllingServer,
|
||||||
|
},
|
||||||
|
reconnection: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
console.log("Socket connected:", socket?.id);
|
console.log("Socket connected:", socket?.id);
|
||||||
|
|
@ -43,6 +100,17 @@ export const initSocket = () => {
|
||||||
socket.on("dataUpdate", handleSocketData);
|
socket.on("dataUpdate", handleSocketData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initSocket = async () => {
|
||||||
|
try {
|
||||||
|
token = await fetchToken();
|
||||||
|
controllingServer = await fetchControllingServer(token);
|
||||||
|
connectSocket();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err, "");
|
||||||
|
store.dispatch(setTelemetryError("Initialization failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const disconnectSocket = () => {
|
export const disconnectSocket = () => {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
|
|
@ -53,8 +121,10 @@ export const disconnectSocket = () => {
|
||||||
const handleSocketData = (data: any) => {
|
const handleSocketData = (data: any) => {
|
||||||
console.log("...");
|
console.log("...");
|
||||||
try {
|
try {
|
||||||
|
console.log(data.dataSeries.assetData, "dataSeries.assetData");
|
||||||
const SoH =
|
const SoH =
|
||||||
data.dataSeries.assetData[0].bms[0].bmsSpecific.ivecSpecific.soh ?? null;
|
data?.dataSeries?.assetData[0]?.bms[0]?.bmsSpecific?.ivecSpecific?.soh ??
|
||||||
|
null;
|
||||||
const SoC = data?.dataSeries?.assetData?.[0]?.bms?.[0]?.batterySoc ?? null;
|
const SoC = data?.dataSeries?.assetData?.[0]?.bms?.[0]?.batterySoc ?? null;
|
||||||
const currentMode =
|
const currentMode =
|
||||||
data?.dataSeries?.assetData?.[0]?.bms?.[0]?.bmsSpecific?.ivecSpecific
|
data?.dataSeries?.assetData?.[0]?.bms?.[0]?.bmsSpecific?.ivecSpecific
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,19 @@ interface UserData {
|
||||||
email: string | null;
|
email: string | null;
|
||||||
profile_url: string | null;
|
profile_url: string | null;
|
||||||
vehicles: Vehicle[];
|
vehicles: Vehicle[];
|
||||||
|
batteries: Battery[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Battery {
|
||||||
|
battery_id: string;
|
||||||
|
warranty_status: boolean;
|
||||||
|
battery_model: string;
|
||||||
|
bms_id: string;
|
||||||
|
warranty_start_date: string;
|
||||||
|
warranty_end_date: string;
|
||||||
|
vim_id: string;
|
||||||
|
serial_no: string;
|
||||||
|
charger_uid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
|
|
@ -42,6 +55,7 @@ export const getUserDetails = createAsyncThunk<UserData>(
|
||||||
console.log("User details fetched successfully:", response.data.data);
|
console.log("User details fetched successfully:", response.data.data);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
} else {
|
} else {
|
||||||
|
console.error("Failed to fetch user data:", response.data.message);
|
||||||
return rejectWithValue("Failed to fetch user data");
|
return rejectWithValue("Failed to fetch user data");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -49,6 +63,7 @@ export const getUserDetails = createAsyncThunk<UserData>(
|
||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.message ||
|
error.message ||
|
||||||
"Something went wrong";
|
"Something went wrong";
|
||||||
|
console.log("Error fetching user details:", message);
|
||||||
return rejectWithValue(message);
|
return rejectWithValue(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,11 +78,14 @@ const userSlice = createSlice({
|
||||||
state.error = null;
|
state.error = null;
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
},
|
},
|
||||||
setUsername: (state, action: PayloadAction<string>) => {
|
setUserData: (state, action: PayloadAction<Partial<UserData>>) => {
|
||||||
if (state.data) {
|
if (state.data) {
|
||||||
state.data.name = action.payload;
|
state.data = {
|
||||||
|
...state.data,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn("Cannot set username, user data is not loaded");
|
console.warn("Cannot update user data, user data is not loaded");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -91,5 +109,5 @@ const userSlice = createSlice({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { clearUser, setUsername } = userSlice.actions;
|
export const { clearUser, setUserData } = userSlice.actions;
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { BASE_URL } from "@/constants/config";
|
||||||
|
import api from "@/services/axiosClient";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
interface PresignedUrlField {
|
||||||
|
key: string;
|
||||||
|
"x-amz-meta-originalFileName": string;
|
||||||
|
bucket: string;
|
||||||
|
"X-Amz-Algorithm": string;
|
||||||
|
"X-Amz-Credential": string;
|
||||||
|
"X-Amz-Date": string;
|
||||||
|
Policy: string;
|
||||||
|
"X-Amz-Signature": string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresignedUrlDataItem {
|
||||||
|
url: string;
|
||||||
|
fields: PresignedUrlField;
|
||||||
|
originalFileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToMB(bytes: number) {
|
||||||
|
if (!bytes) return 0;
|
||||||
|
const MB = 1024 * 1024;
|
||||||
|
return bytes / MB;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleUpload = async (
|
||||||
|
uri: string,
|
||||||
|
presignedUrl: PresignedUrlDataItem
|
||||||
|
) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
console.log("Presigned URL:", presignedUrl.url);
|
||||||
|
if (!presignedUrl) {
|
||||||
|
console.log("Presigned URL not found for file", uri);
|
||||||
|
throw new Error("Image upload failed.");
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(presignedUrl.fields)) {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append("success_action_status", 201);
|
||||||
|
const fileType = getFileTypeFromUri(uri);
|
||||||
|
const fileName = extractFileName(uri);
|
||||||
|
|
||||||
|
formData.append("file", {
|
||||||
|
uri: uri,
|
||||||
|
type: fileType,
|
||||||
|
name: fileName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: presignedUrl.url,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await axios.post(presignedUrl.url, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
maxRedirects: 0, // optional, to detect redirection errors more explicitly
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading image:", error);
|
||||||
|
throw new Error("Image upload failed.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function extractFileName(filePath: string) {
|
||||||
|
let parts = filePath.split("/");
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFileTypeFromUri = (uri: string): string => {
|
||||||
|
const extension = uri.split(".").pop()?.toLowerCase() || "";
|
||||||
|
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
jpg: "image/jpeg",
|
||||||
|
jpeg: "image/jpeg",
|
||||||
|
png: "image/png",
|
||||||
|
gif: "image/gif",
|
||||||
|
bmp: "image/bmp",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
webp: "image/webp",
|
||||||
|
txt: "text/plain",
|
||||||
|
pdf: "application/pdf",
|
||||||
|
doc: "application/msword",
|
||||||
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
xls: "application/vnd.ms-excel",
|
||||||
|
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
ppt: "application/vnd.ms-powerpoint",
|
||||||
|
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
mp4: "video/mp4",
|
||||||
|
mp3: "audio/mpeg",
|
||||||
|
wav: "audio/wav",
|
||||||
|
};
|
||||||
|
|
||||||
|
return mimeTypes[extension] || "application/octet-stream";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserProfile = ({
|
||||||
|
mobile,
|
||||||
|
name,
|
||||||
|
profile_url,
|
||||||
|
}: {
|
||||||
|
mobile: string;
|
||||||
|
name?: string;
|
||||||
|
profile_url?: string;
|
||||||
|
}) => {
|
||||||
|
if (!mobile) {
|
||||||
|
throw new Error("Mobile number is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: { mobile: string; name?: string; profile_url?: string } = {
|
||||||
|
mobile,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof name === "string" && name.trim() !== "") {
|
||||||
|
payload.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof profile_url === "string" && profile_url.trim() !== "") {
|
||||||
|
payload.profile_url = profile_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.put(`${BASE_URL}/api/v1/update-user-information`, payload);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue