chore: update
This commit is contained in:
@@ -562,7 +562,6 @@ export const deleteProductMediaAdmin = async (
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ============================================
|
||||
// ADDRESSES
|
||||
// ============================================
|
||||
@@ -711,31 +710,48 @@ export interface AppSettings {
|
||||
points_enabled: boolean;
|
||||
points_categories_weed: string[];
|
||||
points_categories_zipette: string[];
|
||||
points_categories_total: string[];
|
||||
points_separated: boolean;
|
||||
points_weed_tiers: PointsTier[];
|
||||
points_zipette_tiers: PointsTier[];
|
||||
points_total_tiers: PointsTier[];
|
||||
referral_enabled: boolean;
|
||||
}
|
||||
|
||||
export const getSettings = async (): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
|
||||
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" };
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || "Erreur",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const registerAdminPushToken = async (pushToken: string): Promise<void> => {
|
||||
export const registerAdminPushToken = async (
|
||||
pushToken: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await apiClient.post(`${V2}/admin/protected/push-token`, { push_token: pushToken });
|
||||
} catch { /* ignore */ }
|
||||
await apiClient.post(`${V2}/admin/protected/push-token`, {
|
||||
push_token: pushToken,
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
export const unregisterAdminPushToken = async (): Promise<void> => {
|
||||
try {
|
||||
await apiClient.delete(`${V2}/admin/protected/push-token`);
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
export interface AppNotification {
|
||||
@@ -746,7 +762,10 @@ export interface AppNotification {
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export const getAdminNotifications = async (): Promise<{ notifications: AppNotification[]; unread_count: number }> => {
|
||||
export const getAdminNotifications = async (): Promise<{
|
||||
notifications: AppNotification[];
|
||||
unread_count: number;
|
||||
}> => {
|
||||
const { data } = await apiClient.get(`${V2}/admin/protected/notifications`);
|
||||
return data;
|
||||
};
|
||||
@@ -759,9 +778,15 @@ export const updateSettings = async (
|
||||
settings: AppSettings,
|
||||
): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
|
||||
try {
|
||||
const { data } = await apiClient.put(`${V2}/admin/protected/settings`, settings);
|
||||
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" };
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || "Erreur",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -351,16 +351,22 @@ export interface PublicSettings {
|
||||
points_separated: boolean;
|
||||
}
|
||||
|
||||
export const registerCabinePushToken = async (pushToken: string): Promise<void> => {
|
||||
export const registerCabinePushToken = async (
|
||||
pushToken: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await apiClient.post(`${API}/push-token`, { push_token: pushToken });
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
export const unregisterCabinePushToken = async (): Promise<void> => {
|
||||
try {
|
||||
await apiClient.delete(`${API}/push-token`);
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
export interface AppNotification {
|
||||
@@ -371,7 +377,10 @@ export interface AppNotification {
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export const getCabineNotifications = async (): Promise<{ notifications: AppNotification[]; unread_count: number }> => {
|
||||
export const getCabineNotifications = async (): Promise<{
|
||||
notifications: AppNotification[];
|
||||
unread_count: number;
|
||||
}> => {
|
||||
const { data } = await apiClient.get(`${API}/notifications`);
|
||||
return data;
|
||||
};
|
||||
@@ -382,7 +391,9 @@ export const markCabineNotificationsRead = async (): Promise<void> => {
|
||||
|
||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`http://5.181.0.112/api/v1/app-settings`);
|
||||
const { data } = await apiClient.get(
|
||||
`http://5.181.0.112/api/v1/app-settings`,
|
||||
);
|
||||
return {
|
||||
penalties_enabled: data.penalties_enabled ?? true,
|
||||
show_amende_score: data.show_amende_score ?? true,
|
||||
@@ -390,6 +401,49 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
points_separated: data.points_separated ?? true,
|
||||
};
|
||||
} catch {
|
||||
return { penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true };
|
||||
return {
|
||||
penalties_enabled: true,
|
||||
show_amende_score: true,
|
||||
points_enabled: true,
|
||||
points_separated: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ADDRESSES
|
||||
// ============================================
|
||||
|
||||
export const addAddress = async (
|
||||
invalidAddress: string,
|
||||
correctAddress: string,
|
||||
): Promise<{ success: boolean; message: string }> => {
|
||||
const { data } = await apiClient.post(`${API}/add/address`, {
|
||||
invalid_address: invalidAddress,
|
||||
correct_address: correctAddress,
|
||||
});
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const deleteAddress = async (
|
||||
invalidAddress: string,
|
||||
correctAddress: string,
|
||||
): Promise<{ success: boolean; message: string }> => {
|
||||
const { data } = await apiClient.delete(`${API}/delete/address`, {
|
||||
data: {
|
||||
invalid_address: invalidAddress,
|
||||
correct_address: correctAddress,
|
||||
},
|
||||
});
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const getAllAddresses = async (): Promise<
|
||||
{ invalid_address: string; correct_address: string }[]
|
||||
> => {
|
||||
const { data } = await apiClient.get(`${API}/addresses`);
|
||||
return (data.addresses ?? []).map((a: any) => ({
|
||||
invalid_address: a.invalid_address ?? a.InvalidAddress ?? "",
|
||||
correct_address: a.correct_address ?? a.CorrectAddress ?? "",
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { AppSettings, Category, PointsTier } from "../../api/api_admin";
|
||||
import AlertModal from "../../components/ui/AlertModal";
|
||||
import { useAlert } from "../../hooks/useAlert";
|
||||
|
||||
type PoolAssignment = "weed" | "zipette" | "none";
|
||||
type PoolAssignment = "weed" | "zipette" | "total" | "none";
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Composant éditeur de paliers
|
||||
@@ -146,9 +146,11 @@ export default function SettingsScreen() {
|
||||
points_enabled: true,
|
||||
points_categories_weed: [],
|
||||
points_categories_zipette: [],
|
||||
points_categories_total: [],
|
||||
points_separated: true,
|
||||
points_weed_tiers: [],
|
||||
points_zipette_tiers: [],
|
||||
points_total_tiers: [],
|
||||
referral_enabled: true,
|
||||
});
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
@@ -164,6 +166,8 @@ export default function SettingsScreen() {
|
||||
...settingsRes.settings,
|
||||
points_categories_weed: settingsRes.settings.points_categories_weed ?? [],
|
||||
points_categories_zipette: settingsRes.settings.points_categories_zipette ?? [],
|
||||
points_categories_total: settingsRes.settings.points_categories_total ?? [],
|
||||
points_total_tiers: settingsRes.settings.points_total_tiers ?? [],
|
||||
});
|
||||
}
|
||||
if (categoriesRes) {
|
||||
@@ -179,6 +183,7 @@ export default function SettingsScreen() {
|
||||
const getPoolFor = (name: string): PoolAssignment => {
|
||||
if ((settings.points_categories_weed ?? []).includes(name)) return "weed";
|
||||
if ((settings.points_categories_zipette ?? []).includes(name)) return "zipette";
|
||||
if ((settings.points_categories_total ?? []).includes(name)) return "total";
|
||||
return "none";
|
||||
};
|
||||
|
||||
@@ -186,9 +191,11 @@ export default function SettingsScreen() {
|
||||
setSettings((prev) => {
|
||||
const weed = (prev.points_categories_weed ?? []).filter((c) => c !== name);
|
||||
const zipette = (prev.points_categories_zipette ?? []).filter((c) => c !== name);
|
||||
const total = (prev.points_categories_total ?? []).filter((c) => c !== name);
|
||||
if (pool === "weed") weed.push(name);
|
||||
else if (pool === "zipette") zipette.push(name);
|
||||
return { ...prev, points_categories_weed: weed, points_categories_zipette: zipette };
|
||||
else if (pool === "total") total.push(name);
|
||||
return { ...prev, points_categories_weed: weed, points_categories_zipette: zipette, points_categories_total: total };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -310,6 +317,17 @@ export default function SettingsScreen() {
|
||||
|
||||
const WEED_COLOR = "#10b981";
|
||||
const ZIP_COLOR = "#9333ea";
|
||||
const TOTAL_COLOR = "#f97316";
|
||||
|
||||
// Le mode séparé nécessite que W ET Z aient au moins une catégorie
|
||||
const hasBothPools =
|
||||
(settings.points_categories_weed ?? []).length > 0 &&
|
||||
(settings.points_categories_zipette ?? []).length > 0;
|
||||
|
||||
// Chips disponibles selon le mode
|
||||
const availableChips: PoolAssignment[] = settings.points_separated
|
||||
? ["weed", "zipette", "none"] // mode séparé : T désactivé
|
||||
: ["total", "none"]; // mode non-séparé : W/Z désactivés
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
@@ -393,16 +411,20 @@ export default function SettingsScreen() {
|
||||
thumbColor="#fff"
|
||||
/>
|
||||
</View>
|
||||
<View style={s.row}>
|
||||
<View style={[s.row, { opacity: hasBothPools ? 1 : 0.4 }]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Points séparés par pool</Text>
|
||||
<Text style={s.rowDesc}>
|
||||
Activé : Weed → point / Zipette → point_zipette{"\n"}
|
||||
Désactivé : tout dans un seul compteur (point)
|
||||
Désactivé : Barème Total sur la somme W+Z{"\n"}
|
||||
{!hasBothPools && (
|
||||
"⚠️ Nécessite des catégories W ET Z configurées"
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.points_separated}
|
||||
disabled={!hasBothPools}
|
||||
onValueChange={(v) =>
|
||||
setSettings((prev) => ({ ...prev, points_separated: v }))
|
||||
}
|
||||
@@ -416,19 +438,30 @@ export default function SettingsScreen() {
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Attribution des catégories aux points</Text>
|
||||
<Text style={s.hint}>
|
||||
Pour chaque catégorie, choisis si elle génère des points Weed, Zipette, ou aucun.
|
||||
{settings.points_separated
|
||||
? "Mode séparé : W = pool Weed, Z = pool Zipette."
|
||||
: "Mode non-séparé : T = pool Total (barème sur W+Z). W et Z désactivés."}
|
||||
</Text>
|
||||
|
||||
{/* Légende */}
|
||||
<View style={{ flexDirection: "row", gap: spacing.m, paddingHorizontal: spacing.l, paddingBottom: spacing.m }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<View style={[s.colorDot, { backgroundColor: WEED_COLOR }]} />
|
||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Weed</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<View style={[s.colorDot, { backgroundColor: ZIP_COLOR }]} />
|
||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Zipette</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", gap: spacing.m, paddingHorizontal: spacing.l, paddingBottom: spacing.m, flexWrap: "wrap" }}>
|
||||
{settings.points_separated ? (
|
||||
<>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<View style={[s.colorDot, { backgroundColor: WEED_COLOR }]} />
|
||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Weed</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<View style={[s.colorDot, { backgroundColor: ZIP_COLOR }]} />
|
||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Zipette</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<View style={[s.colorDot, { backgroundColor: TOTAL_COLOR }]} />
|
||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Total</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<View style={[s.colorDot, { backgroundColor: colors.border }]} />
|
||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Aucun</Text>
|
||||
@@ -450,21 +483,24 @@ export default function SettingsScreen() {
|
||||
/>
|
||||
<Text style={s.catName}>{cat.name}</Text>
|
||||
<View style={s.chips}>
|
||||
{(["weed", "zipette", "none"] as PoolAssignment[]).map(
|
||||
(p) => {
|
||||
{availableChips.map((p) => {
|
||||
const active = pool === p;
|
||||
const chipColor =
|
||||
p === "weed"
|
||||
? WEED_COLOR
|
||||
: p === "zipette"
|
||||
? ZIP_COLOR
|
||||
: colors.textMuted;
|
||||
: p === "total"
|
||||
? TOTAL_COLOR
|
||||
: colors.textMuted;
|
||||
const label =
|
||||
p === "weed"
|
||||
? "W"
|
||||
: p === "zipette"
|
||||
? "Z"
|
||||
: "—";
|
||||
: p === "total"
|
||||
? "T"
|
||||
: "—";
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={p}
|
||||
@@ -493,8 +529,7 @@ export default function SettingsScreen() {
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
)}
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -507,23 +542,36 @@ export default function SettingsScreen() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Barème de points */}
|
||||
<TiersSection
|
||||
title="Barème Weed / Hash"
|
||||
tiers={settings.points_weed_tiers ?? []}
|
||||
onChange={(tiers) => setSettings((p) => ({ ...p, points_weed_tiers: tiers }))}
|
||||
colors={colors}
|
||||
s={s}
|
||||
accentColor={WEED_COLOR}
|
||||
/>
|
||||
<TiersSection
|
||||
title="Barème Zipette"
|
||||
tiers={settings.points_zipette_tiers ?? []}
|
||||
onChange={(tiers) => setSettings((p) => ({ ...p, points_zipette_tiers: tiers }))}
|
||||
colors={colors}
|
||||
s={s}
|
||||
accentColor={ZIP_COLOR}
|
||||
/>
|
||||
{/* Barème de points — conditionnel selon le mode */}
|
||||
{settings.points_separated ? (
|
||||
<>
|
||||
<TiersSection
|
||||
title="Barème Weed / Hash"
|
||||
tiers={settings.points_weed_tiers ?? []}
|
||||
onChange={(tiers) => setSettings((p) => ({ ...p, points_weed_tiers: tiers }))}
|
||||
colors={colors}
|
||||
s={s}
|
||||
accentColor={WEED_COLOR}
|
||||
/>
|
||||
<TiersSection
|
||||
title="Barème Zipette"
|
||||
tiers={settings.points_zipette_tiers ?? []}
|
||||
onChange={(tiers) => setSettings((p) => ({ ...p, points_zipette_tiers: tiers }))}
|
||||
colors={colors}
|
||||
s={s}
|
||||
accentColor={ZIP_COLOR}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TiersSection
|
||||
title="Barème Total (W + Z)"
|
||||
tiers={settings.points_total_tiers ?? []}
|
||||
onChange={(tiers) => setSettings((p) => ({ ...p, points_total_tiers: tiers }))}
|
||||
colors={colors}
|
||||
s={s}
|
||||
accentColor={TOTAL_COLOR}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={s.saveButton}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { spacing, fontSize } from "../../theme";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import {
|
||||
addAddress,
|
||||
deleteAddress,
|
||||
getAllAddresses,
|
||||
} from "../../api/api_cabine";
|
||||
import { useAlert } from "../../hooks/useAlert";
|
||||
import Card from "../../components/ui/Card";
|
||||
import Modal from "../../components/ui/Modal";
|
||||
import TextInput from "../../components/ui/TextInput";
|
||||
import Button from "../../components/ui/Button";
|
||||
import AlertModal from "../../components/ui/AlertModal";
|
||||
|
||||
type AddressEntry = {
|
||||
invalid_address: string;
|
||||
correct_address: string;
|
||||
};
|
||||
|
||||
export default function AddressScreen() {
|
||||
const { colors } = useTheme();
|
||||
const { alert, showError, showSuccess, showConfirm, hideAlert } =
|
||||
useAlert();
|
||||
|
||||
const [addresses, setAddresses] = useState<AddressEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [addModal, setAddModal] = useState(false);
|
||||
const [invalidInput, setInvalidInput] = useState("");
|
||||
const [correctInput, setCorrectInput] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const loadAddresses = useCallback(async () => {
|
||||
try {
|
||||
const result = await getAllAddresses();
|
||||
setAddresses(result);
|
||||
} catch (e: any) {
|
||||
showError("Erreur", e.response?.data?.error ?? e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [showError]);
|
||||
|
||||
useEffect(() => {
|
||||
loadAddresses();
|
||||
}, [loadAddresses]);
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await loadAddresses();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
setInvalidInput("");
|
||||
setCorrectInput("");
|
||||
setAddModal(true);
|
||||
};
|
||||
|
||||
const handleAdd = useCallback(async () => {
|
||||
const inv = invalidInput.trim();
|
||||
const cor = correctInput.trim();
|
||||
if (!inv || !cor) {
|
||||
showError("Erreur", "Les deux champs sont obligatoires.");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await addAddress(inv, cor);
|
||||
setAddresses((prev) => [
|
||||
...prev,
|
||||
{ invalid_address: inv, correct_address: cor },
|
||||
]);
|
||||
setAddModal(false);
|
||||
showSuccess("Succès", "Adresse ajoutée avec succès.");
|
||||
} catch (e: any) {
|
||||
showError("Erreur", e.response?.data?.error ?? e.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [invalidInput, correctInput, showError, showSuccess]);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(item: AddressEntry) => {
|
||||
showConfirm(
|
||||
"Supprimer",
|
||||
`Supprimer la correction :\n"${item.invalid_address}" → "${item.correct_address}" ?`,
|
||||
async () => {
|
||||
try {
|
||||
await deleteAddress(
|
||||
item.invalid_address,
|
||||
item.correct_address,
|
||||
);
|
||||
setAddresses((prev) =>
|
||||
prev.filter(
|
||||
(a) =>
|
||||
a.invalid_address !==
|
||||
item.invalid_address ||
|
||||
a.correct_address !== item.correct_address,
|
||||
),
|
||||
);
|
||||
showSuccess("Supprimé", "Adresse supprimée.");
|
||||
} catch (e: any) {
|
||||
showError(
|
||||
"Erreur",
|
||||
e.response?.data?.error ?? e.message,
|
||||
);
|
||||
}
|
||||
},
|
||||
"Supprimer",
|
||||
);
|
||||
},
|
||||
[showConfirm, showError, showSuccess],
|
||||
);
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: spacing.l,
|
||||
paddingBottom: spacing.s,
|
||||
},
|
||||
title: {
|
||||
color: colors.textWhite,
|
||||
fontSize: fontSize.lg,
|
||||
fontWeight: "700",
|
||||
},
|
||||
addBtn: {
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 20,
|
||||
padding: spacing.s,
|
||||
},
|
||||
invalid: {
|
||||
color: colors.danger,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: "600",
|
||||
},
|
||||
correct: {
|
||||
color: colors.success,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: "600",
|
||||
marginTop: 6,
|
||||
},
|
||||
label: {
|
||||
fontSize: fontSize.xs,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 2,
|
||||
},
|
||||
labelInvalid: {
|
||||
color: colors.danger,
|
||||
},
|
||||
labelCorrect: {
|
||||
color: colors.success,
|
||||
},
|
||||
arrow: {
|
||||
color: colors.textMuted,
|
||||
fontSize: fontSize.lg,
|
||||
marginVertical: 4,
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
empty: {
|
||||
color: colors.textMuted,
|
||||
textAlign: "center",
|
||||
marginTop: spacing.xxl,
|
||||
fontSize: fontSize.md,
|
||||
},
|
||||
}),
|
||||
[colors],
|
||||
);
|
||||
|
||||
const renderItem = ({ item }: { item: AddressEntry }) => (
|
||||
<Card style={{ marginBottom: spacing.m }}>
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 1, marginRight: spacing.m }}>
|
||||
<Text style={styles.invalid}>{item.invalid_address}</Text>
|
||||
<Text style={styles.arrow}>↓</Text>
|
||||
<Text style={styles.correct}>{item.correct_address}</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => handleDelete(item)}>
|
||||
<Ionicons
|
||||
name="trash-outline"
|
||||
size={22}
|
||||
color={colors.danger}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Corrections d'adresses</Text>
|
||||
<TouchableOpacity style={styles.addBtn} onPress={openAddModal}>
|
||||
<Ionicons name="add" size={22} color={colors.textWhite} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={addresses}
|
||||
keyExtractor={(item, i) => `${item.invalid_address}-${i}`}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{
|
||||
padding: spacing.l,
|
||||
paddingTop: spacing.s,
|
||||
}}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<Text style={styles.empty}>
|
||||
{loading
|
||||
? "Chargement..."
|
||||
: "Aucune correction.\nAppuyez sur + pour en ajouter une."}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
visible={addModal}
|
||||
onClose={() => setAddModal(false)}
|
||||
title="Ajouter une correction"
|
||||
icon="map-outline"
|
||||
>
|
||||
<TextInput
|
||||
label="Adresse invalide"
|
||||
value={invalidInput}
|
||||
onChangeText={setInvalidInput}
|
||||
placeholder="Ex: 10 rue de la paix"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<TextInput
|
||||
label="Adresse correcte"
|
||||
value={correctInput}
|
||||
onChangeText={setCorrectInput}
|
||||
placeholder="Ex: 10 Rue de la Paix, 75001 Paris"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Button
|
||||
title="Ajouter"
|
||||
onPress={handleAdd}
|
||||
loading={saving}
|
||||
style={{ marginTop: spacing.m }}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<AlertModal
|
||||
visible={alert.visible}
|
||||
type={alert.type}
|
||||
title={alert.title}
|
||||
message={alert.message}
|
||||
onClose={hideAlert}
|
||||
onConfirm={alert.onConfirm}
|
||||
confirmText={alert.confirmText}
|
||||
cancelText={alert.cancelText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -275,19 +275,21 @@ export default function OrderHistoryScreen() {
|
||||
Pts Zipette
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.statCard, shadows.sm]}>
|
||||
<Ionicons
|
||||
name="trophy-outline"
|
||||
size={24}
|
||||
color={colors.warning}
|
||||
/>
|
||||
<Text style={styles.statValue}>
|
||||
{totalPoints}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>
|
||||
Total Points
|
||||
</Text>
|
||||
</View>
|
||||
{(stats?.points || 0) > 0 && (stats?.points_zipette || 0) > 0 && (
|
||||
<View style={[styles.statCard, shadows.sm]}>
|
||||
<Ionicons
|
||||
name="trophy-outline"
|
||||
size={24}
|
||||
color={colors.warning}
|
||||
/>
|
||||
<Text style={styles.statValue}>
|
||||
{totalPoints}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>
|
||||
Total Points
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View style={[styles.statCard, shadows.sm]}>
|
||||
|
||||
Reference in New Issue
Block a user