Initial commit: JuanitoSport - Muay Thai & Yog4Fight PWA
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function AdminYoga({ tiers, members, recentAttendances }: any) {
|
||||
const router = useRouter();
|
||||
|
||||
const [tierName, setTierName] = useState("");
|
||||
const [tierSessions, setTierSessions] = useState("");
|
||||
const [tierPrice, setTierPrice] = useState("");
|
||||
const [savingTier, setSavingTier] = useState(false);
|
||||
const [tierError, setTierError] = useState("");
|
||||
|
||||
async function createTier(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!tierName || !tierSessions || !tierPrice) return;
|
||||
setSavingTier(true);
|
||||
setTierError("");
|
||||
const res = await fetch("/api/admin/yoga-tiers", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: tierName,
|
||||
numberOfSessions: Number(tierSessions),
|
||||
priceTotal: Number(tierPrice),
|
||||
}),
|
||||
});
|
||||
setSavingTier(false);
|
||||
if (res.ok) {
|
||||
setTierName("");
|
||||
setTierSessions("");
|
||||
setTierPrice("");
|
||||
router.refresh();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setTierError(data.error || "Erreur");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold">🧘 Gestion Yoga</h2>
|
||||
|
||||
{/* Tarifs dégressifs */}
|
||||
<section className="card">
|
||||
<h3 className="mb-3 font-bold">💰 Tarifs dégressifs</h3>
|
||||
<p className="mb-4 text-sm text-white/60">
|
||||
Configure les packs de séances que Juanito veut vendre. Les plus
|
||||
assidus paient moins cher par séance.
|
||||
</p>
|
||||
|
||||
<div className="mb-4 space-y-2">
|
||||
{tiers.map((t: any) => (
|
||||
<div key={t.id} className="flex items-center justify-between rounded-xl bg-white/5 px-4 py-3">
|
||||
<div>
|
||||
<p className="font-semibold">{t.name}</p>
|
||||
<p className="text-xs text-white/50">
|
||||
{t.numberOfSessions} séance{t.numberOfSessions > 1 ? "s" : ""} ·{" "}
|
||||
<span className="font-bold text-emerald-300">
|
||||
{t.priceTotal} €
|
||||
</span>{" "}
|
||||
({(t.priceTotal / t.numberOfSessions).toFixed(2)} €/séance)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => toggleTier(t.id, !t.isActive)}
|
||||
className={`rounded-xl px-3 py-1.5 text-sm ${t.isActive ? "bg-white/5 text-white/60" : "bg-emerald-600/20 text-emerald-300"}`}
|
||||
>
|
||||
{t.isActive ? "Actif" : "Inactif"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteTier(t.id)}
|
||||
className="rounded-xl bg-white/5 px-3 py-1.5 text-sm text-white/60"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{tiers.length === 0 && (
|
||||
<p className="text-sm text-white/50">Aucun tarif configuré.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={createTier} className="grid grid-cols-1 gap-2 sm:grid-cols-4">
|
||||
<input className="input sm:col-span-2" placeholder="Nom (ex: Pack 10)" value={tierName} onChange={(e) => setTierName(e.target.value)} />
|
||||
<input className="input" placeholder="Séances" type="number" value={tierSessions} onChange={(e) => setTierSessions(e.target.value)} />
|
||||
<input className="input" placeholder="Prix (€)" type="number" step="0.01" value={tierPrice} onChange={(e) => setTierPrice(e.target.value)} />
|
||||
<button type="submit" className="btn-primary sm:col-span-4" disabled={savingTier}>
|
||||
{savingTier ? "..." : "+ Ajouter ce tarif"}
|
||||
</button>
|
||||
</form>
|
||||
{tierError && <p className="mt-2 text-sm text-red-400">{tierError}</p>}
|
||||
<p className="mt-3 text-xs text-white/40">
|
||||
💡 Un pack de 10 à 70€ = 7€/séance au lieu de 9€. Les membres
|
||||
voient le tarif dégressif dans leur espace.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Compteur séances membres */}
|
||||
<section className="card">
|
||||
<h3 className="mb-3 font-bold">👥 Compteurs de séances des membres</h3>
|
||||
<div className="space-y-3">
|
||||
{members.length === 0 && (
|
||||
<p className="text-sm text-white/50">
|
||||
Aucun membre yoga. Crée-en un dans l'onglet Membres.
|
||||
</p>
|
||||
)}
|
||||
{members.map((m: any) => (
|
||||
<YogaMemberCard key={m.id} member={m} onUpdate={() => router.refresh()} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dernières présences */}
|
||||
<section className="card">
|
||||
<h3 className="mb-3 font-bold">🕐 Dernières présences enregistrées</h3>
|
||||
{recentAttendances.length === 0 && (
|
||||
<p className="text-sm text-white/50">Aucune présence enregistrée.</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{recentAttendances.map((a: any) => (
|
||||
<div key={a.id} className="flex items-center justify-between rounded-xl bg-white/5 px-3 py-2 text-sm">
|
||||
<span className="font-semibold">{a.profile.user.name}</span>
|
||||
<span className="text-xs text-white/50">
|
||||
{new Date(a.date).toLocaleDateString("fr-FR", { day: "numeric", month: "long" })}
|
||||
{a.pricePaid !== null && (
|
||||
<span className="ml-2 text-emerald-300">{a.pricePaid} €</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function toggleTier(id: string, isActive: boolean) {
|
||||
const res = await fetch(`/api/admin/yoga-tiers/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, isActive }),
|
||||
});
|
||||
if (res.ok) router.refresh();
|
||||
}
|
||||
|
||||
async function deleteTier(id: string) {
|
||||
if (!confirm("Supprimer ce tarif ?")) return;
|
||||
const res = await fetch(`/api/admin/yoga-tiers/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
if (res.ok) router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function YogaMemberCard({ member, onUpdate }: any) {
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [count, setCount] = useState("1");
|
||||
const [recordingSession, setRecordingSession] = useState(false);
|
||||
|
||||
const profile = member.yoga;
|
||||
const remaining = profile?.sessionsRemaining ?? 0;
|
||||
|
||||
async function addSessions(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/admin/yoga-members", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId: member.id, sessionsToAdd: Number(count) }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setAdding(false);
|
||||
setCount("1");
|
||||
onUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
async function recordSession() {
|
||||
if (!confirm(`Enregistrer une séance suivie pour ${member.name} ? (1 séance restante va être déduite)`)) return;
|
||||
setRecordingSession(true);
|
||||
const res = await fetch("/api/yoga-sessions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId: member.id }),
|
||||
});
|
||||
setRecordingSession(false);
|
||||
if (res.ok) onUpdate();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-white/5 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-semibold">{member.name}</p>
|
||||
<p className="text-xs text-white/50">
|
||||
<span className="font-bold text-emerald-300">{remaining}</span> séance
|
||||
{remaining > 1 ? "s" : ""} restante{remaining > 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setAdding(!adding)}
|
||||
className="rounded-xl bg-white/5 px-3 py-2 text-sm text-white/70 hover:bg-white/10"
|
||||
>
|
||||
{adding ? "✕" : "+ Séances"}
|
||||
</button>
|
||||
<button
|
||||
onClick={recordSession}
|
||||
disabled={recordingSession || remaining <= 0}
|
||||
className={`rounded-xl px-3 py-2 text-sm font-semibold ${
|
||||
remaining > 0
|
||||
? "bg-emerald-600 text-white hover:bg-emerald-700"
|
||||
: "bg-white/5 text-white/30"
|
||||
}`}
|
||||
>
|
||||
{recordingSession ? "..." : "✓ Séance suivie"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{adding && (
|
||||
<form onSubmit={addSessions} className="mt-3 flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={count}
|
||||
onChange={(e) => setCount(e.target.value)}
|
||||
className="input !w-20"
|
||||
/>
|
||||
<button type="submit" className="btn-primary flex-1 !py-2 text-sm">
|
||||
Ajouter les séances
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user