chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
+491
View File
@@ -0,0 +1,491 @@
import apiClient from "./client";
import type {
AuthResponse,
ClientResponse,
CommandResponse,
Product,
DeliveryPerson,
DeliveryPersonsStats,
Alert,
} from "./types";
const V2 = "http://172.20.167.237:8080/api/v2";
const CABINE_URL = "http://172.20.167.237:8080/api/v1/cabine";
// ============================================
// AUTH
// ============================================
export const loginAdmin = async (
username: string,
password: string,
): Promise<AuthResponse> => {
try {
const { data } = await apiClient.post(`${V2}/admin/auth/login`, {
username,
password,
});
if (!data.access_token) {
return { success: false, message: "Token non reçu du serveur" };
}
return {
success: true,
message: "Connexion réussie",
access_token: data.access_token,
token_type: data.token_type,
expires_in: data.expires_in,
user: data.user,
};
} catch (error: any) {
return {
success: false,
message:
error.response?.data?.error ||
error.message ||
"Erreur de connexion",
};
}
};
export const logoutAdmin = async (): Promise<void> => {
try {
await apiClient.post(`${V2}/admin/auth/logout`);
} catch {
/* ignore */
}
};
// ============================================
// CLIENTS
// ============================================
export const getAllClients = async (): Promise<ClientResponse[]> => {
const { data } = await apiClient.get(`${V2}/admin/protected/all/clients`);
return data.clients || [];
};
export const getAllUsers = async () => {
const { data } = await apiClient.get(`${V2}/admin/protected/all/users`);
return data.users || [];
};
export const updateClientByAdmin = async (
clientId: number,
updates: Record<string, any>,
) => {
const { data } = await apiClient.put(
`${V2}/admin/protected/clients/${clientId}`,
updates,
);
return { success: true, message: data.message, client: data.client };
};
export const updateUserByAdmin = async (
userId: number,
updates: Record<string, any>,
) => {
const { data } = await apiClient.put(
`${V2}/admin/protected/users/${userId}`,
updates,
);
return { success: true, message: data.message, user: data.user };
};
// ============================================
// COMMANDES
// ============================================
export const getAllCommands = async (status?: string, username?: string) => {
let url = `${V2}/admin/protected/orders`;
const params: string[] = [];
if (status) params.push(`status=${status}`);
if (username) params.push(`username=${username}`);
if (params.length) url += `?${params.join("&")}`;
const { data } = await apiClient.get(url);
return {
success: true,
commands: data.commands || [],
count: data.count || 0,
};
};
export const getCommandByID = async (commandId: number) => {
const { data } = await apiClient.get(
`${V2}/admin/protected/orders/${commandId}`,
);
return { success: true, command: data.command };
};
export const getCommandItems = async (commandId: number) => {
const { data } = await apiClient.get(
`${CABINE_URL}/commands/${commandId}/items`,
);
return {
success: true,
items: data.items || [],
count: data.count || 0,
command_info: data.command_info,
client_info: data.client_info,
};
};
export const updateCommandStatus = async (
commandId: number,
newStatus: string,
) => {
const { data } = await apiClient.put(
`${V2}/admin/protected/orders/${commandId}/status`,
{ status: newStatus },
);
return { success: true, message: data.message };
};
export const updateCommandAddress = async (
commandId: number,
newAddress: string,
) => {
const { data } = await apiClient.put(
`${V2}/admin/protected/orders/${commandId}/address`,
{ delivery_address: newAddress },
);
return { success: true, message: data.message };
};
export const validateCommand = async (commandId: number) => {
const { data } = await apiClient.post(
`${V2}/admin/protected/orders/${commandId}/force-validate`,
);
return { success: true, message: data.message };
};
// ============================================
// LIVREURS
// ============================================
export const getAvailableDeliveryPersons = async () => {
const { data } = await apiClient.get(
`${V2}/admin/protected/delivery-persons`,
);
return {
success: true,
livreurs: data.livreurs || [],
count: data.count || 0,
};
};
export const assignDeliveryPerson = async (
commandId: number,
deliveryUsername: string,
) => {
const { data } = await apiClient.post(
`${V2}/admin/protected/delivery-persons/${deliveryUsername}/assign/${commandId}`,
);
return { success: true, message: data.message };
};
export const getDeliveryPersonDetails = async (username: string) => {
const { data } = await apiClient.get(
`${V2}/admin/protected/delivery-persons/${username}`,
);
return data.deliveryman || data;
};
const parseStatus = (status: any): "available" | "busy" | "offline" => {
if (!status) return "offline";
if (status === "available" || status === "busy" || status === "offline")
return status;
if (typeof status === "string" && status.startsWith("{")) {
try {
const obj = JSON.parse(status);
if (
obj.status === "available" ||
obj.status === "busy" ||
obj.status === "offline"
)
return obj.status;
} catch {
/* ignore */
}
}
return "offline";
};
export const getAllDeliveryPersonsWithDetails = async (): Promise<{
success: boolean;
deliveryPersons: DeliveryPerson[];
count: number;
stats: DeliveryPersonsStats;
}> => {
try {
const { data } = await apiClient.get(
`${V2}/admin/protected/delivery-persons`,
);
const livreurs = data.livreurs || [];
const enriched = await Promise.all(
livreurs.map(async (l: any): Promise<DeliveryPerson> => {
try {
const details = await getDeliveryPersonDetails(l.username);
const parsedStatus = parseStatus(details.status);
const hasLoc =
details.location?.latitude &&
details.location?.longitude;
return {
id: l.id,
username: l.username,
status: parsedStatus,
location: {
latitude: hasLoc ? details.location.latitude : 0,
longitude: hasLoc ? details.location.longitude : 0,
last_update: details.location?.last_update
? new Date(
details.location.last_update * 1000,
).toISOString()
: new Date().toISOString(),
is_recent: details.location?.is_recent || false,
},
stats: {
total_deliveries: details.total_deliveries || 0,
completed_today: details.completed_deliveries || 0,
queue_size: details.queue_size || 0,
current_command: details.current_command || null,
},
};
} catch {
return {
id: l.id,
username: l.username,
status: "offline",
location: {
latitude: 0,
longitude: 0,
last_update: new Date().toISOString(),
is_recent: false,
},
stats: {
total_deliveries: 0,
completed_today: 0,
queue_size: 0,
current_command: null,
},
};
}
}),
);
const stats: DeliveryPersonsStats = {
total: enriched.length,
available: enriched.filter((d) => d.status === "available").length,
busy: enriched.filter((d) => d.status === "busy").length,
offline: enriched.filter((d) => d.status === "offline").length,
active_deliveries: enriched.filter(
(d) => d.stats.current_command !== null,
).length,
};
return {
success: true,
deliveryPersons: enriched,
count: enriched.length,
stats,
};
} catch {
return {
success: false,
deliveryPersons: [],
count: 0,
stats: {
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0,
},
};
}
};
export const getDeliverymanLocationForCommand = async (commandId: number) => {
try {
const { data } = await apiClient.get(
`${V2}/admin/protected/commands/${commandId}/deliveryman/location`,
);
return { success: true, data: data.data };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur",
};
}
};
// ============================================
// PRODUITS
// ============================================
export const getAllProductsAdmin = async (): Promise<{
success: boolean;
data: Product[];
count: number;
}> => {
try {
const { data } = await apiClient.get(`${V2}/admin/protected/products`);
return { success: true, data: data.data || [], count: data.count || 0 };
} catch {
return { success: false, data: [], count: 0 };
}
};
export const createProductAdmin = async (formData: FormData) => {
const { data } = await apiClient.post(
`${V2}/admin/protected/products`,
formData,
{
headers: { "Content-Type": "multipart/form-data" },
},
);
return { success: true, data: data.data, message: data.message };
};
export const updateProductAdmin = async (
productId: number,
updates: Record<string, any>,
) => {
const { data } = await apiClient.put(
`${V2}/admin/protected/products/${productId}`,
updates,
);
return { success: true, data: data.data, message: data.message };
};
export const deleteProductAdmin = async (productId: number) => {
const { data } = await apiClient.delete(
`${V2}/admin/protected/products/${productId}`,
);
return { success: true, message: data.message };
};
// ============================================
// CREATION UTILISATEURS / CLIENTS
// ============================================
export const createClientByAdmin = async (data: {
username: string;
password: string;
nom: string;
prenom: string;
telephone: string;
}) => {
const { data: res } = await apiClient.post(
`http://172.20.167.237:8080/api/v1/auth/register`,
data,
);
return {
success: true,
message: res.message || "Client créé",
user: res.user,
};
};
export const createUserByAdmin = async (data: {
username: string;
password: string;
role: string;
}) => {
const { data: res } = await apiClient.post(
`${V2}/admin/auth/register`,
data,
);
return {
success: true,
message: res.message || "Utilisateur créé",
user: res.user,
};
};
// ============================================
// SUPPRESSION UTILISATEURS / CLIENTS
// ============================================
export const deleteUserAdmin = async (userId: number) => {
const { data } = await apiClient.delete(
`${V2}/admin/protected/users/${userId}`,
);
return { success: true, message: data.message };
};
export const deleteClientAdmin = async (clientId: number) => {
const { data } = await apiClient.delete(
`${V2}/admin/protected/clients/${clientId}`,
);
return { success: true, message: data.message };
};
// ============================================
// ALERTES
// ============================================
export const getAdminAlerts = async (): Promise<{
success: boolean;
alerts: Alert[];
count: number;
}> => {
try {
const { data } = await apiClient.get(`${CABINE_URL}/alerts`);
return {
success: true,
alerts: data.alerts || [],
count: data.count || 0,
};
} catch {
return { success: false, alerts: [], count: 0 };
}
};
// ============================================
// MEDIAS PRODUITS
// ============================================
export const uploadProductMediaAdmin = async (
productId: number,
fileUri: string,
fileName: string,
fileType: string,
mediaType: "image" | "video",
) => {
const fd = new FormData();
fd.append("file", { uri: fileUri, name: fileName, type: fileType } as any);
fd.append("type", mediaType);
const { data } = await apiClient.post(
`${V2}/admin/protected/products/${productId}/media`,
fd,
{ headers: { "Content-Type": "multipart/form-data" } },
);
return { success: true, media: data.media, message: data.message };
};
export const deleteProductMediaAdmin = async (
productId: number,
mediaId: number,
) => {
const { data } = await apiClient.delete(
`${V2}/admin/protected/products/${productId}/media/${mediaId}`,
);
return { success: true, message: data.message };
};
// ============================================
// STATS HELPERS
// ============================================
export const getCommandCountByStatus = async (
status: string,
): Promise<number> => {
try {
const result = await getAllCommands(status);
return result.commands.length;
} catch {
return 0;
}
};
+278
View File
@@ -0,0 +1,278 @@
import apiClient from "./client";
import type {
OrderItem,
DeliveryPerson,
DeliveryPersonsStats,
Alert,
} from "./types";
const API = "http://172.20.167.237:8080/api/v1/cabine";
const V2 = "http://172.20.167.237:8080/api/v2";
// ============================================
// ITEMS
// ============================================
export const getCommandItems = async (commandId: number) => {
const { data } = await apiClient.get(`${API}/commands/${commandId}/items`);
return {
success: true,
items: (data.items || []) as OrderItem[],
count: data.count || 0,
command_info: data.command_info,
client_info: data.client_info,
};
};
export const updateItemStatus = async (itemId: number, status: string) => {
const { data } = await apiClient.put(`${API}/items/${itemId}/status`, {
status,
});
return { success: true, message: data.message };
};
// ============================================
// PENALITES
// ============================================
export const applyClientPenalty = async (
clientUsername: string,
reason: string,
amount?: number,
) => {
const { data } = await apiClient.post(`${API}/penalty`, {
client_username: clientUsername,
reason,
amount,
});
return { success: true, message: data.message, penalty: data.penalty };
};
export const getClientPenalties = async (clientUsername: string) => {
const { data } = await apiClient.get(
`${API}/client/${clientUsername}/penalties`,
);
return { success: true, penalties: data.penalties };
};
export const resetClientPenalties = async (clientUsername: string) => {
const { data } = await apiClient.post(
`${API}/client/${clientUsername}/penalties/reset`,
);
return { success: true, message: data.message };
};
export const getAllClientsWithPenalties = async () => {
const { data } = await apiClient.get(`${API}/penalties/all`);
return {
success: true,
clients: data.clients || [],
count: data.count || 0,
};
};
export const getPenaltiesStats = async () => {
const { data } = await apiClient.get(`${API}/penalties/stats`);
return { success: true, data: data.data };
};
// ============================================
// COMMANDES
// ============================================
export const getCancelledOrders = async () => {
const { data } = await apiClient.get(`${API}/commands/cancelled`);
return {
success: true,
commands: data.commands || [],
count: data.count || 0,
};
};
export const deleteCommand = async (commandId: number) => {
const { data } = await apiClient.delete(`${API}/commands/${commandId}`);
return { success: true, message: data.message };
};
export const getDeliverymanLocationForCommand = async (commandId: number) => {
try {
const { data } = await apiClient.get(
`${API}/commands/${commandId}/deliveryman/location`,
);
return { success: true, data: data.data };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur",
};
}
};
// ============================================
// LIVREURS
// ============================================
const parseStatus = (status: any): "available" | "busy" | "offline" => {
if (!status) return "offline";
if (status === "available" || status === "busy" || status === "offline")
return status;
if (typeof status === "string" && status.startsWith("{")) {
try {
return JSON.parse(status).status || "offline";
} catch {
/* ignore */
}
}
return "offline";
};
export const getAllDeliveryPersonsWithDetails = async (): Promise<{
success: boolean;
deliveryPersons: DeliveryPerson[];
count: number;
stats: DeliveryPersonsStats;
}> => {
try {
const { data } = await apiClient.get(`${API}/all/deliveryman`);
const users = data.users || [];
const enriched = await Promise.all(
users.map(async (u: any): Promise<DeliveryPerson> => {
try {
const { data: details } = await apiClient.get(
`${V2}/admin/protected/delivery-persons/${u.username}`,
);
const d = details.deliveryman || details;
const parsedStatus = parseStatus(d.status);
const hasLoc =
d.location?.latitude && d.location?.longitude;
return {
id: u.id,
username: u.username,
status: parsedStatus,
location: {
latitude: hasLoc ? d.location.latitude : 0,
longitude: hasLoc ? d.location.longitude : 0,
last_update: d.location?.last_update
? new Date(
d.location.last_update * 1000,
).toISOString()
: new Date().toISOString(),
is_recent: d.location?.is_recent || false,
},
stats: {
total_deliveries: d.total_deliveries || 0,
completed_today: d.completed_deliveries || 0,
queue_size: d.queue_size || 0,
current_command: d.current_command || null,
},
};
} catch {
return {
id: u.id,
username: u.username,
status: "offline",
location: {
latitude: 0,
longitude: 0,
last_update: new Date().toISOString(),
is_recent: false,
},
stats: {
total_deliveries: 0,
completed_today: 0,
queue_size: 0,
current_command: null,
},
};
}
}),
);
const stats: DeliveryPersonsStats = {
total: enriched.length,
available: enriched.filter((d) => d.status === "available").length,
busy: enriched.filter((d) => d.status === "busy").length,
offline: enriched.filter((d) => d.status === "offline").length,
active_deliveries: enriched.filter(
(d) => d.stats.current_command !== null,
).length,
};
return {
success: true,
deliveryPersons: enriched,
count: enriched.length,
stats,
};
} catch {
return {
success: false,
deliveryPersons: [],
count: 0,
stats: {
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0,
},
};
}
};
export const getDeliveryPersonMapLinks = async (username: string) => {
try {
const { data } = await apiClient.get(
`${API}/deliveryman/${username}/location`,
);
return {
success: true,
location: data.location,
map_links: data.map_links,
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur",
};
}
};
// ============================================
// ALERTES
// ============================================
export const getActiveAlerts = async (): Promise<{
success: boolean;
alerts: Alert[];
count: number;
}> => {
try {
const { data } = await apiClient.get(`${API}/alerts`);
return {
success: true,
alerts: data.alerts || [],
count: data.count || 0,
};
} catch {
return { success: false, alerts: [], count: 0 };
}
};
export const getAllAlerts = async (): Promise<{
success: boolean;
alerts: Alert[];
count: number;
}> => {
try {
const { data } = await apiClient.get(`${API}/all/alerts`);
return {
success: true,
alerts: data.alerts || [],
count: data.count || 0,
};
} catch {
return { success: false, alerts: [], count: 0 };
}
};
+252
View File
@@ -0,0 +1,252 @@
import apiClient from "./client";
import type {
DeliveryStatus,
QueueInfo,
DeliveryItem,
DeliveryDetails,
Alert,
} from "./types";
const API = "http://172.20.167.237:8080/api/v1/livreur";
// ============================================
// STATUT
// ============================================
export const getMyStatus = async (): Promise<{
success: boolean;
status?: DeliveryStatus;
error?: string;
}> => {
try {
const { data } = await apiClient.get(`${API}/status`);
return { success: true, status: data.status };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const updateMyStatus = async (
status: "available" | "busy" | "offline",
): Promise<{ success: boolean; message?: string; error?: string }> => {
try {
const { data } = await apiClient.post(`${API}/update/status`, {
status,
});
return { success: true, message: data.message };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
// ============================================
// QUEUE
// ============================================
export const getMyQueue = async (): Promise<{
success: boolean;
queue_info?: QueueInfo;
error?: string;
}> => {
try {
const { data } = await apiClient.get(`${API}/queue`);
return { success: true, queue_info: data.queue_info };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
// ============================================
// LIVRAISONS
// ============================================
export const getMyDeliveries = async (): Promise<{
success: boolean;
deliveries?: DeliveryItem[];
error?: string;
}> => {
try {
const { data } = await apiClient.get(`${API}/deliveries`);
return { success: true, deliveries: data.deliveries || [] };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const getDeliveryDetails = async (
deliveryId: number,
): Promise<{
success: boolean;
delivery?: DeliveryDetails;
error?: string;
}> => {
try {
const { data } = await apiClient.get(`${API}/deliveries/${deliveryId}`);
return {
success: true,
delivery: {
delivery: data.delivery,
client_info: data.client_info,
},
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const startDelivery = async (
deliveryId: number,
latitude: number,
longitude: number,
): Promise<{ success: boolean; message?: string; error?: string }> => {
try {
const { data } = await apiClient.post(
`${API}/deliveries/${deliveryId}/start`,
{ latitude, longitude },
);
return { success: true, message: data.message };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const updateDeliveryStatus = async (
deliveryId: number,
status: string,
latitude: number,
longitude: number,
): Promise<{ success: boolean; message?: string; error?: string }> => {
try {
const { data } = await apiClient.put(
`${API}/deliveries/${deliveryId}/status`,
{ status, latitude, longitude },
);
return { success: true, message: data.message };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
// ============================================
// GPS
// ============================================
export const updateMyLocation = async (
latitude: number,
longitude: number,
): Promise<{ success: boolean; message?: string; error?: string }> => {
try {
const { data } = await apiClient.post(`${API}/location/update`, {
latitude,
longitude,
});
return { success: true, message: data.message };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const getMyLocation = async (): Promise<{
success: boolean;
latitude?: number;
longitude?: number;
error?: string;
}> => {
try {
const { data } = await apiClient.get(`${API}/location`);
return {
success: true,
latitude: data.latitude,
longitude: data.longitude,
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
// ============================================
// ALERTES
// ============================================
export const triggerPoliceAlert = async (): Promise<{
success: boolean;
alert_id?: number;
message?: string;
error?: string;
}> => {
try {
const { data } = await apiClient.post(`${API}/alert`);
return {
success: true,
alert_id: data.alert_id,
message: data.message,
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const endAlert = async (
alertId: number,
): Promise<{ success: boolean; message?: string; error?: string }> => {
try {
const { data } = await apiClient.delete(`${API}/alert/${alertId}`);
return { success: true, message: data.message };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const getMyAlerts = async (): Promise<{
success: boolean;
alerts?: Alert[];
count?: number;
error?: string;
}> => {
try {
const { data } = await apiClient.get(`${API}/alerts`);
return {
success: true,
alerts: data.alerts || [],
count: data.count || 0,
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
+43
View File
@@ -0,0 +1,43 @@
import axios from 'axios';
import { getToken, getAdminToken } from '../auth/tokenStorage';
// Change this to your server IP/domain
export const API_BASE_URL = 'http://localhost:8080';
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor: attach JWT token
apiClient.interceptors.request.use(async (config) => {
// Determine which token to use based on URL
const isAdminRoute = config.url?.includes('/api/v2/') ||
config.url?.includes('/api/v1/cabine/') ||
config.url?.includes('/api/v1/livreur/');
const token = isAdminRoute
? await getAdminToken()
: await getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor: handle common errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
console.log('Session expirée');
}
return Promise.reject(error);
}
);
export default apiClient;
+204
View File
@@ -0,0 +1,204 @@
import axios from "axios";
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
// ---- Types ----
export interface LatLng {
latitude: number;
longitude: number;
}
export interface RouteInfo {
distance: string; // e.g. "2.4 km"
duration: string; // e.g. "8 min"
distanceMeters: number;
durationSeconds: number;
coordinates: LatLng[]; // polyline points
}
export interface NavigationInstruction {
instruction: string;
distance: string;
maneuver: string;
streetName?: string;
isActive: boolean;
}
// ---- Maneuver translations (FR) ----
export const maneuverTranslations: Record<string, string> = {
TURN_LEFT: "Tournez à gauche",
TURN_RIGHT: "Tournez à droite",
TURN_SLIGHT_LEFT: "Tournez légèrement à gauche",
TURN_SLIGHT_RIGHT: "Tournez légèrement à droite",
TURN_SHARP_LEFT: "Tournez fortement à gauche",
TURN_SHARP_RIGHT: "Tournez fortement à droite",
KEEP_LEFT: "Restez à gauche",
KEEP_RIGHT: "Restez à droite",
STRAIGHT: "Continuez tout droit",
ENTER_ROUNDABOUT: "Entrez dans le rond-point",
EXIT_ROUNDABOUT: "Sortez du rond-point",
MOTORWAY_ENTER: "Entrez sur l'autoroute",
MOTORWAY_EXIT: "Sortez de l'autoroute",
ARRIVE: "Vous êtes arrivé",
ARRIVE_LEFT: "Destination à gauche",
ARRIVE_RIGHT: "Destination à droite",
DEPART: "Départ",
U_TURN: "Faites demi-tour",
FOLLOW: "Suivez",
WAYPOINT_REACHED: "Point de passage atteint",
};
// Ionicons name per maneuver
export const maneuverIcons: Record<string, string> = {
TURN_LEFT: "arrow-back",
TURN_RIGHT: "arrow-forward",
TURN_SLIGHT_LEFT: "arrow-up",
TURN_SLIGHT_RIGHT: "arrow-up",
TURN_SHARP_LEFT: "return-down-back",
TURN_SHARP_RIGHT: "return-down-forward",
KEEP_LEFT: "arrow-back",
KEEP_RIGHT: "arrow-forward",
STRAIGHT: "arrow-up",
ENTER_ROUNDABOUT: "sync",
EXIT_ROUNDABOUT: "arrow-forward",
MOTORWAY_ENTER: "speedometer",
MOTORWAY_EXIT: "exit-outline",
ARRIVE: "flag",
ARRIVE_LEFT: "flag",
ARRIVE_RIGHT: "flag",
DEPART: "car",
U_TURN: "return-up-back",
FOLLOW: "arrow-up",
WAYPOINT_REACHED: "location",
DEFAULT: "arrow-up",
};
// ---- Helpers ----
function formatDistance(meters: number): string {
if (meters < 1000) return `${Math.round(meters)} m`;
return `${(meters / 1000).toFixed(1)} km`;
}
// ---- Geocode address → coords ----
export async function geocodeAddress(
address: string,
): Promise<LatLng | null> {
try {
const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&countrySet=FR&limit=1`;
const { data } = await axios.get(url);
if (data.results && data.results.length > 0) {
const pos = data.results[0].position;
if (pos?.lat != null && pos?.lon != null) {
return { latitude: pos.lat, longitude: pos.lon };
}
}
} catch {
/* silent */
}
return null;
}
// ---- Calculate route ----
export async function calculateRoute(
origin: LatLng,
destination: LatLng,
): Promise<{ route: RouteInfo; instructions: NavigationInstruction[] } | null> {
try {
const start = `${origin.latitude},${origin.longitude}`;
const end = `${destination.latitude},${destination.longitude}`;
const url = `https://api.tomtom.com/routing/1/calculateRoute/${start}:${end}/json?key=${TOMTOM_API_KEY}&instructionsType=text&language=fr-FR&traffic=true&travelMode=car`;
const { data } = await axios.get(url);
if (!data.routes || data.routes.length === 0) return null;
const route = data.routes[0];
const summary = route.summary;
// Extract polyline coordinates
const coordinates: LatLng[] = [];
if (route.legs) {
for (const leg of route.legs) {
if (leg.points) {
for (const pt of leg.points) {
coordinates.push({
latitude: pt.latitude,
longitude: pt.longitude,
});
}
}
}
}
// Fallback: straight line
if (coordinates.length === 0) {
coordinates.push(origin, destination);
}
const distanceKm = (summary.lengthInMeters / 1000).toFixed(1);
const durationMin = Math.round(summary.travelTimeInSeconds / 60);
const routeInfo: RouteInfo = {
distance: `${distanceKm} km`,
duration: `${durationMin} min`,
distanceMeters: summary.lengthInMeters,
durationSeconds: summary.travelTimeInSeconds,
coordinates,
};
// Extract instructions
const instructions: NavigationInstruction[] = [];
if (route.guidance?.instructions) {
for (let i = 0; i < route.guidance.instructions.length; i++) {
const inst = route.guidance.instructions[i];
const maneuver = inst.maneuver || "STRAIGHT";
const text =
inst.message ||
maneuverTranslations[maneuver] ||
"Continuez";
instructions.push({
instruction: text,
distance: formatDistance(inst.routeOffsetInMeters || 0),
maneuver,
streetName: inst.street,
isActive: i === 0,
});
}
}
if (instructions.length === 0) {
instructions.push({
instruction: "Dirigez-vous vers votre destination",
distance: formatDistance(summary.lengthInMeters || 0),
maneuver: "DEPART",
isActive: true,
});
instructions.push({
instruction: "Vous êtes arrivé à destination",
distance: "0 m",
maneuver: "ARRIVE",
isActive: false,
});
}
return { route: routeInfo, instructions };
} catch {
/* silent */
}
return null;
}
// ---- Bearing calculation ----
export function calculateBearing(
from: LatLng,
to: LatLng,
): number {
const φ1 = (from.latitude * Math.PI) / 180;
const φ2 = (to.latitude * Math.PI) / 180;
const Δλ = ((to.longitude - from.longitude) * Math.PI) / 180;
const y = Math.sin(Δλ) * Math.cos(φ2);
const x =
Math.cos(φ1) * Math.sin(φ2) -
Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
}
+149
View File
@@ -0,0 +1,149 @@
// Shared types for admin panel
export interface AdminUser {
id: number;
username: string;
role: string;
}
export interface AuthResponse {
success: boolean;
message?: string;
access_token?: string;
token_type?: string;
expires_in?: number;
user?: AdminUser;
}
export interface ClientResponse {
id: number;
username: string;
nom: string;
prenom: string;
telephone: string;
adresse?: string;
role?: string;
created_at?: string;
updated_at?: string;
command: number;
point: number;
points_zipette: number;
amende: number;
cancellations_count: number;
last_penalty_reason?: string;
}
export interface CommandResponse {
id: number;
username: string;
status: string;
adresse: string;
total_prix: number;
livreur_assign?: string | null;
created_at: string;
updated_at: string;
}
export interface OrderItem {
id: number;
command_id: number;
product_id: number;
product_name: string;
quantity: number;
price: number;
status: string;
created_at: string;
}
export interface Alert {
id: number;
username: string;
status: string;
created_at: string;
updated_at: string;
}
export interface DeliveryPerson {
id: number;
username: string;
status: 'available' | 'busy' | 'offline';
location: {
latitude: number;
longitude: number;
last_update: string;
is_recent: boolean;
};
stats: {
total_deliveries: number;
completed_today: number;
queue_size: number;
current_command: number | null;
};
}
export interface DeliveryPersonsStats {
total: number;
available: number;
busy: number;
offline: number;
active_deliveries: number;
}
export interface Product {
id: number;
name: string;
description?: string;
category: string;
stock: number;
prices?: Array<{ quantity: number; price: number }>;
media?: Array<{ url: string; type: string; id?: number; created_at?: string }>;
created_at?: string;
updated_at?: string;
}
export interface DeliveryStatus {
status: 'available' | 'busy' | 'offline';
current_command?: number;
last_update?: number;
}
export interface QueueInfo {
queue_size: number;
commands: any[];
}
export interface DeliveryItem {
id: number;
status: string;
adresse: string;
total_prix: number;
created_at: string;
updated_at: string;
eta?: string;
}
export interface ClientInfo {
username: string;
nom?: string;
prenom?: string;
telephone?: string;
}
export interface DeliveryDetails {
delivery: DeliveryItem;
client_info: ClientInfo;
}
export interface PenaltyInfo {
client_username: string;
current_amende: number;
cancellations_count: number;
next_penalty: number;
}
export interface MapLinks {
google_maps: string;
waze: string;
apple_maps: string;
openstreetmap: string;
}