feat: min/max participants — shared ParticipantLimits component
- New ParticipantLimits component in FormField.tsx (reusable) - Used in both Open Day settings and MC editor — identical layout - Open Day: event-level min/max (DB migration 15) - MC: per-event min/max (JSON fields) - Public: waiting list when full, spots counter, amber success modal
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
Plus, X, Loader2, Calendar, Trash2, Ban, CheckCircle2, RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { adminFetch } from "@/lib/csrf";
|
||||
import { ParticipantLimits } from "../_components/FormField";
|
||||
|
||||
// --- Types ---
|
||||
|
||||
@@ -17,6 +18,7 @@ interface OpenDayEvent {
|
||||
discountPrice: number;
|
||||
discountThreshold: number;
|
||||
minBookings: number;
|
||||
maxParticipants: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
@@ -128,18 +130,12 @@ function EventSettings({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm text-neutral-400 mb-1.5">Мин. записей на занятие</label>
|
||||
<input
|
||||
type="number"
|
||||
value={event.minBookings}
|
||||
onChange={(e) => onChange({ minBookings: parseInt(e.target.value) || 1 })}
|
||||
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-white outline-none focus:border-gold transition-colors"
|
||||
/>
|
||||
<p className="text-[10px] text-neutral-600 mt-1">Если записей меньше — занятие можно отменить</p>
|
||||
</div>
|
||||
</div>
|
||||
<ParticipantLimits
|
||||
min={event.minBookings}
|
||||
max={event.maxParticipants ?? 0}
|
||||
onMinChange={(v) => onChange({ minBookings: v })}
|
||||
onMaxChange={(v) => onChange({ maxParticipants: v })}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
@@ -185,13 +181,12 @@ function ClassCell({
|
||||
const [trainer, setTrainer] = useState(cls.trainer);
|
||||
const [style, setStyle] = useState(cls.style);
|
||||
const [endTime, setEndTime] = useState(cls.endTime);
|
||||
const [maxPart, setMaxPart] = useState(cls.maxParticipants);
|
||||
|
||||
const atRisk = cls.bookingCount < minBookings && !cls.cancelled;
|
||||
|
||||
function save() {
|
||||
if (trainer.trim() && style.trim()) {
|
||||
onUpdate(cls.id, { trainer: trainer.trim(), style: style.trim(), endTime, maxParticipants: maxPart });
|
||||
onUpdate(cls.id, { trainer: trainer.trim(), style: style.trim(), endTime });
|
||||
setEditing(false);
|
||||
}
|
||||
}
|
||||
@@ -209,15 +204,9 @@ function ClassCell({
|
||||
<option value="">Тренер...</option>
|
||||
{trainers.map((t) => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] text-neutral-500 mb-0.5 block">До</label>
|
||||
<input type="time" value={endTime} onChange={(e) => setEndTime(e.target.value)} className={selectCls} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] text-neutral-500 mb-0.5 block">Макс. чел.</label>
|
||||
<input type="number" min={0} value={maxPart} onChange={(e) => setMaxPart(parseInt(e.target.value) || 0)} placeholder="0 = без лимита" className={selectCls} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-neutral-500 mb-0.5 block">До</label>
|
||||
<input type="time" value={endTime} onChange={(e) => setEndTime(e.target.value)} className={selectCls} />
|
||||
</div>
|
||||
<div className="flex gap-1 justify-end">
|
||||
<button onClick={() => setEditing(false)} className="text-[10px] text-neutral-500 hover:text-white px-1">
|
||||
@@ -255,7 +244,7 @@ function ClassCell({
|
||||
? "text-red-400"
|
||||
: "text-emerald-400"
|
||||
}`}>
|
||||
{cls.bookingCount}{cls.maxParticipants > 0 ? `/${cls.maxParticipants}` : ""} чел.
|
||||
{cls.bookingCount} чел.
|
||||
</span>
|
||||
{atRisk && !cls.cancelled && (
|
||||
<span className="text-[9px] text-red-400">мин. {minBookings}</span>
|
||||
@@ -500,6 +489,7 @@ export default function OpenDayAdminPage() {
|
||||
discountPrice: 20,
|
||||
discountThreshold: 3,
|
||||
minBookings: 4,
|
||||
maxParticipants: 0,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user