558 lines
15 KiB
TypeScript
558 lines
15 KiB
TypeScript
import {
|
|
Animated,
|
|
Easing,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
} from "react-native";
|
|
import { useTranslation } from "react-i18next";
|
|
import SemiCircleProgress from "../../components/home/SemiCircleProgress";
|
|
import { Text, View } from "react-native";
|
|
import { useNavigation } from "expo-router";
|
|
import { useEffect, useLayoutEffect, useState } from "react";
|
|
import { StatusBar } from "expo-status-bar";
|
|
import CustomerCareIcon from "../../assets/icons/customer-care.svg";
|
|
import ServiceReminderCard from "@/components/home/ServiceReminderCard";
|
|
import MetricCard from "@/components/home/MetricCard";
|
|
import PaymentDueCard from "@/components/home/PaymentDueCard";
|
|
import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps";
|
|
import BatteryWarrantyCard from "@/components/home/BatteryWarrantyCars";
|
|
import CustomerSupportModal from "@/components/home/CustomerSupportModal";
|
|
import { useSelector } from "react-redux";
|
|
import { AppDispatch, RootState } from "@/store";
|
|
import LocationOff from "@/assets/icons/location_off.svg";
|
|
import { Linking } from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
import ProfileImage from "@/components/home/Profile";
|
|
import { calculateBearing, calculateDistance } from "@/utils/Map";
|
|
import { useDispatch } from "react-redux";
|
|
import RefreshIcon from "@/assets/icons/refresh.svg";
|
|
|
|
import { payments } from "@/constants/config";
|
|
import {
|
|
clearUser,
|
|
getPaymentSummary,
|
|
getUserDetails,
|
|
} from "@/store/userSlice";
|
|
import { setDueAmount } from "@/store/paymentSlice";
|
|
import { Image } from "expo-image";
|
|
import EMINotification from "@/components/Payments/EmiNotification";
|
|
import { initSocket } from "@/services/socket";
|
|
import { useSnackbar } from "@/contexts/Snackbar";
|
|
|
|
export default function HomeScreen() {
|
|
const { t } = useTranslation();
|
|
const navigation = useNavigation();
|
|
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
|
|
const { SoC, SoH, chargingState, lat, lon, loading, totalDistance, error } =
|
|
useSelector((state: RootState) => state.telemetry);
|
|
|
|
const { showSnackbar } = useSnackbar();
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const spinValue = useState(new Animated.Value(0))[0];
|
|
|
|
const {
|
|
data,
|
|
paymentSummary,
|
|
loading: userLoading,
|
|
} = useSelector((state: RootState) => state.user);
|
|
|
|
const isRefreshing = refreshing || userLoading;
|
|
|
|
const [prevPosition, setPrevPosition] = useState<{
|
|
lat: number;
|
|
lon: number;
|
|
} | null>(null);
|
|
const [bearing, setBearing] = useState<number>(0);
|
|
const dispatch = useDispatch<AppDispatch>();
|
|
const vehicle =
|
|
Array.isArray(data?.vehicles) && data.vehicles.length > 0
|
|
? data.vehicles[0]
|
|
: null;
|
|
const battery =
|
|
Array.isArray(data?.batteries) && data.batteries.length > 0
|
|
? data.batteries[0]
|
|
: null;
|
|
|
|
const model = vehicle?.model ?? "---";
|
|
const chasisNumber = vehicle?.chasis_number ?? "---";
|
|
const warrantyStartDate = data?.batteries[0]?.warranty_start_date || null;
|
|
const warrantyEndDate = data?.batteries[0]?.warranty_end_date || null;
|
|
|
|
useEffect(() => {
|
|
if (lat && lon) {
|
|
if (prevPosition) {
|
|
// Calculate bearing between prev and current position
|
|
const newBearing = calculateBearing(
|
|
prevPosition.lat,
|
|
prevPosition.lon,
|
|
lat,
|
|
lon
|
|
);
|
|
setBearing(newBearing);
|
|
}
|
|
setPrevPosition({ lat, lon }); // Update previous position
|
|
}
|
|
}, [lat, lon]);
|
|
|
|
useEffect(() => {
|
|
if (lat && lon) {
|
|
if (prevPosition) {
|
|
const distance = calculateDistance(
|
|
prevPosition.lat,
|
|
prevPosition.lon,
|
|
lat,
|
|
lon
|
|
);
|
|
if (distance > 5) {
|
|
// Only update bearing if moved >5 meters
|
|
const newBearing = calculateBearing(
|
|
prevPosition.lat,
|
|
prevPosition.lon,
|
|
lat,
|
|
lon
|
|
);
|
|
setBearing(newBearing);
|
|
}
|
|
}
|
|
setPrevPosition({ lat, lon });
|
|
}
|
|
}, [lat, lon]);
|
|
const router = useRouter();
|
|
|
|
useLayoutEffect(() => {
|
|
navigation.setOptions({
|
|
headerStyle: {
|
|
backgroundColor: "#F3F5F8",
|
|
},
|
|
headerTitle: () => (
|
|
<View style={styles.headerTitleContainer}>
|
|
<View style={styles.logoContainer}>
|
|
<Image
|
|
source={require("../../assets/images/lio_logo.png")}
|
|
style={styles.logo}
|
|
/>
|
|
</View>
|
|
<View>
|
|
<Text style={styles.title}>{model}</Text>
|
|
<Text style={styles.subtitle}>{chasisNumber}</Text>
|
|
</View>
|
|
</View>
|
|
),
|
|
headerRight: () => (
|
|
<View style={styles.rightContainer}>
|
|
<Pressable onPress={handleManualRefresh} disabled={isRefreshing}>
|
|
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
|
<RefreshIcon
|
|
width={50}
|
|
height={50}
|
|
opacity={isRefreshing ? 0.5 : 1}
|
|
/>
|
|
</Animated.View>
|
|
</Pressable>
|
|
<Pressable
|
|
style={styles.supportButton}
|
|
onPress={() => {
|
|
setIsSupportModalVisible(true);
|
|
console.log("hkdlfjaldf");
|
|
}}
|
|
>
|
|
<CustomerCareIcon width={50} height={50} />
|
|
</Pressable>
|
|
<View>
|
|
<ProfileImage
|
|
username={data?.name || "Vec"}
|
|
textSize={20}
|
|
boxSize={40}
|
|
onClick={() => {
|
|
router.push("/user/profile");
|
|
}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
),
|
|
});
|
|
}, [navigation, data, model, chasisNumber, isRefreshing]);
|
|
|
|
const openInGoogleMaps = () => {
|
|
const url = `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`;
|
|
Linking.openURL(url).catch((err) =>
|
|
console.error("Failed to open Google Maps:", err)
|
|
);
|
|
};
|
|
|
|
const startSpin = () => {
|
|
spinValue.setValue(0);
|
|
Animated.loop(
|
|
Animated.timing(spinValue, {
|
|
toValue: 1,
|
|
duration: 1000,
|
|
easing: Easing.linear,
|
|
useNativeDriver: true,
|
|
})
|
|
).start();
|
|
};
|
|
|
|
const stopSpin = () => {
|
|
spinValue.stopAnimation();
|
|
spinValue.setValue(0);
|
|
};
|
|
|
|
const spin = spinValue.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ["0deg", "360deg"],
|
|
});
|
|
|
|
const handleManualRefresh = async () => {
|
|
try {
|
|
setRefreshing(true);
|
|
startSpin();
|
|
|
|
await dispatch(clearUser());
|
|
await Promise.all([
|
|
dispatch(getUserDetails()).unwrap(),
|
|
dispatch(getPaymentSummary()).unwrap(),
|
|
]);
|
|
|
|
await initSocket();
|
|
} catch (error: any) {
|
|
if (error.message === "Network Error" || !error.response) {
|
|
showSnackbar(t("common.no-internet-connection"), "error");
|
|
} else {
|
|
showSnackbar(t("common.something-went-wrong"), "error");
|
|
}
|
|
} finally {
|
|
stopSpin();
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
const {
|
|
daysLeftToPayEmi,
|
|
daysLeftToPayEmiText,
|
|
regularServiceDueInDaysText,
|
|
due_amount,
|
|
} = paymentSummary || {};
|
|
|
|
useEffect(() => {
|
|
if (paymentSummary?.due_amount != null) {
|
|
dispatch(setDueAmount(paymentSummary.due_amount));
|
|
}
|
|
}, [paymentSummary, dispatch]);
|
|
|
|
return (
|
|
<View style={{ flex: 1, backgroundColor: "#F3F5F8" }}>
|
|
<StatusBar style="dark" />
|
|
<ScrollView contentContainerStyle={styles.container} style={{ flex: 1 }}>
|
|
{daysLeftToPayEmi != undefined &&
|
|
Boolean(daysLeftToPayEmi && daysLeftToPayEmi > 0) && (
|
|
<ServiceReminderCard
|
|
type={
|
|
daysLeftToPayEmi >= payments.EMI_WARNING_DAYS_THRESHOLD
|
|
? "warning"
|
|
: "danger"
|
|
}
|
|
message={`${daysLeftToPayEmi} ${t("home.days-left-to-pay-emi")}`}
|
|
subMessage={t("home.pay-now")}
|
|
redirectPath="/(tabs)/payments"
|
|
/>
|
|
)}
|
|
|
|
{regularServiceDueInDaysText &&
|
|
Boolean(
|
|
regularServiceDueInDaysText &&
|
|
regularServiceDueInDaysText.trim() !== ""
|
|
) && (
|
|
<ServiceReminderCard
|
|
type="info"
|
|
message={regularServiceDueInDaysText}
|
|
subMessage="Service Reminder"
|
|
redirectPath="/(tabs)/service"
|
|
/>
|
|
)}
|
|
|
|
<View style={styles.iconContainer}>
|
|
<SemiCircleProgress progress={SoC} status={chargingState} />
|
|
</View>
|
|
<View style={styles.metrics}>
|
|
<MetricCard heading={t("home.battery-health")} value={SoH} unit="%" />
|
|
<MetricCard
|
|
heading={t("home.total-distance")}
|
|
value={totalDistance}
|
|
unit={t("home.km")}
|
|
/>
|
|
</View>
|
|
{due_amount && due_amount > 0 ? (
|
|
<PaymentDueCard
|
|
label={t("home.payment-due")}
|
|
amount={due_amount}
|
|
onPress={() => {
|
|
router.push("/payments/selectAmount");
|
|
}}
|
|
/>
|
|
) : due_amount == 0 ? (
|
|
<EMINotification
|
|
message={`${t("home.payment-complete")}`}
|
|
actionText={`${t("home.view-details")}`}
|
|
onActionPress={() => router.push("/(tabs)/payments")}
|
|
/>
|
|
) : null}
|
|
|
|
<View style={styles.map}>
|
|
{error ? (
|
|
<View style={styles.errorContainer}>
|
|
<LocationOff />
|
|
<Text style={styles.errorText}>{t("home.error-location")}</Text>
|
|
</View>
|
|
) : lat != null && lon != null && !(lat == 0 && lon == 0) ? (
|
|
<>
|
|
<View style={styles.mapContainer}>
|
|
<MapView
|
|
provider={PROVIDER_GOOGLE}
|
|
style={styles.mapStyle}
|
|
region={{
|
|
latitude: lat,
|
|
longitude: lon,
|
|
latitudeDelta: 0.0922,
|
|
longitudeDelta: 0.0421,
|
|
}}
|
|
// customMapStyle={mapStyle}
|
|
userInterfaceStyle="light"
|
|
>
|
|
<Marker
|
|
draggable
|
|
coordinate={{
|
|
latitude: lat,
|
|
longitude: lon,
|
|
}}
|
|
rotation={bearing}
|
|
anchor={{ x: 0.5, y: 0.5 }}
|
|
tracksViewChanges={false}
|
|
>
|
|
<Image
|
|
source={require("../../assets/images/marker.png")}
|
|
style={{ height: 35, width: 35 }}
|
|
/>
|
|
</Marker>
|
|
</MapView>
|
|
</View>
|
|
<TouchableOpacity>
|
|
<Text
|
|
style={styles.viewLocationText}
|
|
onPress={openInGoogleMaps}
|
|
>
|
|
{t("home.view-vehicle-location")}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</>
|
|
) : (
|
|
<View style={styles.errorContainer}>
|
|
<LocationOff />
|
|
<Text style={styles.errorText}>
|
|
{t("home.fetching-location")}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<TouchableOpacity onPress={() => router.push("/(tabs)/my-battery")}>
|
|
<BatteryWarrantyCard
|
|
warrantyStartDate={warrantyStartDate}
|
|
warrantyEndDate={warrantyEndDate}
|
|
/>
|
|
</TouchableOpacity>
|
|
</ScrollView>
|
|
<CustomerSupportModal
|
|
visible={isSupportModalVisible}
|
|
onClose={() => setIsSupportModalVisible(false)}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const mapStyle = [
|
|
{ elementType: "geometry", stylers: [{ color: "#242f3e" }] },
|
|
{ elementType: "labels.text.fill", stylers: [{ color: "#746855" }] },
|
|
{ elementType: "labels.text.stroke", stylers: [{ color: "#242f3e" }] },
|
|
{
|
|
featureType: "administrative.locality",
|
|
elementType: "labels.text.fill",
|
|
stylers: [{ color: "#d59563" }],
|
|
},
|
|
{
|
|
featureType: "poi",
|
|
elementType: "labels.text.fill",
|
|
stylers: [{ color: "#d59563" }],
|
|
},
|
|
{
|
|
featureType: "poi.park",
|
|
elementType: "geometry",
|
|
stylers: [{ color: "#263c3f" }],
|
|
},
|
|
{
|
|
featureType: "poi.park",
|
|
elementType: "labels.text.fill",
|
|
stylers: [{ color: "#6b9a76" }],
|
|
},
|
|
{
|
|
featureType: "road",
|
|
elementType: "geometry",
|
|
stylers: [{ color: "#38414e" }],
|
|
},
|
|
{
|
|
featureType: "road",
|
|
elementType: "geometry.stroke",
|
|
stylers: [{ color: "#212a37" }],
|
|
},
|
|
{
|
|
featureType: "road",
|
|
elementType: "labels.text.fill",
|
|
stylers: [{ color: "#9ca5b3" }],
|
|
},
|
|
{
|
|
featureType: "road.highway",
|
|
elementType: "geometry",
|
|
stylers: [{ color: "#746855" }],
|
|
},
|
|
{
|
|
featureType: "road.highway",
|
|
elementType: "geometry.stroke",
|
|
stylers: [{ color: "#1f2835" }],
|
|
},
|
|
{
|
|
featureType: "road.highway",
|
|
elementType: "labels.text.fill",
|
|
stylers: [{ color: "#f3d19c" }],
|
|
},
|
|
{
|
|
featureType: "transit",
|
|
elementType: "geometry",
|
|
stylers: [{ color: "#2f3948" }],
|
|
},
|
|
{
|
|
featureType: "transit.station",
|
|
elementType: "labels.text.fill",
|
|
stylers: [{ color: "#d59563" }],
|
|
},
|
|
{
|
|
featureType: "water",
|
|
elementType: "geometry",
|
|
stylers: [{ color: "#17263c" }],
|
|
},
|
|
{
|
|
featureType: "water",
|
|
elementType: "labels.text.fill",
|
|
stylers: [{ color: "#515c6d" }],
|
|
},
|
|
{
|
|
featureType: "water",
|
|
elementType: "labels.text.stroke",
|
|
stylers: [{ color: "#17263c" }],
|
|
},
|
|
];
|
|
|
|
const styles = StyleSheet.create({
|
|
logo: {
|
|
width: "100%",
|
|
height: "100%",
|
|
resizeMode: "contain",
|
|
},
|
|
logoContainer: {
|
|
padding: 8,
|
|
borderRadius: 44 / 2, // make it perfectly round
|
|
borderWidth: 2,
|
|
borderColor: "#E5E9F0",
|
|
width: 44,
|
|
height: 44,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
overflow: "hidden", // ensures image doesn't overflow
|
|
},
|
|
errorText: {
|
|
color: "#565F70",
|
|
fontWeight: "400",
|
|
fontSize: 16,
|
|
textAlign: "center",
|
|
},
|
|
errorContainer: {
|
|
height: 255,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
backgroundColor: "#FCFCFC",
|
|
gap: 8,
|
|
},
|
|
viewLocationText: {
|
|
color: "#388E3C",
|
|
fontWeight: "600",
|
|
fontSize: 16,
|
|
textAlign: "center",
|
|
marginTop: 8,
|
|
},
|
|
map: {
|
|
padding: 12,
|
|
borderRadius: 10,
|
|
overflow: "hidden",
|
|
backgroundColor: "#fff",
|
|
},
|
|
mapContainer: {
|
|
height: 255,
|
|
},
|
|
mapStyle: {
|
|
width: "100%",
|
|
height: "100%",
|
|
borderRadius: 10,
|
|
},
|
|
metrics: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
backgroundColor: "#F3F5F8",
|
|
},
|
|
container: {
|
|
padding: 16,
|
|
gap: 16,
|
|
paddingBottom: 110,
|
|
},
|
|
iconContainer: {
|
|
backgroundColor: "#FCFCFC",
|
|
},
|
|
supportButton: {
|
|
backgroundColor: "#F3F5F8",
|
|
},
|
|
headerTitleContainer: {
|
|
flexDirection: "row",
|
|
backgroundColor: "#F3F5F8",
|
|
gap: 8,
|
|
},
|
|
title: {
|
|
fontSize: 14,
|
|
color: "#6B7280",
|
|
fontWeight: "500",
|
|
},
|
|
subtitle: {
|
|
fontSize: 18,
|
|
color: "#111827",
|
|
fontWeight: "700",
|
|
},
|
|
rightContainer: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingRight: 16,
|
|
gap: 4,
|
|
backgroundColor: "#F3F5F8",
|
|
},
|
|
badge: {
|
|
backgroundColor: "#FEE2E2",
|
|
borderRadius: 20,
|
|
width: 30,
|
|
height: 30,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
badgeText: {
|
|
color: "#DC2626",
|
|
fontWeight: "700",
|
|
},
|
|
});
|