chore: update point system
This commit is contained in:
@@ -661,6 +661,12 @@ export const deleteCategoryAdmin = async (
|
|||||||
// PARAMÈTRES GLOBAUX
|
// PARAMÈTRES GLOBAUX
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
|
export interface PointsTier {
|
||||||
|
min: number;
|
||||||
|
max: number; // 0 = illimité
|
||||||
|
points: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
penalties_enabled: boolean;
|
penalties_enabled: boolean;
|
||||||
show_amende_score: boolean;
|
show_amende_score: boolean;
|
||||||
@@ -668,10 +674,8 @@ export interface AppSettings {
|
|||||||
points_categories_weed: string[];
|
points_categories_weed: string[];
|
||||||
points_categories_zipette: string[];
|
points_categories_zipette: string[];
|
||||||
points_separated: boolean;
|
points_separated: boolean;
|
||||||
points_weed_threshold_price: number;
|
points_weed_tiers: PointsTier[];
|
||||||
points_weed_per_threshold: number;
|
points_zipette_tiers: PointsTier[];
|
||||||
points_zipette_threshold_price: number;
|
|
||||||
points_zipette_per_threshold: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSettings = async (): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
|
export const getSettings = async (): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => {
|
||||||
|
|||||||
@@ -13,12 +13,127 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
import { getSettings, updateSettings, getCategories } from "../../api/api_admin";
|
import { getSettings, updateSettings, getCategories } from "../../api/api_admin";
|
||||||
import type { AppSettings, Category } from "../../api/api_admin";
|
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" | "none";
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// Composant éditeur de paliers
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
function TiersSection({
|
||||||
|
title,
|
||||||
|
tiers,
|
||||||
|
onChange,
|
||||||
|
colors,
|
||||||
|
s,
|
||||||
|
accentColor,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
tiers: PointsTier[];
|
||||||
|
onChange: (tiers: PointsTier[]) => void;
|
||||||
|
colors: any;
|
||||||
|
s: any;
|
||||||
|
accentColor: string;
|
||||||
|
}) {
|
||||||
|
const updateTier = (i: number, field: keyof PointsTier, raw: string) => {
|
||||||
|
const val = field === "points" ? parseInt(raw, 10) : parseFloat(raw);
|
||||||
|
if (isNaN(val)) return;
|
||||||
|
const next = tiers.map((t, idx) => (idx === i ? { ...t, [field]: val } : t));
|
||||||
|
onChange(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTier = () => {
|
||||||
|
onChange([...tiers, { min: 0, max: 0, points: 1 }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTier = (i: number) => {
|
||||||
|
onChange(tiers.filter((_, idx) => idx !== i));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.section}>
|
||||||
|
<Text style={s.sectionTitle}>{title}</Text>
|
||||||
|
<Text style={s.hint}>
|
||||||
|
Définissez chaque palier : de X€ à Y€ = N points.{"\n"}
|
||||||
|
Max = 0 signifie illimité (pas de borne supérieure).
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* En-tête colonnes */}
|
||||||
|
<View style={{ flexDirection: "row", paddingHorizontal: spacing.l, paddingBottom: spacing.xs, gap: spacing.xs }}>
|
||||||
|
<Text style={[s.thresholdSep, { width: 56, textAlign: "center" }]}>Min €</Text>
|
||||||
|
<Text style={[s.thresholdSep, { width: 56, textAlign: "center" }]}>Max €</Text>
|
||||||
|
<Text style={[s.thresholdSep, { width: 44, textAlign: "center" }]}>Pts</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{tiers.map((tier, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: spacing.l,
|
||||||
|
paddingVertical: spacing.s,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
gap: spacing.xs,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
style={[s.thresholdInput, { width: 56 }]}
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
value={String(tier.min)}
|
||||||
|
onChangeText={(v) => updateTier(i, "min", v)}
|
||||||
|
/>
|
||||||
|
<Text style={s.thresholdSep}>→</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[s.thresholdInput, { width: 56 }]}
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
value={String(tier.max)}
|
||||||
|
onChangeText={(v) => updateTier(i, "max", v)}
|
||||||
|
placeholder="0=∞"
|
||||||
|
placeholderTextColor={colors.textMuted}
|
||||||
|
/>
|
||||||
|
<Text style={s.thresholdSep}>=</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[s.thresholdInput, { width: 44 }]}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
value={String(tier.points)}
|
||||||
|
onChangeText={(v) => updateTier(i, "points", v)}
|
||||||
|
/>
|
||||||
|
<Text style={[s.thresholdSep, { marginRight: spacing.xs }]}>pts</Text>
|
||||||
|
<TouchableOpacity onPress={() => removeTier(i)}>
|
||||||
|
<Ionicons name="trash-outline" size={18} color={colors.danger} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{tiers.length === 0 && (
|
||||||
|
<Text style={[s.hint, { paddingTop: 0 }]}>Aucun palier défini</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={addTier}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: spacing.xs,
|
||||||
|
paddingHorizontal: spacing.l,
|
||||||
|
paddingVertical: spacing.m,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="add-circle-outline" size={18} color={accentColor} />
|
||||||
|
<Text style={{ fontSize: fontSize.sm, color: accentColor, fontWeight: "600" }}>
|
||||||
|
Ajouter un palier
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
||||||
@@ -32,10 +147,8 @@ export default function SettingsScreen() {
|
|||||||
points_categories_weed: [],
|
points_categories_weed: [],
|
||||||
points_categories_zipette: [],
|
points_categories_zipette: [],
|
||||||
points_separated: true,
|
points_separated: true,
|
||||||
points_weed_threshold_price: 30,
|
points_weed_tiers: [],
|
||||||
points_weed_per_threshold: 1,
|
points_zipette_tiers: [],
|
||||||
points_zipette_threshold_price: 30,
|
|
||||||
points_zipette_per_threshold: 1,
|
|
||||||
});
|
});
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
|
||||||
@@ -372,77 +485,22 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Barème de points */}
|
{/* Barème de points */}
|
||||||
<View style={s.section}>
|
<TiersSection
|
||||||
<Text style={s.sectionTitle}>Barème de points</Text>
|
title="Barème Weed / Hash"
|
||||||
<Text style={s.hint}>
|
tiers={settings.points_weed_tiers ?? []}
|
||||||
Pour chaque tranche de prix dépensée, le client gagne un nombre de points.{"\n"}
|
onChange={(tiers) => setSettings((p) => ({ ...p, points_weed_tiers: tiers }))}
|
||||||
Exemple : 50€ = 2 pts → 100€ = 4 pts, 150€ = 6 pts...
|
colors={colors}
|
||||||
</Text>
|
s={s}
|
||||||
|
accentColor={WEED_COLOR}
|
||||||
{/* Weed */}
|
|
||||||
<View style={[s.thresholdRow, { borderTopWidth: 0 }]}>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text style={s.thresholdLabel}>Pool Weed / Hash</Text>
|
|
||||||
<Text style={s.thresholdDesc}>Prix seuil → points gagnés</Text>
|
|
||||||
</View>
|
|
||||||
<View style={s.thresholdInputs}>
|
|
||||||
<TextInput
|
|
||||||
style={s.thresholdInput}
|
|
||||||
keyboardType="decimal-pad"
|
|
||||||
value={String(settings.points_weed_threshold_price)}
|
|
||||||
onChangeText={(v) => {
|
|
||||||
const n = parseFloat(v);
|
|
||||||
if (!isNaN(n) && n > 0)
|
|
||||||
setSettings((p) => ({ ...p, points_weed_threshold_price: n }));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Text style={s.thresholdSep}>€ =</Text>
|
<TiersSection
|
||||||
<TextInput
|
title="Barème Zipette"
|
||||||
style={s.thresholdInput}
|
tiers={settings.points_zipette_tiers ?? []}
|
||||||
keyboardType="number-pad"
|
onChange={(tiers) => setSettings((p) => ({ ...p, points_zipette_tiers: tiers }))}
|
||||||
value={String(settings.points_weed_per_threshold)}
|
colors={colors}
|
||||||
onChangeText={(v) => {
|
s={s}
|
||||||
const n = parseInt(v, 10);
|
accentColor={ZIP_COLOR}
|
||||||
if (!isNaN(n) && n >= 0)
|
|
||||||
setSettings((p) => ({ ...p, points_weed_per_threshold: n }));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Text style={s.thresholdSep}>pts</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Zipette */}
|
|
||||||
<View style={s.thresholdRow}>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text style={s.thresholdLabel}>Pool Zipette</Text>
|
|
||||||
<Text style={s.thresholdDesc}>Prix seuil → points gagnés</Text>
|
|
||||||
</View>
|
|
||||||
<View style={s.thresholdInputs}>
|
|
||||||
<TextInput
|
|
||||||
style={s.thresholdInput}
|
|
||||||
keyboardType="decimal-pad"
|
|
||||||
value={String(settings.points_zipette_threshold_price)}
|
|
||||||
onChangeText={(v) => {
|
|
||||||
const n = parseFloat(v);
|
|
||||||
if (!isNaN(n) && n > 0)
|
|
||||||
setSettings((p) => ({ ...p, points_zipette_threshold_price: n }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text style={s.thresholdSep}>€ =</Text>
|
|
||||||
<TextInput
|
|
||||||
style={s.thresholdInput}
|
|
||||||
keyboardType="number-pad"
|
|
||||||
value={String(settings.points_zipette_per_threshold)}
|
|
||||||
onChangeText={(v) => {
|
|
||||||
const n = parseInt(v, 10);
|
|
||||||
if (!isNaN(n) && n >= 0)
|
|
||||||
setSettings((p) => ({ ...p, points_zipette_per_threshold: n }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text style={s.thresholdSep}>pts</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={s.saveButton}
|
style={s.saveButton}
|
||||||
|
|||||||
Reference in New Issue
Block a user