chore: add settings
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user