diff --git a/src/app/admin/_components/FormField.tsx b/src/app/admin/_components/FormField.tsx index 37b6f4a..48695a4 100644 --- a/src/app/admin/_components/FormField.tsx +++ b/src/app/admin/_components/FormField.tsx @@ -251,6 +251,7 @@ export function SelectField({ onChange(opt.value); setOpen(false); setSearch(""); + inputRef.current?.blur(); }} className={`w-full px-4 py-2 text-left text-sm transition-colors hover:bg-white/5 ${ opt.value === value ? "text-gold bg-gold/5" : "text-white" diff --git a/src/app/admin/schedule/page.tsx b/src/app/admin/schedule/page.tsx index 9ff1ce9..b93a3de 100644 --- a/src/app/admin/schedule/page.tsx +++ b/src/app/admin/schedule/page.tsx @@ -50,11 +50,16 @@ function buildLevelOptions(config: ScheduleConfig) { ]; } +function statusKey(s: { key: string; label: string }): string { + // Use explicit key if set, otherwise derive from label + return s.key || s.label.toLowerCase().replace(/[^a-zа-яё0-9]/gi, "_").replace(/_+/g, "_"); +} + function buildStatusOptions(config: ScheduleConfig) { const statuses = config.statuses ?? DEFAULT_CONFIG.statuses; return [ { value: "", label: "Без статуса" }, - ...statuses.map((s) => ({ value: s.key, label: s.label })), + ...statuses.map((s) => ({ value: statusKey(s), label: s.label })), ]; } @@ -275,7 +280,16 @@ function ClassModal({ statusOptions: { value: string; label: string }[]; statusHint: string; }) { - const [draft, setDraft] = useState(cls); + const [draft, setDraft] = useState(() => { + // Migrate legacy booleans to status field + if (!cls.status && (cls.hasSlots || cls.recruiting)) { + return { + ...cls, + status: cls.hasSlots ? "hasSlots" : "recruiting", + }; + } + return cls; + }); const trainerOptions = trainers.map((t) => ({ value: t, label: t })); const typeOptions = classTypes.map((t) => ({ value: t, label: t })); const isNew = !onDelete; @@ -514,7 +528,7 @@ function ClassModal({ /> setDraft({ ...draft, status: v || undefined, @@ -1199,23 +1213,17 @@ function ConfigEditor({ cfg, updateCfg, onSync }: { cfg: ScheduleConfig; updateC updateCfg({ ...normalized, statuses })} - createItem={() => ({ key: `status_${Date.now()}`, label: "", description: "" })} + createItem={() => ({ key: "", label: "", description: "" })} label="Статусы групп" addLabel="Добавить статус" collapsible getItemTitle={(item) => item.label || "Новый статус"} renderItem={(item, _i, update) => (
- update({ ...item, key: v.replace(/[^a-zA-Z0-9_]/g, "") })} - placeholder="например: intensive" - /> update({ ...item, label: v })} + onChange={(v) => update({ ...item, label: v, key: v.toLowerCase().replace(/[^a-zа-яё0-9]/gi, "_").replace(/_+/g, "_") })} placeholder="Название статуса" /> r.json()) - .then((c: ScheduleConfig) => { if (c?.levels) setConfig(c); }) + .then((c: Partial) => { + setConfig({ + levels: c?.levels ?? DEFAULT_CONFIG.levels, + statuses: c?.statuses ?? DEFAULT_CONFIG.statuses, + }); + }) .catch(() => {}); }, []); diff --git a/src/components/sections/Schedule.tsx b/src/components/sections/Schedule.tsx index 68476e7..fb17969 100644 --- a/src/components/sections/Schedule.tsx +++ b/src/components/sections/Schedule.tsx @@ -192,19 +192,37 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe for (const cls of day.classes) { typeSet.add(cls.type); trainerSet.add(cls.trainer); - if (cls.status) statusSet.add(cls.status); - if (cls.hasSlots) statusSet.add("hasSlots"); - if (cls.recruiting) statusSet.add("recruiting"); + const clsStatus = cls.status || (cls.recruiting ? "recruiting" : cls.hasSlots ? "hasSlots" : ""); + if (clsStatus) statusSet.add(clsStatus); if (cls.level) levelSet.add(cls.level); } } + // Also include all configured statuses/levels so they appear in filters + if (scheduleConfig?.statuses) { + for (const s of scheduleConfig.statuses) if (s.key) statusSet.add(s.key); + } + if (scheduleConfig?.levels) { + for (const l of scheduleConfig.levels) if (l.value) levelSet.add(l.value); + } + // Order statuses by config order, then any extras from data + const configStatusOrder = (scheduleConfig?.statuses ?? []).map((s) => s.key).filter(Boolean); + const orderedStatuses = [ + ...configStatusOrder.filter((k) => statusSet.has(k)), + ...Array.from(statusSet).filter((k) => !configStatusOrder.includes(k)), + ]; + // Order levels by config order + const configLevelOrder = (scheduleConfig?.levels ?? []).map((l) => l.value).filter(Boolean); + const orderedLevels = [ + ...configLevelOrder.filter((v) => levelSet.has(v)), + ...Array.from(levelSet).filter((v) => !configLevelOrder.includes(v)), + ]; return { types: Array.from(typeSet).sort(), - availableStatuses: Array.from(statusSet), - levels: Array.from(levelSet).sort(), + availableStatuses: orderedStatuses, + levels: orderedLevels, trainerNames: Array.from(trainerSet).sort(), }; - }, [activeDays]); + }, [activeDays, scheduleConfig]); // Parse time range for filtering const activeTimeRange = isTimeFilterActive(filterTime) @@ -227,19 +245,17 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe .map((day) => ({ ...day, classes: day.classes.filter( - (cls) => - (filterTrainerSet.size === 0 || filterTrainerSet.has(cls.trainer)) && + (cls) => { + const clsStatus = cls.status || (cls.recruiting ? "recruiting" : cls.hasSlots ? "hasSlots" : ""); + return (filterTrainerSet.size === 0 || filterTrainerSet.has(cls.trainer)) && (filterTypes.size === 0 || filterTypes.has(cls.type)) && - (filterStatusSet.size === 0 || - (cls.status && filterStatusSet.has(cls.status as StatusTag)) || - (filterStatusSet.has("hasSlots" as StatusTag) && cls.hasSlots) || - (filterStatusSet.has("recruiting" as StatusTag) && cls.recruiting)) && + (filterStatusSet.size === 0 || (clsStatus && filterStatusSet.has(clsStatus))) && (!filterLevel || cls.level === filterLevel) && (!activeTimeRange || (() => { const m = startTimeMinutes(cls.time); return m >= activeTimeRange[0] && m < activeTimeRange[1]; - })()) - ), + })()); + }), })) .filter((day) => day.classes.length > 0); }, [activeDays, filterTrainerSet, filterTypes, filterStatusSet, filterLevel, filterTime, activeTimeRange, filterDaySet]); @@ -443,6 +459,7 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe showLocation={isAllMode} onBook={(v) => dispatch({ type: "SET_BOOKING", value: v })} trainerPhotos={trainerPhotos} + scheduleConfig={scheduleConfig} /> )} diff --git a/src/components/sections/schedule/DayCard.tsx b/src/components/sections/schedule/DayCard.tsx index b1155c9..c47391c 100644 --- a/src/components/sections/schedule/DayCard.tsx +++ b/src/components/sections/schedule/DayCard.tsx @@ -44,6 +44,11 @@ function ClassRow({ набор )} + {cls.status && cls.status !== "hasSlots" && cls.status !== "recruiting" && ( + + {cls.status} + + )}