This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
Linking,
|
||||
Alert,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||
import { shadows } from "../../theme/shadows";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import {
|
||||
getAllCommands,
|
||||
getAllClients,
|
||||
getAvailableDeliveryPersons,
|
||||
getCommandCountByStatus,
|
||||
getAdminTelegramStatus,
|
||||
generateAdminLinkToken,
|
||||
unlinkAdminTelegram,
|
||||
} from "../../api/api_admin";
|
||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||
|
||||
interface StatCard {
|
||||
label: string;
|
||||
value: number;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export default function DashboardScreen() {
|
||||
const { colors } = useTheme();
|
||||
const { username } = useAuth();
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
const [stats, setStats] = useState<StatCard[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [tgLinked, setTgLinked] = useState(false);
|
||||
const [tgEnabled, setTgEnabled] = useState(false);
|
||||
const [tgLoading, setTgLoading] = useState(false);
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
const [allCmd, pending, enRoute, completed, clients, livreurs] =
|
||||
await Promise.all([
|
||||
getAllCommands().then((r) => r.count),
|
||||
getCommandCountByStatus("pending"),
|
||||
getCommandCountByStatus("en_route"),
|
||||
getCommandCountByStatus("approved"),
|
||||
getAllClients()
|
||||
.then((c) => c.length)
|
||||
.catch(() => 0),
|
||||
getAvailableDeliveryPersons()
|
||||
.then((r) => r.count)
|
||||
.catch(() => 0),
|
||||
]);
|
||||
|
||||
setStats([
|
||||
{
|
||||
label: "Total commandes",
|
||||
value: allCmd,
|
||||
icon: "receipt-outline",
|
||||
color: colors.accent,
|
||||
},
|
||||
{
|
||||
label: "En attente",
|
||||
value: pending,
|
||||
icon: "time-outline",
|
||||
color: colors.warning,
|
||||
},
|
||||
{
|
||||
label: "En route",
|
||||
value: enRoute,
|
||||
icon: "navigate-outline",
|
||||
color: colors.info,
|
||||
},
|
||||
{
|
||||
label: "Terminées",
|
||||
value: completed,
|
||||
icon: "checkmark-circle-outline",
|
||||
color: colors.success,
|
||||
},
|
||||
{
|
||||
label: "Clients",
|
||||
value: clients,
|
||||
icon: "people-outline",
|
||||
color: colors.accentLight,
|
||||
},
|
||||
{
|
||||
label: "Livreurs",
|
||||
value: livreurs,
|
||||
icon: "bicycle-outline",
|
||||
color: colors.categoryGros,
|
||||
},
|
||||
]);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setLoading(false);
|
||||
}, [colors]);
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
getAdminTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); });
|
||||
}, [loadStats]);
|
||||
|
||||
const handleLinkTelegram = async () => {
|
||||
setTgLoading(true);
|
||||
const res = await generateAdminLinkToken();
|
||||
setTgLoading(false);
|
||||
if (res.error || !res.link_url) {
|
||||
Alert.alert("Erreur", res.error || "Service Telegram non disponible");
|
||||
return;
|
||||
}
|
||||
Linking.openURL(res.link_url);
|
||||
};
|
||||
|
||||
const handleUnlinkTelegram = () => {
|
||||
Alert.alert("Délier Telegram", "Vous ne recevrez plus de notifications Telegram.", [
|
||||
{ text: "Annuler", style: "cancel" },
|
||||
{ text: "Délier", style: "destructive", onPress: async () => { await unlinkAdminTelegram(); setTgLinked(false); } },
|
||||
]);
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await loadStats();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bgPrimary,
|
||||
padding: screenWidth < 380 ? spacing.m : spacing.l,
|
||||
},
|
||||
welcome: {
|
||||
fontSize: screenWidth < 380 ? fontSize.lg : fontSize.xl,
|
||||
fontWeight: "bold",
|
||||
color: colors.textWhite,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: screenWidth < 380 ? fontSize.sm : fontSize.md,
|
||||
color: colors.textSecondary,
|
||||
marginBottom: spacing.l,
|
||||
},
|
||||
grid: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: screenWidth < 380 ? spacing.s : spacing.m,
|
||||
},
|
||||
card: {
|
||||
width: screenWidth < 360 ? "100%" : "47%",
|
||||
backgroundColor: colors.bgCard,
|
||||
borderRadius: borderRadius.md,
|
||||
padding: screenWidth < 380 ? spacing.m : spacing.l,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderLight,
|
||||
alignItems: "center",
|
||||
},
|
||||
iconCircle: {
|
||||
width: screenWidth < 380 ? 40 : 48,
|
||||
height: screenWidth < 380 ? 40 : 48,
|
||||
borderRadius: screenWidth < 380 ? 20 : 24,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
cardValue: {
|
||||
fontSize: screenWidth < 380 ? fontSize.xl : fontSize.xxl,
|
||||
fontWeight: "bold",
|
||||
color: colors.textWhite,
|
||||
},
|
||||
cardLabel: {
|
||||
fontSize: screenWidth < 380 ? fontSize.xs : fontSize.sm,
|
||||
color: colors.textSecondary,
|
||||
marginTop: spacing.xs,
|
||||
textAlign: "center",
|
||||
},
|
||||
}),
|
||||
[colors, screenWidth],
|
||||
);
|
||||
|
||||
if (loading) return <LoadingSpinner message="Chargement..." />;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={colors.accent}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text style={styles.welcome}>Bonjour, {username}</Text>
|
||||
<Text style={styles.subtitle}>Vue d'ensemble</Text>
|
||||
|
||||
<View style={styles.grid}>
|
||||
{stats.map((s, i) => (
|
||||
<View key={i} style={[styles.card, shadows.md]}>
|
||||
<View
|
||||
style={[
|
||||
styles.iconCircle,
|
||||
{ backgroundColor: s.color + "20" },
|
||||
]}
|
||||
>
|
||||
<Ionicons name={s.icon} size={24} color={s.color} />
|
||||
</View>
|
||||
<Text style={styles.cardValue}>{s.value}</Text>
|
||||
<Text style={styles.cardLabel}>{s.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{tgEnabled && (
|
||||
<View style={{ margin: spacing.l, backgroundColor: colors.bgCard, borderRadius: borderRadius.md, padding: spacing.l, borderWidth: 1, borderColor: colors.borderLight }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s, marginBottom: spacing.m }}>
|
||||
<Ionicons name="paper-plane-outline" size={18} color="#2AABEE" />
|
||||
<Text style={{ color: colors.textPrimary, fontSize: fontSize.md, fontWeight: "600" }}>Notifications Telegram</Text>
|
||||
</View>
|
||||
{tgLinked ? (
|
||||
<View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s, marginBottom: spacing.s }}>
|
||||
<Ionicons name="checkmark-circle" size={14} color={colors.success} />
|
||||
<Text style={{ color: colors.success, fontSize: fontSize.sm }}>Compte Telegram lié</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={handleUnlinkTelegram} style={{ flexDirection: "row", alignItems: "center", gap: spacing.s, padding: spacing.s, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: colors.danger + "66" }}>
|
||||
<Ionicons name="unlink-outline" size={14} color={colors.danger} />
|
||||
<Text style={{ color: colors.danger, fontSize: fontSize.sm }}>Délier Telegram</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity onPress={handleLinkTelegram} disabled={tgLoading} style={{ flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.s, padding: spacing.m, borderRadius: borderRadius.sm, backgroundColor: "#2AABEE" }}>
|
||||
<Ionicons name="paper-plane-outline" size={14} color="#fff" />
|
||||
<Text style={{ color: "#fff", fontSize: fontSize.sm, fontWeight: "600" }}>{tgLoading ? "Génération..." : "Lier Telegram"}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user