chore: add input parrain and reset point and add amende for admin
This commit is contained in:
@@ -475,6 +475,21 @@ export const deleteClientAdmin = async (clientId: number) => {
|
|||||||
// PARRAINAGE ADMIN
|
// 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 (
|
export const creditClientReferral = async (
|
||||||
username: string,
|
username: string,
|
||||||
amount: number,
|
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
|
// ALERTES
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -22,9 +22,14 @@ import {
|
|||||||
deleteClientAdmin,
|
deleteClientAdmin,
|
||||||
createClientByAdmin,
|
createClientByAdmin,
|
||||||
createUserByAdmin,
|
createUserByAdmin,
|
||||||
|
setClientParrain,
|
||||||
creditClientReferral,
|
creditClientReferral,
|
||||||
getClientCancelledOrders,
|
getClientCancelledOrders,
|
||||||
getSettings,
|
getSettings,
|
||||||
|
applyClientPenalty,
|
||||||
|
resetClientPenalties,
|
||||||
|
resetClientPoints,
|
||||||
|
addClientPoints,
|
||||||
} from "../../api/api_admin";
|
} from "../../api/api_admin";
|
||||||
import type { CancelledOrder } from "../../api/api_admin";
|
import type { CancelledOrder } from "../../api/api_admin";
|
||||||
import type { ClientResponse } from "../../api/types";
|
import type { ClientResponse } from "../../api/types";
|
||||||
@@ -169,6 +174,7 @@ export default function UsersScreen() {
|
|||||||
const [createNom, setCreateNom] = useState("");
|
const [createNom, setCreateNom] = useState("");
|
||||||
const [createPrenom, setCreatePrenom] = useState("");
|
const [createPrenom, setCreatePrenom] = useState("");
|
||||||
const [createTel, setCreateTel] = useState("");
|
const [createTel, setCreateTel] = useState("");
|
||||||
|
const [createParrain, setCreateParrain] = useState("");
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
// Referral credit modal
|
// Referral credit modal
|
||||||
@@ -180,6 +186,18 @@ export default function UsersScreen() {
|
|||||||
const [referralAmount, setReferralAmount] = useState("");
|
const [referralAmount, setReferralAmount] = useState("");
|
||||||
const [referralLoading, setReferralLoading] = useState(false);
|
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
|
// Cancelled orders modal
|
||||||
const [cancelledModal, setCancelledModal] = useState<{
|
const [cancelledModal, setCancelledModal] = useState<{
|
||||||
visible: boolean;
|
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
|
// Cancelled orders
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
@@ -448,6 +541,7 @@ export default function UsersScreen() {
|
|||||||
setCreateNom("");
|
setCreateNom("");
|
||||||
setCreatePrenom("");
|
setCreatePrenom("");
|
||||||
setCreateTel("");
|
setCreateTel("");
|
||||||
|
setCreateParrain("");
|
||||||
setCreateModal(true);
|
setCreateModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -487,6 +581,9 @@ export default function UsersScreen() {
|
|||||||
prenom: createPrenom.trim(),
|
prenom: createPrenom.trim(),
|
||||||
telephone: createTel.trim(),
|
telephone: createTel.trim(),
|
||||||
});
|
});
|
||||||
|
if (createParrain.trim()) {
|
||||||
|
await setClientParrain(usernameToCreate, createParrain.trim());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await createUserByAdmin({
|
await createUserByAdmin({
|
||||||
username: usernameToCreate,
|
username: usernameToCreate,
|
||||||
@@ -790,6 +887,16 @@ export default function UsersScreen() {
|
|||||||
color={colors.danger}
|
color={colors.danger}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.editIconBtn}
|
||||||
|
onPress={() => openSanctionModal(item.clientData!)}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="wallet-outline"
|
||||||
|
size={20}
|
||||||
|
color={colors.warning}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.editIconBtn}
|
style={styles.editIconBtn}
|
||||||
onPress={() => openEditClient(item.clientData!)}
|
onPress={() => openEditClient(item.clientData!)}
|
||||||
@@ -1188,6 +1295,15 @@ export default function UsersScreen() {
|
|||||||
onChangeText={setCreateTel}
|
onChangeText={setCreateTel}
|
||||||
keyboardType="phone-pad"
|
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
|
<Button
|
||||||
@@ -1270,6 +1386,208 @@ export default function UsersScreen() {
|
|||||||
)}
|
)}
|
||||||
</Modal>
|
</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 crédit parrainage */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={referralModal.visible}
|
visible={referralModal.visible}
|
||||||
|
|||||||
Reference in New Issue
Block a user