chore: add multiple delivery mode
This commit is contained in:
@@ -747,6 +747,16 @@ export const DEFAULT_DELIVERY_SCHEDULE: DeliverySchedule = {
|
||||
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 {
|
||||
min_cancel: number;
|
||||
amount: number;
|
||||
@@ -768,6 +778,7 @@ export interface AppSettings {
|
||||
nowpayments_currencies: string[];
|
||||
telegram_bot_token: string;
|
||||
telegram_bot_username: string;
|
||||
delivery_mode: DeliveryModeConfig;
|
||||
}
|
||||
|
||||
export const getSettings = async (): Promise<{
|
||||
|
||||
@@ -14,8 +14,8 @@ import { useNavigation } from "@react-navigation/native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { getSettings, updateSettings, getCategories, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
|
||||
import type { AppSettings, Category, PointsTier, PointsPool, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||
import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } 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 { useAlert } from "../../hooks/useAlert";
|
||||
|
||||
@@ -667,10 +667,12 @@ export default function SettingsScreen() {
|
||||
nowpayments_currencies: [],
|
||||
telegram_bot_token: "",
|
||||
telegram_bot_username: "",
|
||||
delivery_mode: { mode: "single" as const, category_routes: [] },
|
||||
});
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [showIpnSecret, setShowIpnSecret] = useState(false);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [livreurs, setLivreurs] = useState<string[]>([]);
|
||||
|
||||
// Refs pour l'auto-sauvegarde au départ de la page
|
||||
const settingsRef = useRef(settings);
|
||||
@@ -699,9 +701,10 @@ export default function SettingsScreen() {
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
isLoaded.current = false;
|
||||
const [settingsRes, categoriesRes] = await Promise.all([
|
||||
const [settingsRes, categoriesRes, livreursRes] = await Promise.all([
|
||||
getSettings(),
|
||||
getCategories(),
|
||||
getAvailableDeliveryPersons(),
|
||||
]);
|
||||
if (settingsRes.success && settingsRes.settings) {
|
||||
setSettings({
|
||||
@@ -709,11 +712,15 @@ export default function SettingsScreen() {
|
||||
points_pools: settingsRes.settings.points_pools ?? [],
|
||||
delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE,
|
||||
postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES,
|
||||
delivery_mode: settingsRes.settings.delivery_mode ?? { mode: "single", category_routes: [] },
|
||||
});
|
||||
}
|
||||
if (categoriesRes) {
|
||||
setCategories(categoriesRes.filter((c) => !c.is_coming_soon));
|
||||
}
|
||||
if (livreursRes.success) {
|
||||
setLivreurs(livreursRes.livreurs.map((l: any) => l.username));
|
||||
}
|
||||
setLoading(false);
|
||||
// Marquer comme chargé après un tick pour que le useEffect de settings ne
|
||||
// considère pas le setSettings initial comme une modification utilisateur
|
||||
@@ -1425,6 +1432,131 @@ export default function SettingsScreen() {
|
||||
</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
|
||||
style={s.saveButton}
|
||||
onPress={handleSave}
|
||||
|
||||
Reference in New Issue
Block a user