BaaS_Driver_Android_App/app/(tabs)/index.tsx

515 lines
14 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 api from "@/services/axiosClient";
import { setDueAmount } from "@/store/paymentSlice";
import { EmiResponse } from "./payments";
import { Image } from "expo-image";
import EMINotification from "@/components/Payments/EmiNotification";
export default function HomeScreen() {
const { t } = useTranslation();
const navigation = useNavigation();
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
const { SoC, SoH, chargingState, lat, lon, loading, error, totalDistance } =
useSelector((state: RootState) => state.telemetry);
const [refreshing, setRefreshing] = useState(false);
const spinValue = useState(new Animated.Value(0))[0];
const { data, paymentSummary } = useSelector(
(state: RootState) => state.user
);
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}>
<Text style={styles.title}>{model}</Text>
<Text style={styles.subtitle}>{chasisNumber}</Text>
</View>
),
headerRight: () => (
<View style={styles.rightContainer}>
<Pressable onPress={handleManualRefresh} disabled={refreshing}>
<Animated.View style={{ transform: [{ rotate: spin }] }}>
<RefreshIcon
width={50}
height={50}
opacity={refreshing ? 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, refreshing]);
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 dispatch(getUserDetails()).unwrap();
await dispatch(getPaymentSummary()).unwrap();
console.log("Manual refresh complete");
} catch (error) {
console.error("Manual refresh failed", 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 && daysLeftToPayEmiText && daysLeftToPayEmi > 0 && (
<ServiceReminderCard
type={
daysLeftToPayEmi >= payments.EMI_WARNING_DAYS_THRESHOLD
? "warning"
: "danger"
}
message={daysLeftToPayEmiText}
subMessage={t("home.pay-now")}
redirectPath="/(tabs)/payments"
/>
)}
{regularServiceDueInDaysText && (
<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 && (
<>
<PaymentDueCard
label={t("home.payment-due")}
amount={due_amount}
onPress={() => {
router.push("/payments/selectAmount");
}}
/>
<EMINotification
message="Payment Complete"
actionText="View Details"
onActionPress={() => router.push("/(tabs)/payments")}
/>
</>
)}
<View style={styles.map}>
{loading ? (
<View style={styles.errorContainer}>
<LocationOff />
<Text style={styles.errorText}>Fetching 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.error-location")}</Text>
</View>
)}
</View>
{warrantyEndDate && warrantyStartDate && (
<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({
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: "column",
backgroundColor: "#F3F5F8",
},
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",
},
});