chore: add multiple delivery mode

This commit is contained in:
2026-03-25 18:52:16 +01:00
parent 4aa356f9fe
commit b044e634a5
2 changed files with 146 additions and 3 deletions
+11
View File
@@ -747,6 +747,16 @@ export const DEFAULT_DELIVERY_SCHEDULE: DeliverySchedule = {
saturday: { ...DEFAULT_DAY }, sunday: { ...DEFAULT_DAY }, saturday: { ...DEFAULT_DAY }, sunday: { ...DEFAULT_DAY },
}; };
export interface CategoryRoute {
deliveryman_username: string;
categories: string[];
}
export interface DeliveryModeConfig {
mode: "single" | "category_based";
category_routes: CategoryRoute[];
}
export interface PenaltyTier { export interface PenaltyTier {
min_cancel: number; min_cancel: number;
amount: number; amount: number;
@@ -768,6 +778,7 @@ export interface AppSettings {
nowpayments_currencies: string[]; nowpayments_currencies: string[];
telegram_bot_token: string; telegram_bot_token: string;
telegram_bot_username: string; telegram_bot_username: string;
delivery_mode: DeliveryModeConfig;
} }
export const getSettings = async (): Promise<{ export const getSettings = async (): Promise<{
@@ -14,8 +14,8 @@ import { useNavigation } from "@react-navigation/native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme"; import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext"; import { useTheme } from "../../context/ThemeContext";
import { getSettings, updateSettings, getCategories, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin"; import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
import type { AppSettings, Category, PointsTier, PointsPool, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin"; import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, DaySchedule, DeliverySchedule, PostalZone } 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";
@@ -667,10 +667,12 @@ export default function SettingsScreen() {
nowpayments_currencies: [], nowpayments_currencies: [],
telegram_bot_token: "", telegram_bot_token: "",
telegram_bot_username: "", telegram_bot_username: "",
delivery_mode: { mode: "single" as const, category_routes: [] },
}); });
const [showApiKey, setShowApiKey] = useState(false); const [showApiKey, setShowApiKey] = useState(false);
const [showIpnSecret, setShowIpnSecret] = useState(false); const [showIpnSecret, setShowIpnSecret] = useState(false);
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [livreurs, setLivreurs] = useState<string[]>([]);
// Refs pour l'auto-sauvegarde au départ de la page // Refs pour l'auto-sauvegarde au départ de la page
const settingsRef = useRef(settings); const settingsRef = useRef(settings);
@@ -699,9 +701,10 @@ export default function SettingsScreen() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
isLoaded.current = false; isLoaded.current = false;
const [settingsRes, categoriesRes] = await Promise.all([ const [settingsRes, categoriesRes, livreursRes] = await Promise.all([
getSettings(), getSettings(),
getCategories(), getCategories(),
getAvailableDeliveryPersons(),
]); ]);
if (settingsRes.success && settingsRes.settings) { if (settingsRes.success && settingsRes.settings) {
setSettings({ setSettings({
@@ -709,11 +712,15 @@ export default function SettingsScreen() {
points_pools: settingsRes.settings.points_pools ?? [], points_pools: settingsRes.settings.points_pools ?? [],
delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE, delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE,
postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES, postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES,
delivery_mode: settingsRes.settings.delivery_mode ?? { mode: "single", category_routes: [] },
}); });
} }
if (categoriesRes) { if (categoriesRes) {
setCategories(categoriesRes.filter((c) => !c.is_coming_soon)); setCategories(categoriesRes.filter((c) => !c.is_coming_soon));
} }
if (livreursRes.success) {
setLivreurs(livreursRes.livreurs.map((l: any) => l.username));
}
setLoading(false); setLoading(false);
// Marquer comme chargé après un tick pour que le useEffect de settings ne // Marquer comme chargé après un tick pour que le useEffect de settings ne
// considère pas le setSettings initial comme une modification utilisateur // considère pas le setSettings initial comme une modification utilisateur
@@ -1425,6 +1432,131 @@ export default function SettingsScreen() {
</View> </View>
</View> </View>
{/* ============================================ */}
{/* 🚚 MODE DE LIVRAISON */}
{/* ============================================ */}
<View style={s.section}>
<Text style={s.sectionTitle}>Mode de livraison</Text>
<Text style={[s.hint, { paddingTop: spacing.s, paddingHorizontal: spacing.l }]}>
Choisissez comment les commandes sont assignées aux livreurs.
</Text>
{/* Sélection du mode */}
<View style={{ flexDirection: "row", gap: spacing.m, paddingHorizontal: spacing.l, paddingTop: spacing.m, paddingBottom: spacing.s }}>
{(["single", "category_based"] as const).map((mode) => {
const selected = settings.delivery_mode.mode === mode;
const label = mode === "single" ? "Livreur unique" : "Par catégorie";
const desc = mode === "single" ? "Toutes les commandes → même livreur" : "Chaque livreur gère ses catégories";
return (
<TouchableOpacity
key={mode}
onPress={() => setSettings((p) => ({
...p,
delivery_mode: { ...p.delivery_mode, mode },
}))}
style={{
flex: 1,
padding: spacing.m,
borderRadius: borderRadius.sm,
borderWidth: 2,
borderColor: selected ? "#10b981" : colors.border,
backgroundColor: selected ? "#10b98118" : colors.bgSecondary,
}}
>
<Text style={{ fontSize: fontSize.sm, fontWeight: "700", color: selected ? "#10b981" : colors.text }}>{label}</Text>
<Text style={{ fontSize: fontSize.xs, color: colors.textSecondary, marginTop: 2 }}>{desc}</Text>
</TouchableOpacity>
);
})}
</View>
{/* Configuration des routes par catégorie */}
{settings.delivery_mode.mode === "category_based" && (
<View style={{ paddingHorizontal: spacing.l, paddingBottom: spacing.l }}>
<Text style={[s.rowLabel, { marginBottom: spacing.s, marginTop: spacing.m }]}>
Assignation livreur catégories
</Text>
{settings.delivery_mode.category_routes.map((route, idx) => (
<View key={idx} style={{ backgroundColor: colors.bgSecondary, borderRadius: borderRadius.sm, padding: spacing.m, marginBottom: spacing.m, borderWidth: 1, borderColor: colors.border }}>
{/* Sélection livreur */}
<Text style={[s.rowDesc, { marginBottom: spacing.s }]}>Livreur</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs, marginBottom: spacing.m }}>
{livreurs.length === 0 ? (
<Text style={[s.rowDesc, { fontStyle: "italic" }]}>Aucun livreur actif</Text>
) : livreurs.map((username) => {
const sel = route.deliveryman_username === username;
return (
<TouchableOpacity
key={username}
onPress={() => setSettings((p) => {
const routes = [...p.delivery_mode.category_routes];
routes[idx] = { ...routes[idx], deliveryman_username: username };
return { ...p, delivery_mode: { ...p.delivery_mode, category_routes: routes } };
})}
style={{ paddingHorizontal: spacing.m, paddingVertical: spacing.xs, borderRadius: borderRadius.full, borderWidth: 1.5, borderColor: sel ? "#10b981" : colors.border, backgroundColor: sel ? "#10b98122" : "transparent" }}
>
<Text style={{ fontSize: fontSize.sm, color: sel ? "#10b981" : colors.text, fontWeight: sel ? "600" : "400" }}>{username}</Text>
</TouchableOpacity>
);
})}
</View>
{/* Sélection catégories */}
<Text style={[s.rowDesc, { marginBottom: spacing.s }]}>Catégories assignées</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
{categories.map((cat) => {
const sel = route.categories.includes(cat.name);
return (
<TouchableOpacity
key={cat.name}
onPress={() => setSettings((p) => {
const routes = [...p.delivery_mode.category_routes];
const cats = routes[idx].categories.includes(cat.name)
? routes[idx].categories.filter((c) => c !== cat.name)
: [...routes[idx].categories, cat.name];
routes[idx] = { ...routes[idx], categories: cats };
return { ...p, delivery_mode: { ...p.delivery_mode, category_routes: routes } };
})}
style={{ paddingHorizontal: spacing.m, paddingVertical: spacing.xs, borderRadius: borderRadius.full, borderWidth: 1.5, borderColor: sel ? "#3b82f6" : colors.border, backgroundColor: sel ? "#3b82f622" : "transparent" }}
>
<Text style={{ fontSize: fontSize.sm, color: sel ? "#3b82f6" : colors.text, fontWeight: sel ? "600" : "400" }}>{cat.name}</Text>
</TouchableOpacity>
);
})}
</View>
{/* Supprimer route */}
<TouchableOpacity
onPress={() => setSettings((p) => {
const routes = p.delivery_mode.category_routes.filter((_, i) => i !== idx);
return { ...p, delivery_mode: { ...p.delivery_mode, category_routes: routes } };
})}
style={{ marginTop: spacing.m, flexDirection: "row", alignItems: "center", gap: spacing.xs }}
>
<Ionicons name="trash-outline" size={14} color="#ef4444" />
<Text style={{ fontSize: fontSize.sm, color: "#ef4444" }}>Supprimer cette règle</Text>
</TouchableOpacity>
</View>
))}
<TouchableOpacity
onPress={() => setSettings((p) => ({
...p,
delivery_mode: {
...p.delivery_mode,
category_routes: [...p.delivery_mode.category_routes, { deliveryman_username: "", categories: [] }],
},
}))}
style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, paddingVertical: spacing.m }}
>
<Ionicons name="add-circle-outline" size={18} color="#10b981" />
<Text style={{ fontSize: fontSize.sm, color: "#10b981", fontWeight: "600" }}>Ajouter une règle</Text>
</TouchableOpacity>
</View>
)}
</View>
<TouchableOpacity <TouchableOpacity
style={s.saveButton} style={s.saveButton}
onPress={handleSave} onPress={handleSave}