chore: add input parrain and reset point and add amende for admin

This commit is contained in:
2026-04-22 09:20:28 +02:00
parent f348a87a0b
commit efb0597c00
2 changed files with 400 additions and 0 deletions
+82
View File
@@ -475,6 +475,21 @@ export const deleteClientAdmin = async (clientId: number) => {
// PARRAINAGE ADMIN
// ============================================
export const setClientParrain = async (
username: string,
parrain: string,
): Promise<{ success: boolean; error?: string }> => {
try {
await apiClient.post(
`${V2}/admin/protected/client/${username}/parrain/set`,
{ parrain },
);
return { success: true };
} catch (e: any) {
return { success: false, error: e.response?.data?.error || "Erreur" };
}
};
export const creditClientReferral = async (
username: string,
amount: number,
@@ -541,6 +556,73 @@ export const getClientCancelledOrders = async (
}
};
// ============================================
// AMENDES & POINTS CLIENT (Admin)
// ============================================
export const applyClientPenalty = async (
username: string,
points: number,
reason: string,
): Promise<{ success: boolean; error?: string }> => {
try {
await apiClient.post(`${V2}/admin/protected/penalty`, {
username,
points,
reason,
});
return { success: true };
} catch (e: any) {
return { success: false, error: e.response?.data?.error || "Erreur" };
}
};
export const resetClientPenalties = async (
username: string,
resetCancellationsCount = false,
): Promise<{ success: boolean; error?: string }> => {
try {
await apiClient.post(
`${V2}/admin/protected/client/${username}/penalties/reset`,
{ reset_cancellations_count: resetCancellationsCount },
);
return { success: true };
} catch (e: any) {
return { success: false, error: e.response?.data?.error || "Erreur" };
}
};
export const resetClientPoints = async (
username: string,
pool = -1,
): Promise<{ success: boolean; error?: string }> => {
try {
await apiClient.post(
`${V2}/admin/protected/client/${username}/point/reset`,
{ pool },
);
return { success: true };
} catch (e: any) {
return { success: false, error: e.response?.data?.error || "Erreur" };
}
};
export const addClientPoints = async (
username: string,
poolKey: string,
points: number,
): Promise<{ success: boolean; error?: string }> => {
try {
await apiClient.post(
`${V2}/admin/protected/client/${username}/points/add`,
{ pool_key: poolKey, points },
);
return { success: true };
} catch (e: any) {
return { success: false, error: e.response?.data?.error || "Erreur" };
}
};
// ============================================
// ALERTES
// ============================================
@@ -22,9 +22,14 @@ import {
deleteClientAdmin,
createClientByAdmin,
createUserByAdmin,
setClientParrain,
creditClientReferral,
getClientCancelledOrders,
getSettings,
applyClientPenalty,
resetClientPenalties,
resetClientPoints,
addClientPoints,
} from "../../api/api_admin";
import type { CancelledOrder } from "../../api/api_admin";
import type { ClientResponse } from "../../api/types";
@@ -169,6 +174,7 @@ export default function UsersScreen() {
const [createNom, setCreateNom] = useState("");
const [createPrenom, setCreatePrenom] = useState("");
const [createTel, setCreateTel] = useState("");
const [createParrain, setCreateParrain] = useState("");
const [creating, setCreating] = useState(false);
// Referral credit modal
@@ -180,6 +186,18 @@ export default function UsersScreen() {
const [referralAmount, setReferralAmount] = useState("");
const [referralLoading, setReferralLoading] = useState(false);
// Points & amende modal
const [sanctionModal, setSanctionModal] = useState<{
visible: boolean;
client: ClientResponse | null;
}>({ visible: false, client: null });
const [sanctionTab, setSanctionTab] = useState<"amende" | "points">("amende");
const [amendeAmount, setAmendeAmount] = useState("");
const [amendeReason, setAmendeReason] = useState("");
const [amendeLoading, setAmendeLoading] = useState(false);
const [pointsInputs, setPointsInputs] = useState<Record<string, string>>({});
const [pointsLoading, setPointsLoading] = useState<Record<string, boolean>>({});
// Cancelled orders modal
const [cancelledModal, setCancelledModal] = useState<{
visible: boolean;
@@ -429,6 +447,81 @@ export default function UsersScreen() {
}
};
// --------------------------------------------------
// Points & amende
// --------------------------------------------------
const openSanctionModal = (client: ClientResponse) => {
setAmendeAmount("");
setAmendeReason("");
setPointsInputs({});
setPointsLoading({});
setSanctionTab("amende");
setSanctionModal({ visible: true, client });
};
const handleAddAmende = async () => {
const pts = parseInt(amendeAmount, 10);
if (isNaN(pts) || pts <= 0) {
showError("Erreur", "Entrez un montant valide");
return;
}
if (!amendeReason.trim()) {
showError("Erreur", "La raison est obligatoire");
return;
}
if (!sanctionModal.client) return;
setAmendeLoading(true);
const res = await applyClientPenalty(sanctionModal.client.username, pts, amendeReason.trim());
setAmendeLoading(false);
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
setAmendeAmount("");
setAmendeReason("");
await loadData();
};
const handleResetAmende = () => {
if (!sanctionModal.client) return;
showConfirm(
"Remettre à zéro",
`Remettre l'amende de "${sanctionModal.client.username}" à 0 ?`,
async () => {
const res = await resetClientPenalties(sanctionModal.client!.username, false);
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
await loadData();
},
"Remettre à zéro",
);
};
const handleAddPoints = async (poolKey: string) => {
const pts = parseInt(pointsInputs[poolKey] || "", 10);
if (isNaN(pts) || pts <= 0) {
showError("Erreur", "Entrez un nombre de points valide");
return;
}
if (!sanctionModal.client) return;
setPointsLoading((prev) => ({ ...prev, [poolKey]: true }));
const res = await addClientPoints(sanctionModal.client.username, poolKey, pts);
setPointsLoading((prev) => ({ ...prev, [poolKey]: false }));
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
setPointsInputs((prev) => ({ ...prev, [poolKey]: "" }));
await loadData();
};
const handleResetPoints = (poolKey: string, poolName: string, poolIdx: number) => {
if (!sanctionModal.client) return;
showConfirm(
"Remettre à zéro",
`Remettre les points "${poolName}" de "${sanctionModal.client.username}" à 0 ?`,
async () => {
const res = await resetClientPoints(sanctionModal.client!.username, poolIdx);
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
await loadData();
},
"Remettre à zéro",
);
};
// --------------------------------------------------
// Cancelled orders
// --------------------------------------------------
@@ -448,6 +541,7 @@ export default function UsersScreen() {
setCreateNom("");
setCreatePrenom("");
setCreateTel("");
setCreateParrain("");
setCreateModal(true);
};
@@ -487,6 +581,9 @@ export default function UsersScreen() {
prenom: createPrenom.trim(),
telephone: createTel.trim(),
});
if (createParrain.trim()) {
await setClientParrain(usernameToCreate, createParrain.trim());
}
} else {
await createUserByAdmin({
username: usernameToCreate,
@@ -790,6 +887,16 @@ export default function UsersScreen() {
color={colors.danger}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openSanctionModal(item.clientData!)}
>
<Ionicons
name="wallet-outline"
size={20}
color={colors.warning}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openEditClient(item.clientData!)}
@@ -1188,6 +1295,15 @@ export default function UsersScreen() {
onChangeText={setCreateTel}
keyboardType="phone-pad"
/>
{referralEnabled && (
<TextInput
label="Parrain (optionnel)"
value={createParrain}
onChangeText={setCreateParrain}
autoCapitalize="none"
icon={<Ionicons name="gift-outline" size={18} color={colors.textMuted} />}
/>
)}
</>
)}
<Button
@@ -1270,6 +1386,208 @@ export default function UsersScreen() {
)}
</Modal>
{/* Modal points & amendes */}
<Modal
visible={sanctionModal.visible}
onClose={() => setSanctionModal({ visible: false, client: null })}
title={`Points & amendes — ${sanctionModal.client?.username ?? ""}`}
icon="wallet-outline"
iconColor={colors.warning}
>
{/* Onglets */}
<View style={{ flexDirection: "row", gap: spacing.s, marginBottom: spacing.l }}>
{(["amende", "points"] as const).map((tab) => (
<TouchableOpacity
key={tab}
onPress={() => setSanctionTab(tab)}
style={{
flex: 1,
alignItems: "center",
paddingVertical: spacing.s,
borderRadius: borderRadius.md,
backgroundColor:
sanctionTab === tab
? (tab === "amende" ? colors.danger : colors.success) + "20"
: colors.bgInput,
borderWidth: 1,
borderColor:
sanctionTab === tab
? tab === "amende" ? colors.danger : colors.success
: colors.border,
}}
>
<Ionicons
name={tab === "amende" ? "warning-outline" : "star-outline"}
size={16}
color={
sanctionTab === tab
? tab === "amende" ? colors.danger : colors.success
: colors.textMuted
}
/>
<Text style={{
marginTop: 2,
fontSize: fontSize.xs,
fontWeight: "600",
color:
sanctionTab === tab
? tab === "amende" ? colors.danger : colors.success
: colors.textMuted,
}}>
{tab === "amende" ? "Amende" : "Points"}
</Text>
</TouchableOpacity>
))}
</View>
{sanctionTab === "amende" && (
<>
{/* Valeur actuelle */}
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.bgInput,
borderRadius: borderRadius.md,
padding: spacing.m,
marginBottom: spacing.m,
}}>
<Text style={{ color: colors.textSecondary, fontSize: fontSize.sm }}>
Amende actuelle
</Text>
<Text style={{ color: colors.warning, fontSize: fontSize.lg, fontWeight: "700" }}>
{sanctionModal.client?.amende ?? 0} pts
</Text>
</View>
{/* Ajouter amende */}
<TextInput
label="Points à ajouter"
value={amendeAmount}
onChangeText={setAmendeAmount}
keyboardType="numeric"
icon={<Ionicons name="add-circle-outline" size={18} color={colors.textMuted} />}
/>
<TextInput
label="Raison (obligatoire)"
value={amendeReason}
onChangeText={setAmendeReason}
icon={<Ionicons name="document-text-outline" size={18} color={colors.textMuted} />}
/>
<Button
title={amendeLoading ? "Ajout..." : "Ajouter l'amende"}
onPress={handleAddAmende}
disabled={amendeLoading}
variant="danger"
fullWidth
style={{ marginBottom: spacing.s }}
/>
{/* Remettre à zéro */}
<Button
title="Remettre l'amende à zéro"
onPress={handleResetAmende}
variant="outline"
fullWidth
/>
</>
)}
{sanctionTab === "points" && (
<ScrollView showsVerticalScrollIndicator={false} style={{ maxHeight: 380 }}>
{poolKeys.length === 0 ? (
<Text style={{ color: colors.textMuted, textAlign: "center", paddingVertical: spacing.xl }}>
Aucun pool de points configuré
</Text>
) : poolKeys.map((key, idx) => {
const name = poolNames[idx] ?? key;
const current = sanctionModal.client?.points_extra?.[key] ?? 0;
const color = idx === 0 ? colors.success : idx === 1 ? colors.info : colors.warning;
return (
<View key={key} style={{
borderWidth: 1,
borderColor: colors.border,
borderRadius: borderRadius.md,
padding: spacing.m,
marginBottom: spacing.m,
gap: spacing.s,
}}>
{/* Header pool */}
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Text style={{ color: colors.textWhite, fontWeight: "700", fontSize: fontSize.sm }}>
{name}
</Text>
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
<Ionicons name="star" size={14} color={color} />
<Text style={{ color, fontWeight: "700", fontSize: fontSize.md }}>
{current}
</Text>
</View>
</View>
{/* Input + bouton ajouter */}
<View style={{ flexDirection: "row", gap: spacing.s, alignItems: "center" }}>
<View style={{ flex: 1 }}>
<TextInput
placeholder="Points à ajouter"
value={pointsInputs[key] ?? ""}
onChangeText={(v) =>
setPointsInputs((prev) => ({ ...prev, [key]: v }))
}
keyboardType="numeric"
/>
</View>
<TouchableOpacity
onPress={() => handleAddPoints(key)}
disabled={pointsLoading[key]}
style={{
backgroundColor: color + "20",
borderWidth: 1,
borderColor: color,
borderRadius: borderRadius.md,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
alignItems: "center",
justifyContent: "center",
minWidth: 72,
}}
>
{pointsLoading[key] ? (
<ActivityIndicator size="small" color={color} />
) : (
<Text style={{ color, fontWeight: "700", fontSize: fontSize.sm }}>
+ Ajouter
</Text>
)}
</TouchableOpacity>
</View>
{/* Bouton reset */}
<TouchableOpacity
onPress={() => handleResetPoints(key, name, idx)}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: spacing.xs,
paddingVertical: spacing.s,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.border,
}}
>
<Ionicons name="refresh-outline" size={14} color={colors.textMuted} />
<Text style={{ color: colors.textMuted, fontSize: fontSize.xs, fontWeight: "600" }}>
Remettre à zéro
</Text>
</TouchableOpacity>
</View>
);
})}
</ScrollView>
)}
</Modal>
{/* Modal crédit parrainage */}
<Modal
visible={referralModal.visible}