chore: add parrainage

This commit is contained in:
2026-03-08 13:57:25 +01:00
parent 51c65d1179
commit fee1972435
20 changed files with 1356 additions and 348 deletions
@@ -16,7 +16,10 @@ import {
getAllClients,
getAvailableDeliveryPersons,
getCommandCountByStatus,
registerAdminPushToken,
unregisterAdminPushToken,
} from "../../api/api_admin";
import { getExpoPushToken } from "../../utils/pushTokenUtils";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
interface StatCard {
@@ -33,6 +36,14 @@ export default function DashboardScreen() {
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// Enregistrement push token admin au montage
useEffect(() => {
getExpoPushToken().then((token) => {
if (token) registerAdminPushToken(token);
});
return () => { unregisterAdminPushToken(); };
}, []);
const loadStats = useCallback(async () => {
try {
const [allCmd, pending, enRoute, completed, clients, livreurs] =
@@ -149,6 +149,7 @@ export default function SettingsScreen() {
points_separated: true,
points_weed_tiers: [],
points_zipette_tiers: [],
referral_enabled: true,
});
const [categories, setCategories] = useState<Category[]>([]);
@@ -350,6 +351,28 @@ export default function SettingsScreen() {
</View>
</View>
{/* Parrainage */}
<View style={s.section}>
<Text style={s.sectionTitle}>Parrainage</Text>
<View style={[s.row, s.rowFirst]}>
<View style={s.rowLeft}>
<Text style={s.rowLabel}>Parrainage activé</Text>
<Text style={s.rowDesc}>
Les clients peuvent voir et utiliser leur solde parrainage au checkout.{"\n"}
Désactivé : le solde reste en base mais ne peut plus être utilisé.
</Text>
</View>
<Switch
value={settings.referral_enabled}
onValueChange={(v) =>
setSettings((prev) => ({ ...prev, referral_enabled: v }))
}
trackColor={{ false: colors.border, true: colors.accent }}
thumbColor="#fff"
/>
</View>
</View>
{/* Système de points */}
<View style={s.section}>
<Text style={s.sectionTitle}>Système de points</Text>
+108 -10
View File
@@ -19,6 +19,7 @@ import {
deleteClientAdmin,
createClientByAdmin,
createUserByAdmin,
creditClientReferral,
} from "../../api/api_admin";
import type { ClientResponse } from "../../api/types";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
@@ -155,6 +156,15 @@ export default function UsersScreen() {
const [createTel, setCreateTel] = useState("");
const [creating, setCreating] = useState(false);
// Referral credit modal
const [referralModal, setReferralModal] = useState<{
visible: boolean;
username: string;
currentBalance: number;
}>({ visible: false, username: "", currentBalance: 0 });
const [referralAmount, setReferralAmount] = useState("");
const [referralLoading, setReferralLoading] = useState(false);
const { alert, showError, showConfirm, hideAlert } = useAlert();
// --------------------------------------------------
@@ -318,6 +328,40 @@ export default function UsersScreen() {
);
};
// --------------------------------------------------
// Referral credit
// --------------------------------------------------
const openReferralModal = (client: ClientResponse) => {
setReferralAmount("");
setReferralModal({
visible: true,
username: client.username,
currentBalance: client.referral_balance ?? 0,
});
};
const handleCreditReferral = async () => {
const amount = parseFloat(referralAmount);
if (isNaN(amount) || amount <= 0) {
showError("Erreur", "Entrez un montant valide");
return;
}
setReferralLoading(true);
try {
const res = await creditClientReferral(referralModal.username, amount);
if (res.success) {
setReferralModal((prev) => ({ ...prev, visible: false }));
await loadData();
} else {
showError("Erreur", res.message || "Echec du crédit");
}
} catch (e: any) {
showError("Erreur", e.message);
} finally {
setReferralLoading(false);
}
};
// --------------------------------------------------
// Create user
// --------------------------------------------------
@@ -603,16 +647,28 @@ export default function UsersScreen() {
</View>
<View style={{ flexDirection: "row", gap: spacing.xs }}>
{isClient && (
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openEditClient(item.clientData!)}
>
<Ionicons
name="create-outline"
size={20}
color={colors.info}
/>
</TouchableOpacity>
<>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openReferralModal(item.clientData!)}
>
<Ionicons
name="gift-outline"
size={20}
color={colors.success}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openEditClient(item.clientData!)}
>
<Ionicons
name="create-outline"
size={20}
color={colors.info}
/>
</TouchableOpacity>
</>
)}
{(item.role === "livreur" ||
item.role === "cabine") && (
@@ -704,6 +760,19 @@ export default function UsersScreen() {
</Text>
<Text style={styles.statLabel}>Annul.</Text>
</View>
{(item.clientData.referral_balance ?? 0) > 0 && (
<View style={styles.stat}>
<Text
style={[
styles.statValue,
{ color: colors.success },
]}
>
{(item.clientData.referral_balance ?? 0).toFixed(0)}
</Text>
<Text style={styles.statLabel}>Parrain</Text>
</View>
)}
</View>
</>
)}
@@ -977,6 +1046,35 @@ export default function UsersScreen() {
confirmText={alert.confirmText}
cancelText={alert.cancelText}
/>
{/* Modal crédit parrainage */}
<Modal
visible={referralModal.visible}
onClose={() => setReferralModal((prev) => ({ ...prev, visible: false }))}
title={`Crédit parrainage — ${referralModal.username}`}
icon="gift-outline"
iconColor={colors.success}
>
<Text style={{ color: colors.textSecondary, fontSize: fontSize.sm, marginBottom: spacing.s }}>
Solde actuel : {referralModal.currentBalance.toFixed(2)} €
</Text>
<TextInput
placeholder="Montant à créditer (ex: 10)"
value={referralAmount}
onChangeText={setReferralAmount}
keyboardType="decimal-pad"
icon={<Ionicons name="cash-outline" size={18} color={colors.textMuted} />}
/>
<Button
title={referralLoading ? "Chargement..." : "Créditer"}
onPress={handleCreditReferral}
disabled={referralLoading}
variant="success"
size="md"
fullWidth
style={{ marginTop: spacing.m }}
/>
</Modal>
</View>
);
}
@@ -12,12 +12,21 @@ import { useTheme } from "../../context/ThemeContext";
import { shadows } from "../../theme/shadows";
import { useAuth } from "../../auth/AuthContext";
import { getAllCommands } from "../../api/api_admin";
import { getAllDeliveryPersonsWithDetails } from "../../api/api_cabine";
import { getAllDeliveryPersonsWithDetails, registerCabinePushToken, unregisterCabinePushToken } from "../../api/api_cabine";
import { getExpoPushToken } from "../../utils/pushTokenUtils";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
export default function DashboardScreen() {
const { colors } = useTheme();
const { username } = useAuth();
// Enregistrement push token cabine au montage
useEffect(() => {
getExpoPushToken().then((token) => {
if (token) registerCabinePushToken(token);
});
return () => { unregisterCabinePushToken(); };
}, []);
const [stats, setStats] = useState<
Array<{
label: string;