chore: update
This commit is contained in:
@@ -562,7 +562,6 @@ export const deleteProductMediaAdmin = async (
|
|||||||
return { success: true, message: data.message };
|
return { success: true, message: data.message };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// ADDRESSES
|
// ADDRESSES
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -711,31 +710,48 @@ export interface AppSettings {
|
|||||||
points_enabled: boolean;
|
points_enabled: boolean;
|
||||||
points_categories_weed: string[];
|
points_categories_weed: string[];
|
||||||
points_categories_zipette: string[];
|
points_categories_zipette: string[];
|
||||||
|
points_categories_total: string[];
|
||||||
points_separated: boolean;
|
points_separated: boolean;
|
||||||
points_weed_tiers: PointsTier[];
|
points_weed_tiers: PointsTier[];
|
||||||
points_zipette_tiers: PointsTier[];
|
points_zipette_tiers: PointsTier[];
|
||||||
|
points_total_tiers: PointsTier[];
|
||||||
referral_enabled: boolean;
|
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 {
|
try {
|
||||||
const { data } = await apiClient.get(`${V2}/admin/protected/settings`);
|
const { data } = await apiClient.get(`${V2}/admin/protected/settings`);
|
||||||
return { success: true, settings: data.settings };
|
return { success: true, settings: data.settings };
|
||||||
} catch (error: any) {
|
} 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 {
|
try {
|
||||||
await apiClient.post(`${V2}/admin/protected/push-token`, { push_token: pushToken });
|
await apiClient.post(`${V2}/admin/protected/push-token`, {
|
||||||
} catch { /* ignore */ }
|
push_token: pushToken,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unregisterAdminPushToken = async (): Promise<void> => {
|
export const unregisterAdminPushToken = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`${V2}/admin/protected/push-token`);
|
await apiClient.delete(`${V2}/admin/protected/push-token`);
|
||||||
} catch { /* ignore */ }
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AppNotification {
|
export interface AppNotification {
|
||||||
@@ -746,7 +762,10 @@ export interface AppNotification {
|
|||||||
read: boolean;
|
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`);
|
const { data } = await apiClient.get(`${V2}/admin/protected/notifications`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -759,9 +778,15 @@ export const updateSettings = async (
|
|||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
|
): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
|
||||||
try {
|
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 };
|
return { success: true, settings: data.settings };
|
||||||
} catch (error: any) {
|
} 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;
|
points_separated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const registerCabinePushToken = async (pushToken: string): Promise<void> => {
|
export const registerCabinePushToken = async (
|
||||||
|
pushToken: string,
|
||||||
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`${API}/push-token`, { push_token: pushToken });
|
await apiClient.post(`${API}/push-token`, { push_token: pushToken });
|
||||||
} catch { /* ignore */ }
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unregisterCabinePushToken = async (): Promise<void> => {
|
export const unregisterCabinePushToken = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`${API}/push-token`);
|
await apiClient.delete(`${API}/push-token`);
|
||||||
} catch { /* ignore */ }
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AppNotification {
|
export interface AppNotification {
|
||||||
@@ -371,7 +377,10 @@ export interface AppNotification {
|
|||||||
read: boolean;
|
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`);
|
const { data } = await apiClient.get(`${API}/notifications`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -382,7 +391,9 @@ export const markCabineNotificationsRead = async (): Promise<void> => {
|
|||||||
|
|
||||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||||
try {
|
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 {
|
return {
|
||||||
penalties_enabled: data.penalties_enabled ?? true,
|
penalties_enabled: data.penalties_enabled ?? true,
|
||||||
show_amende_score: data.show_amende_score ?? 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,
|
points_separated: data.points_separated ?? true,
|
||||||
};
|
};
|
||||||
} catch {
|
} 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 AlertModal from "../../components/ui/AlertModal";
|
||||||
import { useAlert } from "../../hooks/useAlert";
|
import { useAlert } from "../../hooks/useAlert";
|
||||||
|
|
||||||
type PoolAssignment = "weed" | "zipette" | "none";
|
type PoolAssignment = "weed" | "zipette" | "total" | "none";
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
// Composant éditeur de paliers
|
// Composant éditeur de paliers
|
||||||
@@ -146,9 +146,11 @@ export default function SettingsScreen() {
|
|||||||
points_enabled: true,
|
points_enabled: true,
|
||||||
points_categories_weed: [],
|
points_categories_weed: [],
|
||||||
points_categories_zipette: [],
|
points_categories_zipette: [],
|
||||||
|
points_categories_total: [],
|
||||||
points_separated: true,
|
points_separated: true,
|
||||||
points_weed_tiers: [],
|
points_weed_tiers: [],
|
||||||
points_zipette_tiers: [],
|
points_zipette_tiers: [],
|
||||||
|
points_total_tiers: [],
|
||||||
referral_enabled: true,
|
referral_enabled: true,
|
||||||
});
|
});
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
@@ -164,6 +166,8 @@ export default function SettingsScreen() {
|
|||||||
...settingsRes.settings,
|
...settingsRes.settings,
|
||||||
points_categories_weed: settingsRes.settings.points_categories_weed ?? [],
|
points_categories_weed: settingsRes.settings.points_categories_weed ?? [],
|
||||||
points_categories_zipette: settingsRes.settings.points_categories_zipette ?? [],
|
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) {
|
if (categoriesRes) {
|
||||||
@@ -179,6 +183,7 @@ export default function SettingsScreen() {
|
|||||||
const getPoolFor = (name: string): PoolAssignment => {
|
const getPoolFor = (name: string): PoolAssignment => {
|
||||||
if ((settings.points_categories_weed ?? []).includes(name)) return "weed";
|
if ((settings.points_categories_weed ?? []).includes(name)) return "weed";
|
||||||
if ((settings.points_categories_zipette ?? []).includes(name)) return "zipette";
|
if ((settings.points_categories_zipette ?? []).includes(name)) return "zipette";
|
||||||
|
if ((settings.points_categories_total ?? []).includes(name)) return "total";
|
||||||
return "none";
|
return "none";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,9 +191,11 @@ export default function SettingsScreen() {
|
|||||||
setSettings((prev) => {
|
setSettings((prev) => {
|
||||||
const weed = (prev.points_categories_weed ?? []).filter((c) => c !== name);
|
const weed = (prev.points_categories_weed ?? []).filter((c) => c !== name);
|
||||||
const zipette = (prev.points_categories_zipette ?? []).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);
|
if (pool === "weed") weed.push(name);
|
||||||
else if (pool === "zipette") zipette.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 WEED_COLOR = "#10b981";
|
||||||
const ZIP_COLOR = "#9333ea";
|
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 (
|
return (
|
||||||
<View style={s.container}>
|
<View style={s.container}>
|
||||||
@@ -393,16 +411,20 @@ export default function SettingsScreen() {
|
|||||||
thumbColor="#fff"
|
thumbColor="#fff"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={s.row}>
|
<View style={[s.row, { opacity: hasBothPools ? 1 : 0.4 }]}>
|
||||||
<View style={s.rowLeft}>
|
<View style={s.rowLeft}>
|
||||||
<Text style={s.rowLabel}>Points séparés par pool</Text>
|
<Text style={s.rowLabel}>Points séparés par pool</Text>
|
||||||
<Text style={s.rowDesc}>
|
<Text style={s.rowDesc}>
|
||||||
Activé : Weed → point / Zipette → point_zipette{"\n"}
|
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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.points_separated}
|
value={settings.points_separated}
|
||||||
|
disabled={!hasBothPools}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setSettings((prev) => ({ ...prev, points_separated: v }))
|
setSettings((prev) => ({ ...prev, points_separated: v }))
|
||||||
}
|
}
|
||||||
@@ -416,19 +438,30 @@ export default function SettingsScreen() {
|
|||||||
<View style={s.section}>
|
<View style={s.section}>
|
||||||
<Text style={s.sectionTitle}>Attribution des catégories aux points</Text>
|
<Text style={s.sectionTitle}>Attribution des catégories aux points</Text>
|
||||||
<Text style={s.hint}>
|
<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>
|
</Text>
|
||||||
|
|
||||||
{/* Légende */}
|
{/* Légende */}
|
||||||
<View style={{ flexDirection: "row", gap: spacing.m, paddingHorizontal: spacing.l, paddingBottom: spacing.m }}>
|
<View style={{ flexDirection: "row", gap: spacing.m, paddingHorizontal: spacing.l, paddingBottom: spacing.m, flexWrap: "wrap" }}>
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
{settings.points_separated ? (
|
||||||
<View style={[s.colorDot, { backgroundColor: WEED_COLOR }]} />
|
<>
|
||||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Weed</Text>
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||||
</View>
|
<View style={[s.colorDot, { backgroundColor: WEED_COLOR }]} />
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Weed</Text>
|
||||||
<View style={[s.colorDot, { backgroundColor: ZIP_COLOR }]} />
|
</View>
|
||||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Zipette</Text>
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||||
</View>
|
<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={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||||
<View style={[s.colorDot, { backgroundColor: colors.border }]} />
|
<View style={[s.colorDot, { backgroundColor: colors.border }]} />
|
||||||
<Text style={{ fontSize: fontSize.sm, color: colors.textMuted }}>Aucun</Text>
|
<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>
|
<Text style={s.catName}>{cat.name}</Text>
|
||||||
<View style={s.chips}>
|
<View style={s.chips}>
|
||||||
{(["weed", "zipette", "none"] as PoolAssignment[]).map(
|
{availableChips.map((p) => {
|
||||||
(p) => {
|
|
||||||
const active = pool === p;
|
const active = pool === p;
|
||||||
const chipColor =
|
const chipColor =
|
||||||
p === "weed"
|
p === "weed"
|
||||||
? WEED_COLOR
|
? WEED_COLOR
|
||||||
: p === "zipette"
|
: p === "zipette"
|
||||||
? ZIP_COLOR
|
? ZIP_COLOR
|
||||||
: colors.textMuted;
|
: p === "total"
|
||||||
|
? TOTAL_COLOR
|
||||||
|
: colors.textMuted;
|
||||||
const label =
|
const label =
|
||||||
p === "weed"
|
p === "weed"
|
||||||
? "W"
|
? "W"
|
||||||
: p === "zipette"
|
: p === "zipette"
|
||||||
? "Z"
|
? "Z"
|
||||||
: "—";
|
: p === "total"
|
||||||
|
? "T"
|
||||||
|
: "—";
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={p}
|
key={p}
|
||||||
@@ -493,8 +529,7 @@ export default function SettingsScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -507,23 +542,36 @@ export default function SettingsScreen() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Barème de points */}
|
{/* Barème de points — conditionnel selon le mode */}
|
||||||
<TiersSection
|
{settings.points_separated ? (
|
||||||
title="Barème Weed / Hash"
|
<>
|
||||||
tiers={settings.points_weed_tiers ?? []}
|
<TiersSection
|
||||||
onChange={(tiers) => setSettings((p) => ({ ...p, points_weed_tiers: tiers }))}
|
title="Barème Weed / Hash"
|
||||||
colors={colors}
|
tiers={settings.points_weed_tiers ?? []}
|
||||||
s={s}
|
onChange={(tiers) => setSettings((p) => ({ ...p, points_weed_tiers: tiers }))}
|
||||||
accentColor={WEED_COLOR}
|
colors={colors}
|
||||||
/>
|
s={s}
|
||||||
<TiersSection
|
accentColor={WEED_COLOR}
|
||||||
title="Barème Zipette"
|
/>
|
||||||
tiers={settings.points_zipette_tiers ?? []}
|
<TiersSection
|
||||||
onChange={(tiers) => setSettings((p) => ({ ...p, points_zipette_tiers: tiers }))}
|
title="Barème Zipette"
|
||||||
colors={colors}
|
tiers={settings.points_zipette_tiers ?? []}
|
||||||
s={s}
|
onChange={(tiers) => setSettings((p) => ({ ...p, points_zipette_tiers: tiers }))}
|
||||||
accentColor={ZIP_COLOR}
|
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
|
<TouchableOpacity
|
||||||
style={s.saveButton}
|
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
|
Pts Zipette
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.statCard, shadows.sm]}>
|
{(stats?.points || 0) > 0 && (stats?.points_zipette || 0) > 0 && (
|
||||||
<Ionicons
|
<View style={[styles.statCard, shadows.sm]}>
|
||||||
name="trophy-outline"
|
<Ionicons
|
||||||
size={24}
|
name="trophy-outline"
|
||||||
color={colors.warning}
|
size={24}
|
||||||
/>
|
color={colors.warning}
|
||||||
<Text style={styles.statValue}>
|
/>
|
||||||
{totalPoints}
|
<Text style={styles.statValue}>
|
||||||
</Text>
|
{totalPoints}
|
||||||
<Text style={styles.statLabel}>
|
</Text>
|
||||||
Total Points
|
<Text style={styles.statLabel}>
|
||||||
</Text>
|
Total Points
|
||||||
</View>
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<View style={[styles.statCard, shadows.sm]}>
|
<View style={[styles.statCard, shadows.sm]}>
|
||||||
|
|||||||
Reference in New Issue
Block a user