chore: fix

This commit is contained in:
2026-03-07 20:51:48 +01:00
parent 5c212e5f9c
commit 916d2a30ad
8 changed files with 316 additions and 194 deletions
+3 -1
View File
@@ -663,7 +663,9 @@ export const deleteCategoryAdmin = async (
export interface AppSettings {
penalties_enabled: boolean;
points_categories: string[];
points_enabled: boolean;
points_categories_weed: string[];
points_categories_zipette: string[];
points_separated: boolean;
}
+21
View File
@@ -339,3 +339,24 @@ export const getAllAlerts = async (): Promise<{
return { success: false, alerts: [], count: 0 };
}
};
// ============================================
// PARAMÈTRES PUBLICS
// ============================================
export interface PublicSettings {
penalties_enabled: boolean;
points_enabled: boolean;
}
export const getPublicSettings = async (): Promise<PublicSettings> => {
try {
const { data } = await apiClient.get(`http://5.181.0.112/api/v1/app-settings`);
return {
penalties_enabled: data.penalties_enabled ?? true,
points_enabled: data.points_enabled ?? true,
};
} catch {
return { penalties_enabled: true, points_enabled: true };
}
};
@@ -2,6 +2,8 @@ import React from "react";
import { TouchableOpacity, View } from "react-native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { Ionicons } from "@expo/vector-icons";
import { useAuth } from "../auth/AuthContext";
import { useTheme } from "../context/ThemeContext";
@@ -26,6 +28,7 @@ const Stack = createNativeStackNavigator<AdminStackParamList>();
function AdminTabs() {
const { logout } = useAuth();
const { colors, isDark, toggleTheme } = useTheme();
const navigation = useNavigation<NativeStackNavigationProp<AdminStackParamList>>();
const handleLogout = async () => {
await logoutAdmin();
@@ -55,6 +58,16 @@ function AdminTabs() {
color={colors.textSecondary}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => navigation.navigate("Settings")}
style={{ marginRight: spacing.m }}
>
<Ionicons
name="settings-outline"
size={22}
color={colors.textSecondary}
/>
</TouchableOpacity>
<TouchableOpacity onPress={handleLogout}>
<Ionicons
name="log-out-outline"
@@ -186,20 +199,6 @@ function AdminTabs() {
),
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
title: "Paramètres",
tabBarIcon: ({ color, size }) => (
<Ionicons
name="settings-outline"
size={size}
color={color}
/>
),
}}
/>
</Tab.Navigator>
);
}
@@ -224,6 +223,11 @@ export default function AdminNavigator() {
component={OrderDetailScreen}
options={{ title: "Détail commande" }}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{ title: "Paramètres" }}
/>
</Stack.Navigator>
);
}
+1 -1
View File
@@ -14,12 +14,12 @@ export type AdminTabParamList = {
Delivery: undefined;
Alerts: undefined;
Addresses: undefined;
Settings: undefined;
};
export type AdminStackParamList = {
AdminTabs: undefined;
OrderDetail: { orderId: number };
Settings: undefined;
};
export type CabineTabParamList = {
@@ -16,6 +16,8 @@ import type { AppSettings, Category } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
type PoolAssignment = "weed" | "zipette" | "none";
export default function SettingsScreen() {
const { colors } = useTheme();
const { alert, showError, showSuccess, hideAlert } = useAlert();
@@ -24,7 +26,9 @@ export default function SettingsScreen() {
const [saving, setSaving] = useState(false);
const [settings, setSettings] = useState<AppSettings>({
penalties_enabled: true,
points_categories: [],
points_enabled: true,
points_categories_weed: [],
points_categories_zipette: [],
points_separated: true,
});
const [categories, setCategories] = useState<Category[]>([]);
@@ -38,7 +42,8 @@ export default function SettingsScreen() {
if (settingsRes.success && settingsRes.settings) {
setSettings({
...settingsRes.settings,
points_categories: settingsRes.settings.points_categories ?? [],
points_categories_weed: settingsRes.settings.points_categories_weed ?? [],
points_categories_zipette: settingsRes.settings.points_categories_zipette ?? [],
});
}
if (categoriesRes) {
@@ -51,13 +56,19 @@ export default function SettingsScreen() {
loadData();
}, [loadData]);
const toggleCategory = (name: string) => {
const getPoolFor = (name: string): PoolAssignment => {
if ((settings.points_categories_weed ?? []).includes(name)) return "weed";
if ((settings.points_categories_zipette ?? []).includes(name)) return "zipette";
return "none";
};
const setPool = (name: string, pool: PoolAssignment) => {
setSettings((prev) => {
const cats = prev.points_categories ?? [];
if (cats.includes(name)) {
return { ...prev, points_categories: cats.filter((c) => c !== name) };
}
return { ...prev, points_categories: [...cats, name] };
const weed = (prev.points_categories_weed ?? []).filter((c) => c !== name);
const zipette = (prev.points_categories_zipette ?? []).filter((c) => c !== name);
if (pool === "weed") weed.push(name);
else if (pool === "zipette") zipette.push(name);
return { ...prev, points_categories_weed: weed, points_categories_zipette: zipette };
});
};
@@ -73,14 +84,8 @@ export default function SettingsScreen() {
};
const s = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.bgPrimary,
},
content: {
padding: spacing.l,
paddingBottom: spacing.xl * 2,
},
container: { flex: 1, backgroundColor: colors.bgPrimary },
content: { padding: spacing.l, paddingBottom: spacing.xl * 2 },
section: {
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.lg,
@@ -106,53 +111,31 @@ export default function SettingsScreen() {
borderTopWidth: 1,
borderTopColor: colors.border,
},
rowFirst: {
borderTopWidth: 0,
},
rowLeft: {
flex: 1,
marginRight: spacing.m,
},
rowLabel: {
fontSize: fontSize.md,
color: colors.textPrimary,
fontWeight: "600",
},
rowDesc: {
fontSize: fontSize.sm,
color: colors.textMuted,
marginTop: 2,
},
categoryRow: {
rowFirst: { borderTopWidth: 0 },
rowLeft: { flex: 1, marginRight: spacing.m },
rowLabel: { fontSize: fontSize.md, color: colors.textPrimary, fontWeight: "600" },
rowDesc: { fontSize: fontSize.sm, color: colors.textMuted, marginTop: 2 },
catRow: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: spacing.l,
paddingVertical: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
gap: spacing.m,
gap: spacing.s,
},
categoryRowFirst: {
borderTopWidth: 0,
catRowFirst: { borderTopWidth: 0 },
colorDot: { width: 10, height: 10, borderRadius: 5 },
catName: { flex: 1, fontSize: fontSize.md, color: colors.textPrimary },
chips: { flexDirection: "row", gap: spacing.xs },
chip: {
paddingHorizontal: spacing.s,
paddingVertical: 4,
borderRadius: borderRadius.sm,
borderWidth: 1,
},
checkBox: {
width: 22,
height: 22,
borderRadius: 6,
borderWidth: 2,
borderColor: colors.accent,
alignItems: "center",
justifyContent: "center",
},
checkBoxChecked: {
backgroundColor: colors.accent,
},
categoryName: {
fontSize: fontSize.md,
color: colors.textPrimary,
flex: 1,
},
allCatsHint: {
chipText: { fontSize: fontSize.sm, fontWeight: "600" },
hint: {
fontSize: fontSize.sm,
color: colors.textMuted,
fontStyle: "italic",
@@ -169,11 +152,7 @@ export default function SettingsScreen() {
gap: spacing.s,
marginTop: spacing.s,
},
saveButtonText: {
color: "#fff",
fontSize: fontSize.md,
fontWeight: "700",
},
saveButtonText: { color: "#fff", fontSize: fontSize.md, fontWeight: "700" },
});
if (loading) {
@@ -184,13 +163,13 @@ export default function SettingsScreen() {
);
}
const selectedCats = settings.points_categories ?? [];
const allSelected = selectedCats.length === 0;
const WEED_COLOR = "#10b981";
const ZIP_COLOR = "#9333ea";
return (
<View style={s.container}>
<ScrollView contentContainerStyle={s.content}>
{/* Section amendes */}
{/* Amendes */}
<View style={s.section}>
<Text style={s.sectionTitle}>Amendes</Text>
<View style={[s.row, s.rowFirst]}>
@@ -211,16 +190,32 @@ export default function SettingsScreen() {
</View>
</View>
{/* Section points */}
{/* Système de points */}
<View style={s.section}>
<Text style={s.sectionTitle}>Système de points</Text>
<View style={[s.row, s.rowFirst]}>
<View style={s.rowLeft}>
<Text style={s.rowLabel}>Points séparés par catégorie</Text>
<Text style={s.rowLabel}>Points activés</Text>
<Text style={s.rowDesc}>
Activé : weed point, zipette point_zipette{"\n"}
Désactivé : tous les points dans un seul compteur
Les clients voient leurs scores de points et point_zipette.{"\n"}
La cabine peut réinitialiser les points.
</Text>
</View>
<Switch
value={settings.points_enabled}
onValueChange={(v) =>
setSettings((prev) => ({ ...prev, points_enabled: v }))
}
trackColor={{ false: colors.border, true: colors.accent }}
thumbColor="#fff"
/>
</View>
<View style={s.row}>
<View style={s.rowLeft}>
<Text style={s.rowLabel}>Points séparés par pool</Text>
<Text style={s.rowDesc}>
Activé : Weed point / Zipette point_zipette{"\n"}
Désactivé : tout dans un seul compteur (point)
</Text>
</View>
<Switch
@@ -234,50 +229,96 @@ export default function SettingsScreen() {
</View>
</View>
{/* Section catégories éligibles aux points */}
{/* Attribution des catégories */}
<View style={s.section}>
<Text style={s.sectionTitle}>Catégories éligibles aux points</Text>
{allSelected && (
<Text style={s.allCatsHint}>
Aucune sélection = comportement par défaut (toutes les catégories sauf gros&semi)
</Text>
)}
<Text style={s.sectionTitle}>Attribution des catégories aux points</Text>
<Text style={s.hint}>
Pour chaque catégorie, choisis si elle génère des points Weed, Zipette, ou aucun.
</Text>
{/* Légende */}
<View style={{ flexDirection: "row", gap: spacing.m, paddingHorizontal: spacing.l, paddingBottom: spacing.m }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
<View style={[s.colorDot, { backgroundColor: WEED_COLOR }]} />
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Weed</Text>
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
<View style={[s.colorDot, { backgroundColor: ZIP_COLOR }]} />
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Zipette</Text>
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
<View style={[s.colorDot, { backgroundColor: colors.border }]} />
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Aucun</Text>
</View>
</View>
{categories.map((cat, index) => {
const checked = selectedCats.includes(cat.name);
const pool = getPoolFor(cat.name);
return (
<TouchableOpacity
<View
key={cat.id}
style={[
s.categoryRow,
index === 0 && s.categoryRowFirst,
]}
onPress={() => toggleCategory(cat.name)}
style={[s.catRow, index === 0 && s.catRowFirst]}
>
<View
style={[
s.checkBox,
checked && s.checkBoxChecked,
{ borderColor: cat.color || colors.accent },
s.colorDot,
{ backgroundColor: cat.color || colors.accent },
]}
>
{checked && (
<Ionicons name="checkmark" size={14} color="#fff" />
/>
<Text style={s.catName}>{cat.name}</Text>
<View style={s.chips}>
{(["weed", "zipette", "none"] as PoolAssignment[]).map(
(p) => {
const active = pool === p;
const chipColor =
p === "weed"
? WEED_COLOR
: p === "zipette"
? ZIP_COLOR
: colors.textMuted;
const label =
p === "weed"
? "W"
: p === "zipette"
? "Z"
: "—";
return (
<TouchableOpacity
key={p}
style={[
s.chip,
{
borderColor: chipColor,
backgroundColor: active
? chipColor
: "transparent",
},
]}
onPress={() => setPool(cat.name, p)}
>
<Text
style={[
s.chipText,
{
color: active
? "#fff"
: chipColor,
},
]}
>
{label}
</Text>
</TouchableOpacity>
);
},
)}
</View>
<View
style={{
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: cat.color || colors.accent,
}}
/>
<Text style={s.categoryName}>{cat.name}</Text>
</TouchableOpacity>
</View>
);
})}
{categories.length === 0 && (
<Text style={[s.allCatsHint, { paddingTop: 0 }]}>
<Text style={[s.hint, { paddingTop: 0 }]}>
Aucune catégorie disponible
</Text>
)}
@@ -3,7 +3,8 @@ import { View, Text, StyleSheet, FlatList, RefreshControl } from "react-native";
import { spacing, fontSize } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getAllClients } from "../../api/api_admin";
import { applyClientPenalty, resetClientPenalties, resetClientPoints } from "../../api/api_cabine";
import { applyClientPenalty, resetClientPenalties, resetClientPoints, getPublicSettings } from "../../api/api_cabine";
import type { PublicSettings } from "../../api/api_cabine";
import type { ClientResponse } from "../../api/types";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card";
@@ -16,6 +17,10 @@ export default function UsersScreen() {
const [clients, setClients] = useState<ClientResponse[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [appSettings, setAppSettings] = useState<PublicSettings>({
penalties_enabled: true,
points_enabled: true,
});
const [penaltyModal, setPenaltyModal] = useState<{
visible: boolean;
username: string;
@@ -25,7 +30,12 @@ export default function UsersScreen() {
const loadData = useCallback(async () => {
try {
setClients(await getAllClients());
const [clientsData, settings] = await Promise.all([
getAllClients(),
getPublicSettings(),
]);
setClients(clientsData);
setAppSettings(settings);
} catch {
/* ignore */
}
@@ -35,6 +45,7 @@ export default function UsersScreen() {
useEffect(() => {
loadData();
}, [loadData]);
const onRefresh = async () => {
setRefreshing(true);
await loadData();
@@ -115,6 +126,7 @@ export default function UsersScreen() {
flexDirection: "row",
gap: spacing.s,
marginTop: spacing.m,
flexWrap: "wrap",
},
empty: {
color: colors.textMuted,
@@ -132,24 +144,30 @@ export default function UsersScreen() {
{item.prenom} {item.nom} - {item.telephone}
</Text>
<View style={styles.statsRow}>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.success }]}>
{item.point}
</Text>
<Text style={styles.statLabel}>Points</Text>
</View>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.info }]}>
{item.points_zipette}
</Text>
<Text style={styles.statLabel}>Zipette</Text>
</View>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.warning }]}>
{item.amende}
</Text>
<Text style={styles.statLabel}>Amendes</Text>
</View>
{appSettings.points_enabled && (
<>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.success }]}>
{item.point}
</Text>
<Text style={styles.statLabel}>Points</Text>
</View>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.info }]}>
{item.points_zipette}
</Text>
<Text style={styles.statLabel}>Zipette</Text>
</View>
</>
)}
{appSettings.penalties_enabled && (
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.warning }]}>
{item.amende}
</Text>
<Text style={styles.statLabel}>Amendes</Text>
</View>
)}
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.danger }]}>
{item.cancellations_count}
@@ -158,24 +176,30 @@ export default function UsersScreen() {
</View>
</View>
<View style={styles.actions}>
<Button
title="Reset pénalités"
onPress={() => handleReset(item.username)}
size="sm"
variant="outline"
/>
<Button
title="Reset points"
onPress={() => handleResetPoints(item.username)}
size="sm"
variant="outline"
/>
<Button
title="Reset zipette"
onPress={() => handleResetZipette(item.username)}
size="sm"
variant="outline"
/>
{appSettings.penalties_enabled && (
<Button
title="Reset pénalités"
onPress={() => handleReset(item.username)}
size="sm"
variant="outline"
/>
)}
{appSettings.points_enabled && (
<>
<Button
title="Reset points"
onPress={() => handleResetPoints(item.username)}
size="sm"
variant="outline"
/>
<Button
title="Reset zipette"
onPress={() => handleResetZipette(item.username)}
size="sm"
variant="outline"
/>
</>
)}
</View>
</Card>
);
+21
View File
@@ -704,6 +704,27 @@ export const markNotificationsRead = async (): Promise<{
}
};
// ============================================
// PUBLIC SETTINGS
// ============================================
export interface PublicSettings {
penalties_enabled: boolean;
points_enabled: boolean;
}
export const getPublicSettings = async (): Promise<PublicSettings> => {
try {
const { data } = await apiClient.get(`${V1}/app-settings`);
return {
penalties_enabled: data.penalties_enabled ?? true,
points_enabled: data.points_enabled ?? true,
};
} catch {
return { penalties_enabled: true, points_enabled: true };
}
};
export const calculateOrderTotal = (order: any): number => {
if (typeof order.total === "number" && order.total > 0) return order.total;
if (typeof order.total_prix === "number" && order.total_prix > 0)
@@ -13,9 +13,11 @@ import { Ionicons } from "@expo/vector-icons";
import {
getMyCompletedOrders,
getMyPenalties,
getPublicSettings,
formatOrderDate,
formatPrice,
} from "../../api/api";
import type { PublicSettings } from "../../api/api";
import type {
CompletedOrder,
ClientStats,
@@ -42,14 +44,16 @@ export default function OrderHistoryScreen() {
const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [stats, setStats] = useState<ClientStats | null>(null);
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, points_enabled: true });
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchData = useCallback(async () => {
try {
const [histRes, penRes] = await Promise.all([
const [histRes, penRes, settings] = await Promise.all([
getMyCompletedOrders(),
getMyPenalties(),
getPublicSettings(),
]);
if (histRes.success) {
setOrders(histRes.commands || []);
@@ -58,6 +62,7 @@ export default function OrderHistoryScreen() {
if (penRes.success) {
setPenalties(penRes.data || null);
}
setAppSettings(settings);
} catch {
/* ignore */
} finally {
@@ -239,48 +244,52 @@ export default function OrderHistoryScreen() {
</Text>
<Text style={styles.statLabel}>Commandes</Text>
</View>
<View style={[styles.statCard, shadows.sm]}>
<Ionicons
name="leaf-outline"
size={24}
color={colors.categoryWeedHash}
/>
<Text style={styles.statValue}>
{stats?.points || 0}
</Text>
<Text style={styles.statLabel}>
Pts Weed/Hash
</Text>
</View>
<View style={[styles.statCard, shadows.sm]}>
<Ionicons
name="flash-outline"
size={24}
color={colors.info}
/>
<Text style={styles.statValue}>
{stats?.points_zipette || 0}
</Text>
<Text style={styles.statLabel}>
Pts Zipette
</Text>
</View>
<View style={[styles.statCard, shadows.sm]}>
<Ionicons
name="trophy-outline"
size={24}
color={colors.warning}
/>
<Text style={styles.statValue}>
{totalPoints}
</Text>
<Text style={styles.statLabel}>
Total Points
</Text>
</View>
{appSettings.points_enabled && (
<>
<View style={[styles.statCard, shadows.sm]}>
<Ionicons
name="leaf-outline"
size={24}
color={colors.categoryWeedHash}
/>
<Text style={styles.statValue}>
{stats?.points || 0}
</Text>
<Text style={styles.statLabel}>
Pts Weed/Hash
</Text>
</View>
<View style={[styles.statCard, shadows.sm]}>
<Ionicons
name="flash-outline"
size={24}
color={colors.info}
/>
<Text style={styles.statValue}>
{stats?.points_zipette || 0}
</Text>
<Text style={styles.statLabel}>
Pts Zipette
</Text>
</View>
<View style={[styles.statCard, shadows.sm]}>
<Ionicons
name="trophy-outline"
size={24}
color={colors.warning}
/>
<Text style={styles.statValue}>
{totalPoints}
</Text>
<Text style={styles.statLabel}>
Total Points
</Text>
</View>
</>
)}
</View>
{penaltyCount > 0 && (
{appSettings.penalties_enabled && penaltyCount > 0 && (
<View
style={[
styles.penaltyBanner,