The drivers table was seeded with 4 fake fixtures (Karim/Rana/Omar/Layal) using randomuser.me/unsplash placeholder images. They had no user_id, no position, and were excluded from matching and the rider map by design, so they only ever cluttered the dashboard. The table now starts empty — real drivers are created in-app via onboarding (driver/profile POST), which links a row to a real user account and gives it a live GPS position. Dropped the dead random-offset scatter in generateMarkersFromData that fabricated fake driver positions for drivers without GPS. It was already unreachable (the null-position filter excluded those drivers), so this is stub cleanup, not a behavior change — drivers without a real position simply aren't rendered. The 4 fixture rows were also removed from the live Neon DB (user_id IS NULL); real onboarded drivers were untouched. Co-Authored-By: Claude <noreply@anthropic.com>
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
import { calculateFare } from "@/lib/pricing";
|
|
import { DEFAULT_SERVICE, type ServiceId } from "@/constants/services";
|
|
import type { Driver, MarkerData } from "@/types/type";
|
|
|
|
const directionsAPI = process.env.EXPO_PUBLIC_GOOGLE_API_KEY;
|
|
|
|
// Build map markers from driver rows. Only drivers reporting a real GPS
|
|
// position are shown — there is no fallback/scatter, so drivers without a
|
|
// position (none, now that seed fixtures are gone) simply aren't rendered.
|
|
export const generateMarkersFromData = ({
|
|
data,
|
|
}: {
|
|
data: Driver[];
|
|
userLatitude: number;
|
|
userLongitude: number;
|
|
}): MarkerData[] => {
|
|
return data
|
|
.filter((driver) => driver.latitude != null && driver.longitude != null)
|
|
.map((driver) => ({
|
|
...driver,
|
|
latitude: driver.latitude as number,
|
|
longitude: driver.longitude as number,
|
|
title: `${driver.first_name} ${driver.last_name}`,
|
|
}));
|
|
};
|
|
|
|
export const calculateRegion = ({
|
|
userLatitude,
|
|
userLongitude,
|
|
destinationLatitude,
|
|
destinationLongitude,
|
|
}: {
|
|
userLatitude: number | null;
|
|
userLongitude: number | null;
|
|
destinationLatitude?: number | null;
|
|
destinationLongitude?: number | null;
|
|
}) => {
|
|
if (!userLatitude || !userLongitude) {
|
|
// Default to Beirut, Lebanon.
|
|
return {
|
|
latitude: 33.8938,
|
|
longitude: 35.5018,
|
|
latitudeDelta: 0.09,
|
|
longitudeDelta: 0.09,
|
|
};
|
|
}
|
|
|
|
if (!destinationLatitude || !destinationLongitude) {
|
|
return {
|
|
latitude: userLatitude,
|
|
longitude: userLongitude,
|
|
latitudeDelta: 0.01,
|
|
longitudeDelta: 0.01,
|
|
};
|
|
}
|
|
|
|
const minLat = Math.min(userLatitude, destinationLatitude);
|
|
const maxLat = Math.max(userLatitude, destinationLatitude);
|
|
const minLng = Math.min(userLongitude, destinationLongitude);
|
|
const maxLng = Math.max(userLongitude, destinationLongitude);
|
|
|
|
const latitudeDelta = (maxLat - minLat) * 1.3; // Adding some padding
|
|
const longitudeDelta = (maxLng - minLng) * 1.3; // Adding some padding
|
|
|
|
const latitude = (userLatitude + destinationLatitude) / 2;
|
|
const longitude = (userLongitude + destinationLongitude) / 2;
|
|
|
|
return {
|
|
latitude,
|
|
longitude,
|
|
latitudeDelta,
|
|
longitudeDelta,
|
|
};
|
|
};
|
|
|
|
// Per-driver ETA + fare. The rider pays for the trip leg only (distance +
|
|
// duration) — never the driver's approach leg. `service` drives the fare
|
|
// multiplier.
|
|
export const calculateDriverTimes = async ({
|
|
markers,
|
|
userLatitude,
|
|
userLongitude,
|
|
destinationLatitude,
|
|
destinationLongitude,
|
|
service = DEFAULT_SERVICE,
|
|
}: {
|
|
markers: MarkerData[];
|
|
userLatitude: number | null;
|
|
userLongitude: number | null;
|
|
destinationLatitude: number | null;
|
|
destinationLongitude: number | null;
|
|
service?: ServiceId;
|
|
}) => {
|
|
if (
|
|
!userLatitude ||
|
|
!userLongitude ||
|
|
!destinationLatitude ||
|
|
!destinationLongitude
|
|
)
|
|
return;
|
|
|
|
try {
|
|
const timesPromises = markers.map(async (marker) => {
|
|
const responseToUser = await fetch(
|
|
`https://maps.googleapis.com/maps/api/directions/json?origin=${marker.latitude},${marker.longitude}&destination=${userLatitude},${userLongitude}&key=${directionsAPI}`,
|
|
);
|
|
const dataToUser = await responseToUser.json();
|
|
|
|
const responseToDestination = await fetch(
|
|
`https://maps.googleapis.com/maps/api/directions/json?origin=${userLatitude},${userLongitude}&destination=${destinationLatitude},${destinationLongitude}&key=${directionsAPI}`,
|
|
);
|
|
const dataToDestination = await responseToDestination.json();
|
|
|
|
// Google returns no routes when a leg is unreachable (ZERO_RESULTS).
|
|
const legToUser = dataToUser.routes?.[0]?.legs?.[0];
|
|
const legToDestination = dataToDestination.routes?.[0]?.legs?.[0];
|
|
if (!legToUser || !legToDestination) {
|
|
return { ...marker, time: 0, price: "0.00" };
|
|
}
|
|
|
|
const timeToUser = legToUser.duration.value; // Pickup ETA in seconds
|
|
const timeToDestination = legToDestination.duration.value; // Trip duration in seconds
|
|
|
|
// The rider pays for the trip leg only (distance + duration) —
|
|
// never for the driver's approach.
|
|
const price = calculateFare(
|
|
{
|
|
distanceMeters: legToDestination.distance.value,
|
|
durationSeconds: timeToDestination,
|
|
},
|
|
service,
|
|
);
|
|
|
|
const totalTripTime = (timeToUser + timeToDestination) / 60; // Minutes until drop-off
|
|
|
|
return { ...marker, time: totalTripTime, price };
|
|
});
|
|
|
|
return await Promise.all(timesPromises);
|
|
} catch (error) {
|
|
console.error("Error calculating driver times:", error);
|
|
}
|
|
};
|
|
|
|
// A single trip-leg fare estimate for the confirm-ride screen. One Directions
|
|
// call instead of one per driver, since the trip leg is the same regardless of
|
|
// which driver arrives. Returns { fare, durationSeconds, distanceMeters } or
|
|
// null when the route is unreachable.
|
|
export const calculateTripFare = async ({
|
|
userLatitude,
|
|
userLongitude,
|
|
destinationLatitude,
|
|
destinationLongitude,
|
|
service = DEFAULT_SERVICE,
|
|
}: {
|
|
userLatitude: number | null;
|
|
userLongitude: number | null;
|
|
destinationLatitude: number | null;
|
|
destinationLongitude: number | null;
|
|
service?: ServiceId;
|
|
}): Promise<{
|
|
fare: string;
|
|
durationSeconds: number;
|
|
distanceMeters: number;
|
|
} | null> => {
|
|
if (
|
|
!userLatitude ||
|
|
!userLongitude ||
|
|
!destinationLatitude ||
|
|
!destinationLongitude
|
|
)
|
|
return null;
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`https://maps.googleapis.com/maps/api/directions/json?origin=${userLatitude},${userLongitude}&destination=${destinationLatitude},${destinationLongitude}&key=${directionsAPI}`,
|
|
);
|
|
const data = await response.json();
|
|
const leg = data.routes?.[0]?.legs?.[0];
|
|
if (!leg) return null;
|
|
|
|
return {
|
|
fare: calculateFare(
|
|
{
|
|
distanceMeters: leg.distance.value,
|
|
durationSeconds: leg.duration.value,
|
|
},
|
|
service,
|
|
),
|
|
durationSeconds: leg.duration.value,
|
|
distanceMeters: leg.distance.value,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error calculating trip fare:", error);
|
|
return null;
|
|
}
|
|
}; |