fix: schedule status system — auto-key, config order, label lookup

- Auto-generate status key from label (admin doesn't need to set keys)
- Remove visible key field from status config editor
- Order statuses/levels in filters by config order (matches admin panel)
- Shared findStatusConfig() for robust label lookup (by key, label, or derived key)
- Custom status badges in DayCard, GroupCard, MobileSchedule
- Simplified filter logic with clsStatus helper
- Removed dead code: TIME_PRESETS, StatusFilter type
- SelectField: blur input after selection to prevent re-open
This commit is contained in:
2026-03-28 00:33:55 +03:00
parent b322c969f2
commit bdeedcfcc8
9 changed files with 102 additions and 37 deletions
+1
View File
@@ -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"
+25 -12
View File
@@ -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<ScheduleClass>(cls);
const [draft, setDraft] = useState<ScheduleClass>(() => {
// 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({
/>
<SelectField
label="Статус"
value={draft.status || (draft.recruiting ? "recruiting" : draft.hasSlots ? "hasSlots" : "")}
value={draft.status || ""}
onChange={(v) => setDraft({
...draft,
status: v || undefined,
@@ -1199,23 +1213,17 @@ function ConfigEditor({ cfg, updateCfg, onSync }: { cfg: ScheduleConfig; updateC
<ArrayEditor
items={normalized.statuses}
onChange={(statuses) => 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) => (
<div className="space-y-2">
<InputField
label="Ключ (латиницей)"
value={item.key}
onChange={(v) => update({ ...item, key: v.replace(/[^a-zA-Z0-9_]/g, "") })}
placeholder="например: intensive"
/>
<InputField
label="Название"
value={item.label}
onChange={(v) => update({ ...item, label: v })}
onChange={(v) => update({ ...item, label: v, key: v.toLowerCase().replace(/[^a-zа-яё0-9]/gi, "_").replace(/_+/g, "_") })}
placeholder="Название статуса"
/>
<InputField
@@ -1262,7 +1270,12 @@ export default function ScheduleEditorPage() {
adminFetch("/api/admin/sections/scheduleConfig")
.then((r) => r.json())
.then((c: ScheduleConfig) => { if (c?.levels) setConfig(c); })
.then((c: Partial<ScheduleConfig>) => {
setConfig({
levels: c?.levels ?? DEFAULT_CONFIG.levels,
statuses: c?.statuses ?? DEFAULT_CONFIG.statuses,
});
})
.catch(() => {});
}, []);