chore: add settings

This commit is contained in:
2026-03-07 19:31:54 +01:00
parent 91ad46c0f8
commit 4be03a067c
4 changed files with 360 additions and 0 deletions
+30
View File
@@ -656,3 +656,33 @@ export const deleteCategoryAdmin = async (
};
}
};
// ============================================
// PARAMÈTRES GLOBAUX
// ============================================
export interface AppSettings {
penalties_enabled: boolean;
points_categories: string[];
points_separated: boolean;
}
export const getSettings = async (): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
try {
const { data } = await apiClient.get(`${V2}/admin/protected/settings`);
return { success: true, settings: data.settings };
} catch (error: any) {
return { success: false, error: error.response?.data?.error || "Erreur" };
}
};
export const updateSettings = async (
settings: AppSettings,
): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
try {
const { data } = await apiClient.put(`${V2}/admin/protected/settings`, settings);
return { success: true, settings: data.settings };
} catch (error: any) {
return { success: false, error: error.response?.data?.error || "Erreur" };
}
};
@@ -18,6 +18,7 @@ import CategoriesScreen from "../screens/admin/CategoriesScreen";
import DeliveryScreen from "../screens/admin/DeliveryScreen";
import AlertsScreen from "../screens/admin/AlertsScreen";
import AddressScreen from "../screens/admin/AddressScreen";
import SettingsScreen from "../screens/admin/SettingsScreen";
const Tab = createBottomTabNavigator<AdminTabParamList>();
const Stack = createNativeStackNavigator<AdminStackParamList>();
@@ -185,6 +186,20 @@ 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>
);
}
+1
View File
@@ -14,6 +14,7 @@ export type AdminTabParamList = {
Delivery: undefined;
Alerts: undefined;
Addresses: undefined;
Settings: undefined;
};
export type AdminStackParamList = {
@@ -0,0 +1,314 @@
import React, { useState, useEffect, useCallback } from "react";
import {
View,
Text,
StyleSheet,
ScrollView,
Switch,
TouchableOpacity,
ActivityIndicator,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getSettings, updateSettings, getCategories } from "../../api/api_admin";
import type { AppSettings, Category } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
export default function SettingsScreen() {
const { colors } = useTheme();
const { alert, showError, showSuccess, hideAlert } = useAlert();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [settings, setSettings] = useState<AppSettings>({
penalties_enabled: true,
points_categories: [],
points_separated: true,
});
const [categories, setCategories] = useState<Category[]>([]);
const loadData = useCallback(async () => {
setLoading(true);
const [settingsRes, categoriesRes] = await Promise.all([
getSettings(),
getCategories(),
]);
if (settingsRes.success && settingsRes.settings) {
setSettings({
...settingsRes.settings,
points_categories: settingsRes.settings.points_categories ?? [],
});
}
if (categoriesRes) {
setCategories(categoriesRes.filter((c) => !c.is_coming_soon));
}
setLoading(false);
}, []);
useEffect(() => {
loadData();
}, [loadData]);
const toggleCategory = (name: string) => {
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 handleSave = async () => {
setSaving(true);
const res = await updateSettings(settings);
setSaving(false);
if (res.success) {
showSuccess("Succès", "Paramètres sauvegardés");
} else {
showError("Erreur", res.error || "Erreur lors de la sauvegarde");
}
};
const s = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.bg,
},
content: {
padding: spacing.l,
paddingBottom: spacing.xl * 2,
},
section: {
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.l,
marginBottom: spacing.l,
overflow: "hidden",
},
sectionTitle: {
fontSize: fontSize.s,
fontWeight: "700",
color: colors.textMuted,
textTransform: "uppercase",
letterSpacing: 0.8,
paddingHorizontal: spacing.l,
paddingTop: spacing.l,
paddingBottom: spacing.s,
},
row: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: spacing.l,
paddingVertical: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
},
rowFirst: {
borderTopWidth: 0,
},
rowLeft: {
flex: 1,
marginRight: spacing.m,
},
rowLabel: {
fontSize: fontSize.m,
color: colors.text,
fontWeight: "600",
},
rowDesc: {
fontSize: fontSize.s,
color: colors.textMuted,
marginTop: 2,
},
categoryRow: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: spacing.l,
paddingVertical: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
gap: spacing.m,
},
categoryRowFirst: {
borderTopWidth: 0,
},
checkBox: {
width: 22,
height: 22,
borderRadius: 6,
borderWidth: 2,
borderColor: colors.accent,
alignItems: "center",
justifyContent: "center",
},
checkBoxChecked: {
backgroundColor: colors.accent,
},
categoryName: {
fontSize: fontSize.m,
color: colors.text,
flex: 1,
},
allCatsHint: {
fontSize: fontSize.s,
color: colors.textMuted,
fontStyle: "italic",
paddingHorizontal: spacing.l,
paddingBottom: spacing.m,
},
saveButton: {
backgroundColor: colors.accent,
borderRadius: borderRadius.l,
paddingVertical: spacing.m,
alignItems: "center",
flexDirection: "row",
justifyContent: "center",
gap: spacing.s,
marginTop: spacing.s,
},
saveButtonText: {
color: "#fff",
fontSize: fontSize.m,
fontWeight: "700",
},
});
if (loading) {
return (
<View style={[s.container, { alignItems: "center", justifyContent: "center" }]}>
<ActivityIndicator size="large" color={colors.accent} />
</View>
);
}
const selectedCats = settings.points_categories ?? [];
const allSelected = selectedCats.length === 0;
return (
<View style={s.container}>
<ScrollView contentContainerStyle={s.content}>
{/* Section amendes */}
<View style={s.section}>
<Text style={s.sectionTitle}>Amendes</Text>
<View style={[s.row, s.rowFirst]}>
<View style={s.rowLeft}>
<Text style={s.rowLabel}>Amendes activées</Text>
<Text style={s.rowDesc}>
Bloquer les commandes si le client a une amende non payée
</Text>
</View>
<Switch
value={settings.penalties_enabled}
onValueChange={(v) =>
setSettings((prev) => ({ ...prev, penalties_enabled: v }))
}
trackColor={{ false: colors.border, true: colors.accent }}
thumbColor="#fff"
/>
</View>
</View>
{/* Section 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.rowDesc}>
Activé : weed point, zipette point_zipette{"\n"}
Désactivé : tous les points dans un seul compteur
</Text>
</View>
<Switch
value={settings.points_separated}
onValueChange={(v) =>
setSettings((prev) => ({ ...prev, points_separated: v }))
}
trackColor={{ false: colors.border, true: colors.accent }}
thumbColor="#fff"
/>
</View>
</View>
{/* Section catégories éligibles aux points */}
<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>
)}
{categories.map((cat, index) => {
const checked = selectedCats.includes(cat.name);
return (
<TouchableOpacity
key={cat.id}
style={[
s.categoryRow,
index === 0 && s.categoryRowFirst,
]}
onPress={() => toggleCategory(cat.name)}
>
<View
style={[
s.checkBox,
checked && s.checkBoxChecked,
{ borderColor: cat.color || colors.accent },
]}
>
{checked && (
<Ionicons name="checkmark" size={14} color="#fff" />
)}
</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 && (
<Text style={[s.allCatsHint, { paddingTop: 0 }]}>
Aucune catégorie disponible
</Text>
)}
</View>
<TouchableOpacity
style={s.saveButton}
onPress={handleSave}
disabled={saving}
>
{saving ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<>
<Ionicons name="save-outline" size={18} color="#fff" />
<Text style={s.saveButtonText}>Sauvegarder</Text>
</>
)}
</TouchableOpacity>
</ScrollView>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onConfirm={alert.onConfirm ?? hideAlert}
onCancel={alert.onCancel ? hideAlert : undefined}
confirmText={alert.confirmText}
cancelText={alert.cancelText}
/>
</View>
);
}