241 lines
8.7 KiB
TypeScript
241 lines
8.7 KiB
TypeScript
"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>
|
|
);
|
|
} |