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 },
|
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user