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 DeliveryScreen from "../screens/admin/DeliveryScreen";
|
||||||
import AlertsScreen from "../screens/admin/AlertsScreen";
|
import AlertsScreen from "../screens/admin/AlertsScreen";
|
||||||
import AddressScreen from "../screens/admin/AddressScreen";
|
import AddressScreen from "../screens/admin/AddressScreen";
|
||||||
|
import SettingsScreen from "../screens/admin/SettingsScreen";
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator<AdminTabParamList>();
|
const Tab = createBottomTabNavigator<AdminTabParamList>();
|
||||||
const Stack = createNativeStackNavigator<AdminStackParamList>();
|
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>
|
</Tab.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type AdminTabParamList = {
|
|||||||
Delivery: undefined;
|
Delivery: undefined;
|
||||||
Alerts: undefined;
|
Alerts: undefined;
|
||||||
Addresses: undefined;
|
Addresses: undefined;
|
||||||
|
Settings: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminStackParamList = {
|
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