This commit is contained in:
Generated
+2
-32
@@ -2757,7 +2757,7 @@
|
|||||||
"version": "19.1.17",
|
"version": "19.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
||||||
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -3771,7 +3771,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
@@ -3966,29 +3966,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/encoding": {
|
|
||||||
"version": "0.1.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
|
||||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"iconv-lite": "^0.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/encoding/node_modules/iconv-lite": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
@@ -7697,13 +7674,6 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export interface ProductQuantityBreakdown {
|
|||||||
quantities: QuantityStat[];
|
quantities: QuantityStat[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StatSection = "commandes" | "revenus" | "produits";
|
||||||
|
|
||||||
export interface AdminStats {
|
export interface AdminStats {
|
||||||
summary: StatsSummary;
|
summary: StatsSummary;
|
||||||
by_weekday: WeekdayStat[];
|
by_weekday: WeekdayStat[];
|
||||||
@@ -112,6 +114,9 @@ export interface AdminStats {
|
|||||||
by_hour: HourStat[];
|
by_hour: HourStat[];
|
||||||
top_products: ProductStat[];
|
top_products: ProductStat[];
|
||||||
by_quantity: ProductQuantityBreakdown[];
|
by_quantity: ProductQuantityBreakdown[];
|
||||||
|
reset_at_commandes?: string;
|
||||||
|
reset_at_revenus?: string;
|
||||||
|
reset_at_produits?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAdminStats = async (): Promise<AdminStats> => {
|
export const getAdminStats = async (): Promise<AdminStats> => {
|
||||||
@@ -119,6 +124,11 @@ export const getAdminStats = async (): Promise<AdminStats> => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const resetAdminStats = async (section: StatSection): Promise<{ success: boolean; reset_at: string }> => {
|
||||||
|
const { data } = await apiClient.post(`${V2}/admin/protected/stats/reset/${section}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAllClients = async (): Promise<ClientResponse[]> => {
|
export const getAllClients = async (): Promise<ClientResponse[]> => {
|
||||||
const { data } = await apiClient.get(`${V2}/admin/protected/all/clients`);
|
const { data } = await apiClient.get(`${V2}/admin/protected/all/clients`);
|
||||||
return data.clients || [];
|
return data.clients || [];
|
||||||
|
|||||||
@@ -394,6 +394,31 @@ export const getMyStats = async (): Promise<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface LivreurRating {
|
||||||
|
id: number;
|
||||||
|
order_id: number;
|
||||||
|
livreur_username: string;
|
||||||
|
client_username: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMyRatings = async (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
ratings: LivreurRating[];
|
||||||
|
average: number;
|
||||||
|
count: number;
|
||||||
|
error?: string;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get(`${API}/ratings`);
|
||||||
|
return { success: true, ratings: data.ratings || [], average: data.average || 0, count: data.count || 0 };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, ratings: [], average: 0, count: 0, error: e?.response?.data?.error || "Erreur" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const reportDeliveryIssue = async (
|
export const reportDeliveryIssue = async (
|
||||||
deliveryId: number,
|
deliveryId: number,
|
||||||
issueType: IssueType,
|
issueType: IssueType,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type { DeliveryTabParamList } from "./types";
|
|||||||
import DashboardScreen from "../screens/delivery/DashboardScreen";
|
import DashboardScreen from "../screens/delivery/DashboardScreen";
|
||||||
import StatsScreen from "../screens/delivery/StatsScreen";
|
import StatsScreen from "../screens/delivery/StatsScreen";
|
||||||
import AlertsScreen from "../screens/delivery/AlertsScreen";
|
import AlertsScreen from "../screens/delivery/AlertsScreen";
|
||||||
|
import RatingsScreen from "../screens/delivery/RatingsScreen";
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator<DeliveryTabParamList>();
|
const Tab = createBottomTabNavigator<DeliveryTabParamList>();
|
||||||
|
|
||||||
@@ -201,6 +202,20 @@ export default function DeliveryNavigator() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Ratings"
|
||||||
|
component={RatingsScreen}
|
||||||
|
options={{
|
||||||
|
title: "Mes avis",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Ionicons
|
||||||
|
name="star-outline"
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|
||||||
{/* Modal notifications */}
|
{/* Modal notifications */}
|
||||||
|
|||||||
@@ -36,4 +36,5 @@ export type DeliveryTabParamList = {
|
|||||||
Dashboard: undefined;
|
Dashboard: undefined;
|
||||||
Stats: undefined;
|
Stats: undefined;
|
||||||
Alerts: undefined;
|
Alerts: undefined;
|
||||||
|
Ratings: undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
@@ -13,8 +14,8 @@ import { useTheme } from "../../context/ThemeContext";
|
|||||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||||
import { shadows } from "../../theme/shadows";
|
import { shadows } from "../../theme/shadows";
|
||||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
import { getAdminStats } from "../../api/api_admin";
|
import { getAdminStats, resetAdminStats } from "../../api/api_admin";
|
||||||
import type { AdminStats, WeekdayStat, DayStat, DayRevenueStat, HourStat, ProductStat, ProductQuantityBreakdown } from "../../api/api_admin";
|
import type { AdminStats, WeekdayStat, DayStat, DayRevenueStat, HourStat, ProductStat, ProductQuantityBreakdown, StatSection } from "../../api/api_admin";
|
||||||
|
|
||||||
// ── Palette graphiques ────────────────────────────────────────────────────────
|
// ── Palette graphiques ────────────────────────────────────────────────────────
|
||||||
const CHART_ACCENT = "#6366f1";
|
const CHART_ACCENT = "#6366f1";
|
||||||
@@ -28,7 +29,7 @@ const maxOf = (arr: number[]) => arr.length ? Math.max(...arr) : 1;
|
|||||||
const fmtNum = (n: number) =>
|
const fmtNum = (n: number) =>
|
||||||
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n));
|
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n));
|
||||||
const fmtEuro = (n: number) =>
|
const fmtEuro = (n: number) =>
|
||||||
n >= 1000 ? `${(n / 1000).toFixed(1)}k€` : `${Math.round(n)}€`;
|
n >= 1000 ? `${(n / 1000).toFixed(2)}k€` : `${n.toFixed(2)}€`;
|
||||||
|
|
||||||
// ── Barre horizontale ─────────────────────────────────────────────────────────
|
// ── Barre horizontale ─────────────────────────────────────────────────────────
|
||||||
function HBar({
|
function HBar({
|
||||||
@@ -163,6 +164,28 @@ const secStyles = StyleSheet.create({
|
|||||||
title: { fontSize: fontSize.md, fontWeight: "600" },
|
title: { fontSize: fontSize.md, fontWeight: "600" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Bouton reset de section ───────────────────────────────────────────────────
|
||||||
|
function SectionResetBtn({ onPress, loading, resetAt }: { onPress: () => void; loading: boolean; resetAt?: string }) {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
return (
|
||||||
|
<View style={{ alignItems: "flex-end", marginBottom: 6 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
disabled={loading}
|
||||||
|
style={{ flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6, borderWidth: 1, borderColor: CHART_RED + "66", backgroundColor: CHART_RED + "11" }}
|
||||||
|
>
|
||||||
|
<Ionicons name="refresh-outline" size={11} color={CHART_RED} />
|
||||||
|
<Text style={{ fontSize: 10, fontWeight: "600", color: CHART_RED }}>{loading ? "..." : "Réinitialiser"}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{resetAt ? (
|
||||||
|
<Text style={{ fontSize: 9, color: colors.textMuted, marginTop: 2 }}>
|
||||||
|
Depuis le {new Date(resetAt).toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" })}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Sélecteur de période top produits ────────────────────────────────────────
|
// ── Sélecteur de période top produits ────────────────────────────────────────
|
||||||
type ProdSort = "quantity" | "orders" | "revenue";
|
type ProdSort = "quantity" | "orders" | "revenue";
|
||||||
|
|
||||||
@@ -172,6 +195,7 @@ export default function StatsScreen() {
|
|||||||
const [stats, setStats] = useState<AdminStats | null>(null);
|
const [stats, setStats] = useState<AdminStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [resetting, setResetting] = useState<StatSection | null>(null);
|
||||||
const [prodSort, setProdSort] = useState<ProdSort>("quantity");
|
const [prodSort, setProdSort] = useState<ProdSort>("quantity");
|
||||||
|
|
||||||
const loadStats = useCallback(async () => {
|
const loadStats = useCallback(async () => {
|
||||||
@@ -190,6 +214,28 @@ export default function StatsScreen() {
|
|||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReset = (section: StatSection, label: string) => {
|
||||||
|
Alert.alert(
|
||||||
|
`Réinitialiser — ${label}`,
|
||||||
|
"Les données de cette section repartiront de zéro. Les commandes restent en base.\n\nConfirmer ?",
|
||||||
|
[
|
||||||
|
{ text: "Annuler", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Réinitialiser",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
setResetting(section);
|
||||||
|
try {
|
||||||
|
await resetAdminStats(section);
|
||||||
|
await loadStats();
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
setResetting(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Produits triés selon le sélecteur
|
// Produits triés selon le sélecteur
|
||||||
const sortedProducts = useMemo<ProductStat[]>(() => {
|
const sortedProducts = useMemo<ProductStat[]>(() => {
|
||||||
if (!stats?.top_products) return [];
|
if (!stats?.top_products) return [];
|
||||||
@@ -210,6 +256,10 @@ export default function StatsScreen() {
|
|||||||
sortBtn: { paddingHorizontal: spacing.m, paddingVertical: spacing.xs, borderRadius: borderRadius.sm, borderWidth: 1 },
|
sortBtn: { paddingHorizontal: spacing.m, paddingVertical: spacing.xs, borderRadius: borderRadius.sm, borderWidth: 1 },
|
||||||
sortBtnText:{ fontSize: fontSize.xs, fontWeight: "600" },
|
sortBtnText:{ fontSize: fontSize.xs, fontWeight: "600" },
|
||||||
emptyText: { color: colors.textMuted, fontSize: fontSize.sm, textAlign: "center", paddingVertical: spacing.l },
|
emptyText: { color: colors.textMuted, fontSize: fontSize.sm, textAlign: "center", paddingVertical: spacing.l },
|
||||||
|
headerRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: spacing.xs },
|
||||||
|
resetBtn: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: spacing.s, paddingVertical: 3, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: CHART_RED + "66", backgroundColor: CHART_RED + "11" },
|
||||||
|
resetBtnText:{ fontSize: 10, fontWeight: "600", color: CHART_RED },
|
||||||
|
resetDate: { fontSize: fontSize.xs, color: colors.textMuted, marginBottom: spacing.l },
|
||||||
}), [colors]);
|
}), [colors]);
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner message="Chargement des statistiques..." />;
|
if (loading) return <LoadingSpinner message="Chargement des statistiques..." />;
|
||||||
@@ -261,6 +311,11 @@ export default function StatsScreen() {
|
|||||||
|
|
||||||
{/* ── Évolution 30 jours (commandes) ── */}
|
{/* ── Évolution 30 jours (commandes) ── */}
|
||||||
<Section title="30 derniers jours — commandes" icon="bar-chart-outline">
|
<Section title="30 derniers jours — commandes" icon="bar-chart-outline">
|
||||||
|
<SectionResetBtn
|
||||||
|
onPress={() => handleReset("commandes", "Commandes")}
|
||||||
|
loading={resetting === "commandes"}
|
||||||
|
resetAt={stats?.reset_at_commandes}
|
||||||
|
/>
|
||||||
{stats?.by_day_30?.length ? (
|
{stats?.by_day_30?.length ? (
|
||||||
<SparkLine data={stats.by_day_30} color={CHART_ACCENT} />
|
<SparkLine data={stats.by_day_30} color={CHART_ACCENT} />
|
||||||
) : (
|
) : (
|
||||||
@@ -275,6 +330,11 @@ export default function StatsScreen() {
|
|||||||
const best = [...revData].sort((a, b) => b.revenue - a.revenue)[0];
|
const best = [...revData].sort((a, b) => b.revenue - a.revenue)[0];
|
||||||
return (
|
return (
|
||||||
<Section title="Revenus par jour (30j)" icon="trending-up-outline">
|
<Section title="Revenus par jour (30j)" icon="trending-up-outline">
|
||||||
|
<SectionResetBtn
|
||||||
|
onPress={() => handleReset("revenus", "Revenus")}
|
||||||
|
loading={resetting === "revenus"}
|
||||||
|
resetAt={stats?.reset_at_revenus}
|
||||||
|
/>
|
||||||
{revData.length ? (
|
{revData.length ? (
|
||||||
<>
|
<>
|
||||||
<SparkLineRevenue data={revData} color={CHART_GREEN} />
|
<SparkLineRevenue data={revData} color={CHART_GREEN} />
|
||||||
@@ -432,6 +492,11 @@ export default function StatsScreen() {
|
|||||||
|
|
||||||
{/* ── Top produits ── */}
|
{/* ── Top produits ── */}
|
||||||
<Section title="Top produits" icon="cube-outline">
|
<Section title="Top produits" icon="cube-outline">
|
||||||
|
<SectionResetBtn
|
||||||
|
onPress={() => handleReset("produits", "Produits")}
|
||||||
|
loading={resetting === "produits"}
|
||||||
|
resetAt={stats?.reset_at_produits}
|
||||||
|
/>
|
||||||
{/* Sélecteur tri */}
|
{/* Sélecteur tri */}
|
||||||
<View style={styles.sortRow}>
|
<View style={styles.sortRow}>
|
||||||
{(["quantity", "orders", "revenue"] as ProdSort[]).map((key) => {
|
{(["quantity", "orders", "revenue"] as ProdSort[]).map((key) => {
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import React, { useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
ScrollView,
|
||||||
|
RefreshControl,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
|
import { spacing, fontSize } from "../../theme";
|
||||||
|
import { getMyRatings } from "../../api/api_delivery";
|
||||||
|
import type { LivreurRating } from "../../api/api_delivery";
|
||||||
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||||
|
|
||||||
|
const STAR_COLOR = "#f59e0b";
|
||||||
|
const STAR_EMPTY = "#374151";
|
||||||
|
|
||||||
|
function Stars({ value }: { value: number }) {
|
||||||
|
return (
|
||||||
|
<View style={{ flexDirection: "row", gap: 2 }}>
|
||||||
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<Ionicons
|
||||||
|
key={i}
|
||||||
|
name={i <= value ? "star" : "star-outline"}
|
||||||
|
size={14}
|
||||||
|
color={i <= value ? STAR_COLOR : STAR_EMPTY}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso: string): string {
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleDateString("fr-FR", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RatingsScreen() {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const [ratings, setRatings] = useState<LivreurRating[]>([]);
|
||||||
|
const [average, setAverage] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const load = useCallback(async (silent = false) => {
|
||||||
|
if (!silent) setLoading(true);
|
||||||
|
const res = await getMyRatings();
|
||||||
|
if (res.success) {
|
||||||
|
setRatings(res.ratings);
|
||||||
|
setAverage(res.average);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFocusEffect(useCallback(() => { load(); }, [load]));
|
||||||
|
|
||||||
|
const onRefresh = () => { setRefreshing(true); load(true); };
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
||||||
|
content: { padding: spacing.l, paddingBottom: spacing.xxxl },
|
||||||
|
headerCard: {
|
||||||
|
backgroundColor: colors.bgCard,
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: spacing.l,
|
||||||
|
marginBottom: spacing.l,
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderLight,
|
||||||
|
},
|
||||||
|
avgNumber: { fontSize: 48, fontWeight: "800", color: STAR_COLOR, lineHeight: 56 },
|
||||||
|
avgLabel: { fontSize: fontSize.sm, color: colors.textMuted, marginTop: spacing.xs },
|
||||||
|
countLabel: { fontSize: fontSize.xs, color: colors.textMuted, marginTop: 4 },
|
||||||
|
starsRow: { flexDirection: "row", gap: 4, marginTop: spacing.s },
|
||||||
|
card: {
|
||||||
|
backgroundColor: colors.bgCard,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: spacing.l,
|
||||||
|
marginBottom: spacing.m,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderLight,
|
||||||
|
},
|
||||||
|
cardHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: spacing.s },
|
||||||
|
client: { fontSize: fontSize.sm, fontWeight: "600", color: colors.textPrimary },
|
||||||
|
date: { fontSize: fontSize.xs, color: colors.textMuted },
|
||||||
|
orderRef: { fontSize: fontSize.xs, color: colors.textMuted, marginBottom: spacing.s },
|
||||||
|
comment: { fontSize: fontSize.sm, color: colors.textSecondary, fontStyle: "italic", marginTop: spacing.s, lineHeight: 20 },
|
||||||
|
emptyWrap: { alignItems: "center", paddingVertical: spacing.xxxl },
|
||||||
|
emptyText: { color: colors.textMuted, fontSize: fontSize.md, marginTop: spacing.m, textAlign: "center" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner message="Chargement des avis..." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={styles.container}
|
||||||
|
contentContainerStyle={styles.content}
|
||||||
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={STAR_COLOR} />}
|
||||||
|
>
|
||||||
|
{/* Résumé */}
|
||||||
|
<View style={styles.headerCard}>
|
||||||
|
<Text style={styles.avgNumber}>
|
||||||
|
{average > 0 ? average.toFixed(1) : "—"}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.starsRow}>
|
||||||
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<Ionicons
|
||||||
|
key={i}
|
||||||
|
name={i <= Math.round(average) ? "star" : "star-outline"}
|
||||||
|
size={22}
|
||||||
|
color={i <= Math.round(average) ? STAR_COLOR : STAR_EMPTY}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.avgLabel}>Note moyenne</Text>
|
||||||
|
<Text style={styles.countLabel}>{ratings.length} avis client{ratings.length > 1 ? "s" : ""}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Liste */}
|
||||||
|
{ratings.length === 0 ? (
|
||||||
|
<View style={styles.emptyWrap}>
|
||||||
|
<Ionicons name="chatbubble-ellipses-outline" size={48} color={colors.textMuted} />
|
||||||
|
<Text style={styles.emptyText}>Aucun avis reçu pour l'instant</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
ratings.map((r) => (
|
||||||
|
<View key={r.id} style={styles.card}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<Text style={styles.client}>{r.client_username}</Text>
|
||||||
|
<Text style={styles.date}>{formatDate(r.created_at)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.orderRef}>Commande #{r.order_id}</Text>
|
||||||
|
<Stars value={r.rating} />
|
||||||
|
{r.comment ? (
|
||||||
|
<Text style={styles.comment}>"{r.comment}"</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user