fix: schedule status labels, Open Day halls, unsaved data guards

Schedule:
- Status badges use admin config labels (not hardcoded text) everywhere
- DayCard: level badge moved next to status badge
- Single location: hide "Все студии" tab, auto-select the only hall
- Group view: hide per-card address when all share same location
- Filter tooltip z-index fixed (above dropdowns)
- Trainer bio: status labels from config, not raw keys

Open Day:
- Hall name + address shown in schedule grid headers
- Only one class card editable at a time (edit/create mutually exclusive)
- Bigger action buttons (cancel/delete) on class cards
- Create as empty draft (not pre-filled with published status)
- Fix discount threshold input (allow delete to empty)
- Skip auto-save during partial date input

Admin:
- SectionEditor: unsaved data guard (force-save before navigation)
- Open Day + Team: same navigation guards
- Contact: removed working hours field
- TimeRangeField: allow end time hour changes
- Schedule cards: visible borders, 90min default duration
- Trainer bio: RichTextarea for description
- Open Day: RichTextarea for description
This commit is contained in:
2026-03-30 22:57:36 +03:00
parent 06be6b48ce
commit ae30be8f9d
19 changed files with 286 additions and 129 deletions
+31 -1
View File
@@ -32,11 +32,12 @@ export default function TeamEditorPage() {
// Auto-save section title with debounce (skip initial load)
const titleChangeCount = useRef(0);
const pendingSaveRef = useRef(false);
useEffect(() => {
if (!titleLoadedRef.current) return;
titleChangeCount.current++;
// Skip the first change (initial load setting the value)
if (titleChangeCount.current <= 1) return;
pendingSaveRef.current = true;
if (titleTimerRef.current) clearTimeout(titleTimerRef.current);
titleTimerRef.current = setTimeout(async () => {
const res = await adminFetch("/api/admin/sections/team", {
@@ -45,11 +46,40 @@ export default function TeamEditorPage() {
body: JSON.stringify({ title: sectionTitle }),
});
setSaveStatus(res.ok ? "saved" : "error");
if (res.ok) pendingSaveRef.current = false;
setTimeout(() => setSaveStatus("idle"), 2000);
}, 800);
return () => { if (titleTimerRef.current) clearTimeout(titleTimerRef.current); };
}, [sectionTitle]);
// Warn before leaving with unsaved title
useEffect(() => {
function onBeforeUnload(e: BeforeUnloadEvent) {
if (pendingSaveRef.current) e.preventDefault();
}
function onLinkClick(e: MouseEvent) {
if (!pendingSaveRef.current) return;
const link = (e.target as HTMLElement).closest("a");
if (!link || link.target === "_blank") return;
const href = link.getAttribute("href");
if (!href || href.startsWith("#") || href.startsWith("/admin/team/")) return;
e.preventDefault();
e.stopPropagation();
if (titleTimerRef.current) clearTimeout(titleTimerRef.current);
adminFetch("/api/admin/sections/team", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: sectionTitle }),
}).finally(() => { window.location.href = href; });
}
window.addEventListener("beforeunload", onBeforeUnload);
document.addEventListener("click", onLinkClick, true);
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
document.removeEventListener("click", onLinkClick, true);
};
}, [sectionTitle]);
const saveOrder = useCallback(async (updated: Member[]) => {
setMembers(updated);
const res = await adminFetch("/api/admin/team/reorder", {