chore: update

This commit is contained in:
2026-03-08 16:33:50 +01:00
parent 712d667d86
commit dc9e812391
5 changed files with 476 additions and 66 deletions
+35 -10
View File
@@ -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",
};
} }
}; };
+60 -6
View File
@@ -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]}>