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