From 6d4e0862ff965b7320578721a812a9c227024c06 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sun, 14 Jun 2026 18:09:44 +0200 Subject: [PATCH] chore: build --- frontend-admin/App.tsx | 19 +- frontend-admin/app.config.js | 11 + frontend-admin/app.json | 12 +- frontend-admin/eas.json | 22 +- frontend-admin/package-lock.json | 77 ++ frontend-admin/package.json | 1 + frontend-admin/src/api/api_admin.ts | 154 +++- frontend-admin/src/api/api_cabine.ts | 50 +- frontend-admin/src/api/api_delivery.ts | 37 +- frontend-admin/src/api/client.ts | 2 - frontend-admin/src/api/tomtom.ts | 5 - frontend-admin/src/api/types.ts | 3 +- .../src/navigation/AdminNavigator.tsx | 11 + frontend-admin/src/navigation/types.ts | 1 + .../src/screens/admin/OrderDetailScreen.tsx | 732 +++++++++-------- .../src/screens/admin/OrdersScreen.tsx | 96 ++- .../src/screens/admin/ProductsScreen.tsx | 118 ++- .../src/screens/admin/SettingsScreen.tsx | 743 ++++++++++++++++-- .../src/screens/admin/StatsScreen.tsx | 500 ++++++++++++ .../src/screens/auth/AdminLoginScreen.tsx | 2 +- .../src/screens/auth/CabineLoginScreen.tsx | 2 +- .../src/screens/auth/DeliveryLoginScreen.tsx | 2 +- .../src/screens/cabine/DashboardScreen.tsx | 4 +- .../src/screens/cabine/OrdersScreen.tsx | 18 +- .../src/screens/cabine/UsersScreen.tsx | 5 +- .../src/screens/delivery/DashboardScreen.tsx | 499 +++++++++--- .../src/screens/delivery/StatsScreen.tsx | 255 ++++-- mobile/app.config.js | 11 + mobile/app.json | 3 +- mobile/eas.json | 15 +- mobile/src/api/api.ts | 137 +++- mobile/src/api/api_types.ts | 2 + mobile/src/api/client.ts | 5 +- mobile/src/auth/tokenStorage.ts | 2 - mobile/src/components/ProductCard.tsx | 72 +- mobile/src/components/ui/Modal.tsx | 2 +- .../src/screens/auth/ChangePasswordScreen.tsx | 2 +- mobile/src/screens/auth/ClientLoginScreen.tsx | 173 ++-- mobile/src/screens/client/CheckoutScreen.tsx | 44 +- .../src/screens/client/OrderDetailsScreen.tsx | 12 +- .../src/screens/client/OrderHistoryScreen.tsx | 218 ++++- .../screens/client/OrderTrackingScreen.tsx | 16 +- .../src/screens/client/ParrainageScreen.tsx | 39 +- .../screens/client/ProductDetailScreen.tsx | 84 +- mobile/src/screens/client/ProfileScreen.tsx | 400 +++++++++- 45 files changed, 3745 insertions(+), 873 deletions(-) create mode 100644 frontend-admin/app.config.js create mode 100644 frontend-admin/src/screens/admin/StatsScreen.tsx create mode 100644 mobile/app.config.js diff --git a/frontend-admin/App.tsx b/frontend-admin/App.tsx index 23549c56..d9120788 100644 --- a/frontend-admin/App.tsx +++ b/frontend-admin/App.tsx @@ -1,6 +1,7 @@ -import React, { useMemo } from "react"; +import React, { useMemo, useEffect } from "react"; import { StatusBar } from "expo-status-bar"; import { ActivityIndicator, View, StyleSheet } from "react-native"; +import * as Updates from "expo-updates"; import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; @@ -91,6 +92,22 @@ function RootNavigator() { } export default function App() { + useEffect(() => { + if (__DEV__) return; + const checkForUpdate = async () => { + try { + const update = await Updates.checkForUpdateAsync(); + if (update.isAvailable) { + await Updates.fetchUpdateAsync(); + await Updates.reloadAsync(); + } + } catch { + // Silently ignore update errors + } + }; + checkForUpdate(); + }, []); + return ( diff --git a/frontend-admin/app.config.js b/frontend-admin/app.config.js new file mode 100644 index 00000000..54e99fb5 --- /dev/null +++ b/frontend-admin/app.config.js @@ -0,0 +1,11 @@ +module.exports = ({ config }) => { + const updateUrl = process.env.EXPO_PUBLIC_UPDATE_URL; + + return { + ...config, + updates: { + ...config.updates, + ...(updateUrl ? { url: updateUrl } : {}), + }, + }; +}; diff --git a/frontend-admin/app.json b/frontend-admin/app.json index 800bf641..28f89dd6 100644 --- a/frontend-admin/app.json +++ b/frontend-admin/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Admin Panel", "slug": "frontend-admin", - "version": "1.0.0", + "version": "1.0.1", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "dark", @@ -18,6 +18,7 @@ }, "android": { "package": "com.uberstup.adminpanel", + "abiFilters": ["arm64-v8a"], "adaptiveIcon": { "foregroundImage": "./assets/icon.png", "backgroundColor": "#000000" @@ -36,6 +37,7 @@ "plugins": [ "expo-font", "expo-location", + "expo-updates", [ "expo-build-properties", { @@ -50,6 +52,12 @@ "projectId": "fcb7a0bc-5b2f-453d-ba16-9f97d9f0e440" } }, - "owner": "xor290" + "owner": "xor290", + "runtimeVersion": { + "policy": "appVersion" + }, + "updates": { + "url": "https://u.expo.dev/fcb7a0bc-5b2f-453d-ba16-9f97d9f0e440" + } } } diff --git a/frontend-admin/eas.json b/frontend-admin/eas.json index f62e5b47..3dfa22d4 100644 --- a/frontend-admin/eas.json +++ b/frontend-admin/eas.json @@ -11,21 +11,21 @@ "buildType": "apk", "gradleCommand": ":app:assembleDebug" }, - "ios": { - "simulator": true - }, "env": { - "EXPO_PUBLIC_API_URL": "https://5.181.0.112.nip.io" - } + "EXPO_PUBLIC_API_URL": "http://localhost:8080" + }, + "channel": "development" }, - "preview": { + "pre-prod": { "distribution": "internal", "android": { "buildType": "apk" }, "env": { - "EXPO_PUBLIC_API_URL": "https://5.181.0.112.nip.io" - } + "EXPO_PUBLIC_API_URL": "https://uber-demo.club", + "EXPO_PUBLIC_UPDATE_URL": "https://ota.uber-stup.club" + }, + "channel": "pre-prod-admin" }, "production": { "distribution": "internal", @@ -34,8 +34,10 @@ "buildType": "apk" }, "env": { - "EXPO_PUBLIC_API_URL": "https://mln-uber.club" - } + "EXPO_PUBLIC_API_URL": "https://mln-uber.club", + "EXPO_PUBLIC_UPDATE_URL": "https://ota.uber-stup.club" + }, + "channel": "production-admin" } } } diff --git a/frontend-admin/package-lock.json b/frontend-admin/package-lock.json index 8c8a9900..12738e27 100644 --- a/frontend-admin/package-lock.json +++ b/frontend-admin/package-lock.json @@ -21,6 +21,7 @@ "expo-image-picker": "~17.0.11", "expo-location": "~19.0.8", "expo-status-bar": "~3.0.9", + "expo-updates": "~29.0.17", "jwt-decode": "^4.0.0", "react": "19.1.0", "react-dom": "19.1.0", @@ -4210,6 +4211,12 @@ "react-native": "*" } }, + "node_modules/expo-eas-client": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-1.0.8.tgz", + "integrity": "sha512-5or11NJhSeDoHHI6zyvQDW2cz/yFyE+1Cz8NTs5NK8JzC7J0JrkUgptWtxyfB6Xs/21YRNifd3qgbBN3hfKVgA==", + "license": "MIT" + }, "node_modules/expo-file-system": { "version": "19.0.22", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.22.tgz", @@ -4253,6 +4260,12 @@ "expo": "*" } }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, "node_modules/expo-keep-awake": { "version": "15.0.8", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", @@ -4270,6 +4283,19 @@ "expo": "*" } }, + "node_modules/expo-manifests": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.11.tgz", + "integrity": "sha512-6zItytTewN37Cjhp3glUg0ozrgW2GwB8x9wtfzUNoJIMmxO38nnGdTLMaotYhRqdf5PP2Dzdmej1HDHXVNUpRw==", + "license": "MIT", + "dependencies": { + "@expo/config": "~12.0.13", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.25", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz", @@ -4320,6 +4346,57 @@ "react-native": "*" } }, + "node_modules/expo-structured-headers": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-5.0.0.tgz", + "integrity": "sha512-RmrBtnSphk5REmZGV+lcdgdpxyzio5rJw8CXviHE6qH5pKQQ83fhMEcigvrkBdsn2Efw2EODp4Yxl1/fqMvOZw==", + "license": "MIT" + }, + "node_modules/expo-updates": { + "version": "29.0.17", + "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-29.0.17.tgz", + "integrity": "sha512-9h78cs6Q2rs/dEY7zAgyEm/m6J5rHy8RNpRyhilEAvrzrGLHChVZJT+bSR2RwNJg1DtwUNEjCgZrxDlM7LnNkg==", + "license": "MIT", + "dependencies": { + "@expo/code-signing-certificates": "^0.0.6", + "@expo/plist": "^0.4.8", + "@expo/spawn-async": "^1.7.2", + "arg": "4.1.0", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "expo-eas-client": "~1.0.8", + "expo-manifests": "~1.0.11", + "expo-structured-headers": "~5.0.0", + "expo-updates-interface": "~2.0.0", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "ignore": "^5.3.1", + "resolve-from": "^5.0.0" + }, + "bin": { + "expo-updates": "bin/cli.js" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-updates-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz", + "integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-updates/node_modules/arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "license": "MIT" + }, "node_modules/expo/node_modules/@expo/cli": { "version": "54.0.24", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.24.tgz", diff --git a/frontend-admin/package.json b/frontend-admin/package.json index f93c97b5..93911244 100644 --- a/frontend-admin/package.json +++ b/frontend-admin/package.json @@ -22,6 +22,7 @@ "expo-image-picker": "~17.0.11", "expo-location": "~19.0.8", "expo-status-bar": "~3.0.9", + "expo-updates": "~29.0.17", "jwt-decode": "^4.0.0", "react": "19.1.0", "react-dom": "19.1.0", diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index c94b8f4e..e28093f4 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -1,4 +1,5 @@ -import apiClient, { API_BASE_URL } from "./client"; +import apiClient from "./client"; +import { API_BASE_URL } from "./client"; import type { AuthResponse, ClientResponse, @@ -52,6 +53,72 @@ export const logoutAdmin = async (): Promise => { } }; +export interface StatsSummary { + total_orders: number; + total_revenue: number; + peak_weekday: string; + top_product: string; + avg_per_day: number; +} +export interface WeekdayStat { + weekday: string; + count: number; +} +export interface DayStat { + day: string; + label: string; + count: number; +} +export interface DayRevenueStat { + day: string; + label: string; + revenue: number; +} +export interface HourStat { + hour: number; + label: string; + count: number; + revenue: number; +} +export interface ProductStat { + product_id: number; + name: string; + quantity: number; + order_count: number; + revenue: number; + category: string; + category_color: string; +} + +export interface QuantityStat { + quantity: number; + order_count: number; + total_sold: number; + revenue: number; +} +export interface ProductQuantityBreakdown { + product_id: number; + name: string; + category_color: string; + total_orders: number; + quantities: QuantityStat[]; +} + +export interface AdminStats { + summary: StatsSummary; + by_weekday: WeekdayStat[]; + by_day_30: DayStat[]; + by_day_revenue: DayRevenueStat[]; + by_hour: HourStat[]; + top_products: ProductStat[]; + by_quantity: ProductQuantityBreakdown[]; +} + +export const getAdminStats = async (): Promise => { + const { data } = await apiClient.get(`${V2}/admin/protected/stats`); + return data; +}; + export const getAllClients = async (): Promise => { const { data } = await apiClient.get(`${V2}/admin/protected/all/clients`); return data.clients || []; @@ -84,9 +151,6 @@ export const updateUserByAdmin = async ( 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[] = []; @@ -171,8 +235,11 @@ export const updateCommandAddress = async ( export const validateCommand = async (commandId: number) => { const { data } = await apiClient.post( `${V2}/admin/protected/orders/${commandId}/force-validate`, + { command_id: commandId }, ); - return { success: true, message: data.message }; + const validated = (data.validated ?? []) as { command_id: number; points_awarded: number }[]; + const points = validated.find((v) => v.command_id === commandId)?.points_awarded ?? 0; + return { success: true, points_awarded: points, validated_count: data.validated_count ?? 0 }; }; export const proposeAddressChangeAdmin = async ( @@ -197,31 +264,22 @@ export const notifyClientToDescend = async (commandId: number) => { }; }; -export const confirmReceptionAdmin = async (commandId: number) => { - const { data } = await apiClient.post( - `${V2}/admin/protected/orders/${commandId}/confirm-reception`, - ); - return { - success: true, - message: data.message, - points_earned: data.points_earned, - client_username: data.client_username, - }; -}; -// ============================================ -// 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, - }; + try { + const { data } = await apiClient.get( + `${V2}/admin/protected/delivery-persons`, + ); + return { + success: true, + livreurs: data.livreurs || [], + count: data.count || 0, + }; + } catch { + return { success: false, livreurs: [], count: 0 }; + } }; export const assignDeliveryPerson = async ( @@ -370,10 +428,6 @@ export const getDeliverymanLocationForCommand = async (commandId: number) => { } }; -// ============================================ -// PRODUITS -// ============================================ - export const getAllProductsAdmin = async (): Promise<{ success: boolean; data: Product[]; @@ -445,7 +499,7 @@ export const createUserByAdmin = async (data: { role: string; }) => { const { data: res } = await apiClient.post( - `${V2}/admin/auth/register`, + `${V2}/admin/protected/users`, data, ); return { @@ -710,6 +764,20 @@ export const deleteProductMediaAdmin = async ( return { success: true, message: data.message }; }; +export const activateProductPrice = async (priceId: number) => { + const { data } = await apiClient.post( + `${V2}/admin/protected/active/product/price/${priceId}`, + ); + return { success: true, message: data.message }; +}; + +export const deactivateProductPrice = async (priceId: number) => { + const { data } = await apiClient.post( + `${V2}/admin/protected/desactive/product/price/${priceId}`, + ); + return { success: true, message: data.message }; +}; + // ============================================ // ADDRESSES // ============================================ @@ -849,6 +917,26 @@ export interface PointsTier { points: number; } +export interface RewardCategoryConfig { + category: string; + all_products: boolean; + product_ids: number[]; +} + +export interface RewardItem { + product_id: number; + quantity: number; + price: number; +} + +export interface PointsReward { + threshold: number; + type: "free_product" | "half_price_product" | "custom"; + description: string; + category_configs: RewardCategoryConfig[]; + reward_items: RewardItem[]; +} + export interface PointsPool { key: string; name: string; @@ -947,6 +1035,7 @@ export interface AppSettings { penalty_tiers: PenaltyTier[]; points_enabled: boolean; points_pools: PointsPool[]; + points_reward?: PointsReward | null; referral_enabled: boolean; delivery_schedule: DeliverySchedule; postal_zones: PostalZone[]; @@ -958,7 +1047,10 @@ export interface AppSettings { telegram_bot_token: string; telegram_bot_username: string; telegram_notifications_enabled: boolean; + telegram_2fa_enabled: boolean; delivery_mode: DeliveryModeConfig; + shop_name: string; + contact_telegram: string; } export const getSettings = async (): Promise<{ diff --git a/frontend-admin/src/api/api_cabine.ts b/frontend-admin/src/api/api_cabine.ts index 6527b2c6..1be5da95 100644 --- a/frontend-admin/src/api/api_cabine.ts +++ b/frontend-admin/src/api/api_cabine.ts @@ -1,4 +1,7 @@ -import apiClient, { API_BASE_URL } from "./client"; +import apiClient from "./client"; +import { API_BASE_URL } from "./client"; + +//@ts import type { OrderItem, DeliveryPerson, @@ -39,10 +42,6 @@ export const confirmReceptionCabine = async (commandId: number) => { }; }; -// ============================================ -// PENALITES -// ============================================ - export const applyClientPenalty = async ( clientUsername: string, reason: string, @@ -70,7 +69,6 @@ export const resetClientPenalties = async (clientUsername: string) => { return { success: true, message: data.message }; }; -// pool: index dans pool_names/pool_keys, -1 = reset tous les points export const resetClientPoints = async ( clientUsername: string, pool: number = -1, @@ -96,10 +94,6 @@ export const getPenaltiesStats = async () => { return { success: true, data: data.data }; }; -// ============================================ -// COMMANDES -// ============================================ - export const getCancelledOrders = async () => { const { data } = await apiClient.get(`${API}/commands/cancelled`); return { @@ -200,7 +194,7 @@ export const getAllDeliveryPersonsWithDetails = async (): Promise<{ users.map(async (u: any): Promise => { try { const { data: details } = await apiClient.get( - `${V2}/admin/protected/delivery-persons/${u.username}`, + `${API}/delivery-persons/${u.username}`, ); const d = details.deliveryman || details; const parsedStatus = parseStatus(d.status); @@ -373,7 +367,7 @@ export const markCabineNotificationsRead = async (): Promise => { export const getPublicSettings = async (): Promise => { try { const { data } = await apiClient.get( - `https://5.181.0.112.nip.io/api/v1/app-settings`, + `${API_BASE_URL}/api/v1/app-settings`, ); return { penalties_enabled: data.penalties_enabled ?? true, @@ -433,11 +427,41 @@ export const getAllAddresses = async (): Promise< })); }; +// ============================================ +// COMMANDES — CABINE +// ============================================ + +export const getCabineCommands = async (): Promise<{ + success: boolean; + commands: any[]; + count: number; +}> => { + try { + const { data } = await apiClient.get(`${API}/commands`); + return { success: true, commands: data.commands || [], count: data.count || 0 }; + } catch { + return { success: false, commands: [], count: 0 }; + } +}; + +// ============================================ +// CLIENTS — CABINE +// ============================================ + +export const getCabineAllClients = async (): Promise => { + try { + const { data } = await apiClient.get(`${API}/all/clients`); + return data.clients || []; + } catch { + return []; + } +}; + // ============================================ // TELEGRAM — CABINE // ============================================ -const CABINE_API = "https://5.181.0.112.nip.io/api/v1/cabine"; +const CABINE_API = `${API_BASE_URL}/api/v1/cabine`; export const getCabineTelegramStatus = async (): Promise<{ linked: boolean; diff --git a/frontend-admin/src/api/api_delivery.ts b/frontend-admin/src/api/api_delivery.ts index f53e7166..427cd367 100644 --- a/frontend-admin/src/api/api_delivery.ts +++ b/frontend-admin/src/api/api_delivery.ts @@ -1,4 +1,5 @@ -import apiClient, { API_BASE_URL } from "./client"; +import apiClient from "./client"; +import { API_BASE_URL } from "./client"; import type { DeliveryStatus, QueueInfo, @@ -9,10 +10,6 @@ import type { const API = `${API_BASE_URL}/api/v1/livreur`; -// ============================================ -// STATUT -// ============================================ - export const getMyStatus = async (): Promise<{ success: boolean; status?: DeliveryStatus; @@ -45,10 +42,6 @@ export const updateMyStatus = async ( } }; -// ============================================ -// QUEUE -// ============================================ - export const getMyQueue = async (): Promise<{ success: boolean; queue_info?: QueueInfo; @@ -65,10 +58,6 @@ export const getMyQueue = async (): Promise<{ } }; -// ============================================ -// LIVRAISONS -// ============================================ - export const getMyDeliveries = async (): Promise<{ success: boolean; deliveries?: DeliveryItem[]; @@ -383,6 +372,28 @@ export const ISSUE_LABELS: Record = { other: "Autre", }; +export type StatPoint = { label: string; count: number; revenue: number }; + +export const getMyStats = async (): Promise<{ + success: boolean; + by_day?: StatPoint[]; + by_week?: StatPoint[]; + by_month?: StatPoint[]; + error?: string; +}> => { + try { + const { data } = await apiClient.get(`${API}/stats`); + return { + success: true, + by_day: data.by_day || [], + by_week: data.by_week || [], + by_month: data.by_month || [], + }; + } catch (error: any) { + return { success: false, error: error.response?.data?.error || "Erreur réseau" }; + } +}; + export const reportDeliveryIssue = async ( deliveryId: number, issueType: IssueType, diff --git a/frontend-admin/src/api/client.ts b/frontend-admin/src/api/client.ts index efa514c2..d52169b2 100644 --- a/frontend-admin/src/api/client.ts +++ b/frontend-admin/src/api/client.ts @@ -12,7 +12,6 @@ const apiClient = axios.create({ }, }); -// Request interceptor: attach JWT token apiClient.interceptors.request.use(async (config) => { const isAdminRoute = config.url?.includes("/api/v2/") || @@ -27,7 +26,6 @@ apiClient.interceptors.request.use(async (config) => { return config; }); -// Response interceptor: handle common errors apiClient.interceptors.response.use( (response) => response, (error) => { diff --git a/frontend-admin/src/api/tomtom.ts b/frontend-admin/src/api/tomtom.ts index 5959c79e..2a3bb8fc 100644 --- a/frontend-admin/src/api/tomtom.ts +++ b/frontend-admin/src/api/tomtom.ts @@ -2,7 +2,6 @@ import axios from "axios"; const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB"; -// ---- Types ---- export interface LatLng { latitude: number; longitude: number; @@ -24,7 +23,6 @@ export interface NavigationInstruction { isActive: boolean; } -// ---- Maneuver translations (FR) ---- export const maneuverTranslations: Record = { TURN_LEFT: "Tournez à gauche", TURN_RIGHT: "Tournez à droite", @@ -48,7 +46,6 @@ export const maneuverTranslations: Record = { WAYPOINT_REACHED: "Point de passage atteint", }; -// Ionicons name per maneuver export const maneuverIcons: Record = { TURN_LEFT: "arrow-back", TURN_RIGHT: "arrow-forward", @@ -79,7 +76,6 @@ function formatDistance(meters: number): string { return `${(meters / 1000).toFixed(1)} km`; } -// ---- Geocode address → coords ---- export async function geocodeAddress(address: string): Promise { try { const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&limit=1`; @@ -96,7 +92,6 @@ export async function geocodeAddress(address: string): Promise { return null; } -// ---- Calculate route ---- export async function calculateRoute( origin: LatLng, destination: LatLng, diff --git a/frontend-admin/src/api/types.ts b/frontend-admin/src/api/types.ts index bd82bb33..59d4539e 100644 --- a/frontend-admin/src/api/types.ts +++ b/frontend-admin/src/api/types.ts @@ -102,8 +102,9 @@ export interface Product { category: string; stock: number; unit: string; - prices?: Array<{ quantity: number; price: number }>; + prices?: Array<{ id?: number; quantity: number; price: number; active_price?: boolean }>; media?: Array<{ url: string; type: string; id?: number; created_at?: string }>; + coming_soon?: boolean; created_at?: string; updated_at?: string; } diff --git a/frontend-admin/src/navigation/AdminNavigator.tsx b/frontend-admin/src/navigation/AdminNavigator.tsx index 5d14938f..52d81b3d 100644 --- a/frontend-admin/src/navigation/AdminNavigator.tsx +++ b/frontend-admin/src/navigation/AdminNavigator.tsx @@ -24,6 +24,7 @@ import { fontSize, spacing } from "../theme"; import type { AdminTabParamList, AdminStackParamList } from "./types"; import DashboardScreen from "../screens/admin/DashboardScreen"; +import StatsScreen from "../screens/admin/StatsScreen"; import OrdersScreen from "../screens/admin/OrdersScreen"; import OrderDetailScreen from "../screens/admin/OrderDetailScreen"; import UsersScreen from "../screens/admin/UsersScreen"; @@ -175,6 +176,16 @@ function AdminTabs() { ), }} /> + ( + + ), + }} + /> (null); - const fullscreenMapRef = useRef(null); + const mapRef = useRef(null); + const fullscreenMapRef = useRef(null); const [mapFullscreen, setMapFullscreen] = useState(false); const [livreurCoords, setLivreurCoords] = useState(null); const [destCoords, setDestCoords] = useState(null); + const [livreurMarkers, setLivreurMarkers] = useState([]); const [routeInfo, setRouteInfo] = useState(null); const [mapLoading, setMapLoading] = useState(false); const { alert, showError, showSuccess, hideAlert } = useAlert(); @@ -72,7 +71,7 @@ export default function OrderDetailScreen() { getCommandItems(orderId), ]); setCommand(cmdRes.command); - setItems(itemsRes.items); + setItems(itemsRes.items ?? []); // If livreur assigned, fetch their location and calc route const cmd = cmdRes.command; @@ -99,13 +98,21 @@ export default function OrderDetailScreen() { return () => clearInterval(interval); }, [orderId]); + // Rejoue la route sur la carte fullscreen quand elle s'ouvre + useEffect(() => { + if (mapFullscreen && livreurCoords && destCoords) { + setTimeout(() => { + fullscreenMapRef.current?.calcRoute(livreurCoords, destCoords); + }, 600); + } + }, [mapFullscreen]); + const loadLivreurRoute = async ( livreurUsername: string, deliveryAddress: string, ) => { setMapLoading(true); try { - // Get livreur location const details = await getDeliveryPersonDetails(livreurUsername); const loc = details?.location; if (!loc?.latitude || !loc?.longitude) { @@ -113,24 +120,26 @@ export default function OrderDetailScreen() { return; } - const origin: LatLng = { + const origin: LatLng = { latitude: loc.latitude, longitude: loc.longitude }; + setLivreurCoords(origin); + setLivreurMarkers([{ + id: livreurUsername, latitude: loc.latitude, longitude: loc.longitude, - }; - setLivreurCoords(origin); + color: "#22c55e", + label: livreurUsername, + description: "Livreur", + }]); - // Geocode destination + // Dessine la route sur la carte et récupère les infos (distance/durée) const dest = await geocodeAddress(deliveryAddress); - if (!dest) { - setMapLoading(false); - return; - } - setDestCoords(dest); - - // Calculate route - const result = await calculateRoute(origin, dest); - if (result) { - setRouteInfo(result.route); + if (dest) { + setDestCoords(dest); + const result = await calculateRoute(origin, dest); + if (result) { + setRouteInfo(result.route); + mapRef.current?.calcRoute(origin, dest); + } } } catch { /* silent */ @@ -138,27 +147,6 @@ export default function OrderDetailScreen() { setMapLoading(false); }; - const fitMapToRoute = (ref: React.RefObject) => { - if (ref.current && livreurCoords && destCoords) { - ref.current.fitToCoordinates( - [ - { - latitude: livreurCoords.latitude, - longitude: livreurCoords.longitude, - }, - { - latitude: destCoords.latitude, - longitude: destCoords.longitude, - }, - ], - { - edgePadding: { top: 80, right: 60, bottom: 80, left: 60 }, - animated: true, - }, - ); - } - }; - const handleValidate = async () => { try { await validateCommand(orderId); @@ -205,7 +193,7 @@ export default function OrderDetailScreen() { try { await deleteCommandItem(orderId, itemId); const itemsRes = await getCommandItems(orderId); - setItems(itemsRes.items); + setItems(itemsRes.items ?? []); const cmdRes = await getCommandByID(orderId); setCommand(cmdRes.command); } catch (e: any) { @@ -217,20 +205,6 @@ export default function OrderDetailScreen() { ); }; - const handleConfirmReception = async () => { - try { - const res = await confirmReceptionAdmin(orderId); - showSuccess( - "Réception confirmée", - `${res.points_earned} point(s) attribués au client ${res.client_username}`, - ); - const updated = await getCommandByID(orderId); - setCommand(updated.command); - } catch (e: any) { - showError("Erreur", e.message); - } - }; - const handleDeleteCommand = () => { Alert.alert( "Supprimer la commande", @@ -316,39 +290,6 @@ export default function OrderDetailScreen() { }, map: { width: "100%", height: MAP_HEIGHT }, - driverMarkerOuter: { - width: 34, - height: 34, - borderRadius: 17, - backgroundColor: colors.success + "40", - justifyContent: "center", - alignItems: "center", - }, - driverMarkerInner: { - width: 26, - height: 26, - borderRadius: 13, - backgroundColor: colors.success, - justifyContent: "center", - alignItems: "center", - }, - destMarkerOuter: { - width: 30, - height: 30, - borderRadius: 15, - backgroundColor: colors.danger + "40", - justifyContent: "center", - alignItems: "center", - }, - destMarkerInner: { - width: 22, - height: 22, - borderRadius: 11, - backgroundColor: colors.danger, - justifyContent: "center", - alignItems: "center", - }, - routeOverlay: { position: "absolute", top: spacing.s, @@ -409,6 +350,81 @@ export default function OrderDetailScreen() { marginTop: spacing.xs, }, + // Grouped items + subItemRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: spacing.xs, + paddingLeft: spacing.m, + borderLeftWidth: 2, + borderLeftColor: colors.border, + marginLeft: spacing.xs, + marginBottom: 2, + }, + subItemText: { + color: colors.textSecondary, + fontSize: fontSize.sm, + flex: 1, + }, + groupTotalRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginTop: spacing.s, + paddingTop: spacing.s, + borderTopWidth: 1, + borderTopColor: colors.border, + }, + groupTotalQty: { + color: colors.textWhite, + fontSize: fontSize.sm, + fontWeight: "700", + }, + groupTotalPrice: { + color: colors.accent, + fontSize: fontSize.md, + fontWeight: "700", + }, + categoryBadge: { + fontSize: fontSize.xs, + color: colors.accent, + fontWeight: "600", + marginRight: spacing.s, + textTransform: "uppercase", + }, + // Category summary + categorySummaryCard: { + marginTop: spacing.s, + }, + categoryRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: spacing.s, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + categoryName: { + color: colors.textSecondary, + fontSize: fontSize.sm, + fontWeight: "600", + flex: 1, + }, + categoryQty: { + color: colors.textWhite, + fontSize: fontSize.sm, + fontWeight: "700", + marginRight: spacing.l, + }, + categoryTotal: { + color: colors.accent, + fontSize: fontSize.sm, + fontWeight: "700", + minWidth: 70, + textAlign: "right", + }, + // Items modal itemsModalOverlay: { flex: 1, @@ -476,6 +492,30 @@ export default function OrderDetailScreen() { [colors, MAP_HEIGHT], ); + // Groupement des items par product_id + const productGroups = (items ?? []).reduce>( + (acc, item) => { + const key = String(item.product_id || item.produit); + if (!acc[key]) acc[key] = []; + acc[key].push(item); + return acc; + }, + {}, + ); + const groupedList = Object.values(productGroups); + + // Récapitulatif par catégorie + const categoryTotals = (items ?? []).reduce< + Record + >((acc, item) => { + const cat = item.category || "Autre"; + if (!acc[cat]) acc[cat] = { qty: 0, total: 0 }; + acc[cat].qty += item.quantite ?? 0; + acc[cat].total += item.prix ?? 0; + return acc; + }, {}); + const categoryEntries = Object.entries(categoryTotals); + if (loading) return ; if (!command) return ( @@ -496,74 +536,21 @@ export default function OrderDetailScreen() { visible={mapFullscreen} animationType="fade" onRequestClose={() => setMapFullscreen(false)} - statusBarTranslucent > -