feat: flexible group management in schedule editor

- Group = trainer + type (time-independent)
- Edit modal shows per-day time fields (Mon 12:00, Fri 18:00)
- Calendar blocks colored by group, not class type
- Color picker for site dots moved to classes editor
- New class: single time + multi-day selector
- Edit class: per-day times, add/remove days from group
- Delete removes group from all days

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 21:31:05 +03:00
parent b5262b4adc
commit bfa59a8d18
2 changed files with 209 additions and 227 deletions

View File

@@ -9,6 +9,25 @@ const ICON_OPTIONS = [
"heart", "music", "dumbbell", "trophy", "heart", "music", "dumbbell", "trophy",
]; ];
const COLOR_SWATCHES: { value: string; bg: string }[] = [
{ value: "rose", bg: "bg-rose-500" },
{ value: "orange", bg: "bg-orange-500" },
{ value: "amber", bg: "bg-amber-500" },
{ value: "yellow", bg: "bg-yellow-400" },
{ value: "lime", bg: "bg-lime-500" },
{ value: "emerald", bg: "bg-emerald-500" },
{ value: "teal", bg: "bg-teal-500" },
{ value: "cyan", bg: "bg-cyan-500" },
{ value: "sky", bg: "bg-sky-500" },
{ value: "blue", bg: "bg-blue-500" },
{ value: "indigo", bg: "bg-indigo-500" },
{ value: "violet", bg: "bg-violet-500" },
{ value: "purple", bg: "bg-purple-500" },
{ value: "fuchsia", bg: "bg-fuchsia-500" },
{ value: "pink", bg: "bg-pink-500" },
{ value: "red", bg: "bg-red-500" },
];
interface ClassesData { interface ClassesData {
title: string; title: string;
items: { items: {
@@ -63,6 +82,31 @@ export default function ClassesEditorPage() {
</select> </select>
</div> </div>
</div> </div>
<div>
<label className="block text-sm text-neutral-400 mb-1.5">
Цвет в расписании
</label>
<div className="flex flex-wrap gap-1.5">
{COLOR_SWATCHES.map((c) => {
const isUsed = data.items.some(
(other) => other !== item && other.color === c.value
);
if (isUsed) return null;
return (
<button
key={c.value}
type="button"
onClick={() => updateItem({ ...item, color: c.value })}
className={`h-6 w-6 rounded-full ${c.bg} transition-all ${
item.color === c.value
? "ring-2 ring-white ring-offset-1 ring-offset-neutral-900 scale-110"
: "opacity-50 hover:opacity-100"
}`}
/>
);
})}
</div>
</div>
<TextareaField <TextareaField
label="Краткое описание" label="Краткое описание"
value={item.description} value={item.description}

View File

@@ -31,75 +31,48 @@ const LEVELS = [
{ value: "Продвинутый", label: "Продвинутый" }, { value: "Продвинутый", label: "Продвинутый" },
]; ];
const COLOR_SWATCHES: { value: string; bg: string }[] = [ const GROUP_PALETTE = [
{ value: "rose", bg: "bg-rose-500" }, "bg-rose-500/80 border-rose-400",
{ value: "orange", bg: "bg-orange-500" }, "bg-orange-500/80 border-orange-400",
{ value: "amber", bg: "bg-amber-500" }, "bg-amber-500/80 border-amber-400",
{ value: "yellow", bg: "bg-yellow-400" }, "bg-yellow-400/80 border-yellow-300",
{ value: "lime", bg: "bg-lime-500" }, "bg-lime-500/80 border-lime-400",
{ value: "emerald", bg: "bg-emerald-500" }, "bg-emerald-500/80 border-emerald-400",
{ value: "teal", bg: "bg-teal-500" }, "bg-teal-500/80 border-teal-400",
{ value: "cyan", bg: "bg-cyan-500" }, "bg-cyan-500/80 border-cyan-400",
{ value: "sky", bg: "bg-sky-500" }, "bg-sky-500/80 border-sky-400",
{ value: "blue", bg: "bg-blue-500" }, "bg-blue-500/80 border-blue-400",
{ value: "indigo", bg: "bg-indigo-500" }, "bg-indigo-500/80 border-indigo-400",
{ value: "violet", bg: "bg-violet-500" }, "bg-violet-500/80 border-violet-400",
{ value: "purple", bg: "bg-purple-500" }, "bg-purple-500/80 border-purple-400",
{ value: "fuchsia", bg: "bg-fuchsia-500" }, "bg-fuchsia-500/80 border-fuchsia-400",
{ value: "pink", bg: "bg-pink-500" }, "bg-pink-500/80 border-pink-400",
{ value: "red", bg: "bg-red-500" }, "bg-red-500/80 border-red-400",
]; ];
const COLOR_MAP: Record<string, string> = { /** Build a unique group key (trainer + type) */
rose: "bg-rose-500/80 border-rose-400", function groupKey(cls: ScheduleClass): string {
orange: "bg-orange-500/80 border-orange-400", return `${cls.trainer}|${cls.type}`;
amber: "bg-amber-500/80 border-amber-400",
yellow: "bg-yellow-400/80 border-yellow-300",
lime: "bg-lime-500/80 border-lime-400",
emerald: "bg-emerald-500/80 border-emerald-400",
teal: "bg-teal-500/80 border-teal-400",
cyan: "bg-cyan-500/80 border-cyan-400",
sky: "bg-sky-500/80 border-sky-400",
blue: "bg-blue-500/80 border-blue-400",
indigo: "bg-indigo-500/80 border-indigo-400",
violet: "bg-violet-500/80 border-violet-400",
purple: "bg-purple-500/80 border-purple-400",
fuchsia: "bg-fuchsia-500/80 border-fuchsia-400",
pink: "bg-pink-500/80 border-pink-400",
red: "bg-red-500/80 border-red-400",
};
const FALLBACK_PALETTE = COLOR_SWATCHES.map((c) => COLOR_MAP[c.value]);
function buildColorAssignments(classTypes: string[], classColors: Record<string, string>): Record<string, string> {
const result: Record<string, string> = {};
const usedPalette = new Set<number>();
// First pass: assign explicit colors
for (const type of classTypes) {
const c = classColors[type];
if (c && COLOR_MAP[c]) {
result[type] = COLOR_MAP[c];
const idx = COLOR_SWATCHES.findIndex((s) => s.value === c);
if (idx >= 0) usedPalette.add(idx);
}
}
// Second pass: assign remaining types to unused palette slots
let nextSlot = 0;
for (const type of classTypes) {
if (result[type]) continue;
while (usedPalette.has(nextSlot) && nextSlot < FALLBACK_PALETTE.length) nextSlot++;
result[type] = FALLBACK_PALETTE[nextSlot % FALLBACK_PALETTE.length];
usedPalette.add(nextSlot);
nextSlot++;
}
return result;
} }
function getTypeColor(type: string, assignments: Record<string, string>): string { /** Assign a color to each unique group across all days */
return assignments[type] ?? "bg-neutral-600/80 border-neutral-500"; function buildGroupColors(days: ScheduleDay[]): Record<string, string> {
const seen = new Set<string>();
const keys: string[] = [];
for (const day of days) {
for (const cls of day.classes) {
const k = groupKey(cls);
if (!seen.has(k)) {
seen.add(k);
keys.push(k);
}
}
}
const result: Record<string, string> = {};
keys.forEach((k, i) => {
result[k] = GROUP_PALETTE[i % GROUP_PALETTE.length];
});
return result;
} }
// Calendar config // Calendar config
@@ -179,7 +152,7 @@ function ClassBlock({
index, index,
isOverlapping, isOverlapping,
isDragging, isDragging,
colorAssignments, groupColors,
onClick, onClick,
onDragStart, onDragStart,
}: { }: {
@@ -187,7 +160,7 @@ function ClassBlock({
index: number; index: number;
isOverlapping: boolean; isOverlapping: boolean;
isDragging: boolean; isDragging: boolean;
colorAssignments: Record<string, string>; groupColors: Record<string, string>;
onClick: () => void; onClick: () => void;
onDragStart: (e: React.MouseEvent) => void; onDragStart: (e: React.MouseEvent) => void;
}) { }) {
@@ -199,7 +172,7 @@ function ClassBlock({
const top = minutesToY(startMin); const top = minutesToY(startMin);
const height = Math.max(((endMin - startMin) / 60) * HOUR_HEIGHT, 20); const height = Math.max(((endMin - startMin) / 60) * HOUR_HEIGHT, 20);
const colors = getTypeColor(cls.type, colorAssignments); const colors = groupColors[groupKey(cls)] ?? "bg-neutral-600/80 border-neutral-500";
return ( return (
<div <div
@@ -239,8 +212,9 @@ function ClassBlock({
// ---------- Edit Modal ---------- // ---------- Edit Modal ----------
/** Check if two classes are the same "group" (same trainer + type + time) */ /** Check if two classes are the same "group" (same trainer + type + time) */
/** Same group = same trainer + type */
function isSameGroup(a: ScheduleClass, b: ScheduleClass): boolean { function isSameGroup(a: ScheduleClass, b: ScheduleClass): boolean {
return a.trainer === b.trainer && a.type === b.type && a.time === b.time; return a.trainer === b.trainer && a.type === b.type;
} }
function ClassModal({ function ClassModal({
@@ -256,13 +230,10 @@ function ClassModal({
cls: ScheduleClass; cls: ScheduleClass;
trainers: string[]; trainers: string[];
classTypes: string[]; classTypes: string[];
/** For edit: saves to current day + manages group days. For new: creates on selected days. */ onSave: (cls: ScheduleClass, dayTimes: Record<string, string>) => void;
onSave: (cls: ScheduleClass, days: string[]) => void;
onDelete?: () => void; onDelete?: () => void;
onClose: () => void; onClose: () => void;
/** All schedule days (sorted) */
allDays: ScheduleDay[]; allDays: ScheduleDay[];
/** Current day name */
currentDay: string; currentDay: string;
}) { }) {
const [draft, setDraft] = useState<ScheduleClass>(cls); const [draft, setDraft] = useState<ScheduleClass>(cls);
@@ -270,36 +241,43 @@ function ClassModal({
const typeOptions = classTypes.map((t) => ({ value: t, label: t })); const typeOptions = classTypes.map((t) => ({ value: t, label: t }));
const isNew = !onDelete; const isNew = !onDelete;
// Find which days already have this exact group (for edit mode) // For edit mode: build per-day times from existing group entries
const groupDays = useMemo(() => { const initialDayTimes = useMemo(() => {
if (isNew) return new Set<string>(); if (isNew) return { [currentDay]: cls.time };
const days = new Set<string>(); const times: Record<string, string> = {};
for (const day of allDays) { for (const day of allDays) {
if (day.classes.some((c) => isSameGroup(c, cls))) { const match = day.classes.find((c) => isSameGroup(c, cls));
days.add(day.day); if (match) times[day.day] = match.time;
}
} }
return days; return times;
}, [allDays, cls, isNew]); }, [allDays, cls, isNew, currentDay]);
const [selectedDays, setSelectedDays] = useState<Set<string>>( const [dayTimes, setDayTimes] = useState<Record<string, string>>(initialDayTimes);
() => isNew ? new Set([currentDay]) : new Set(groupDays)
); const selectedDays = new Set(Object.keys(dayTimes));
function toggleDay(day: string) { function toggleDay(day: string) {
setSelectedDays((prev) => { setDayTimes((prev) => {
const next = new Set(prev); // Must keep at least one day
// Must have at least one day selected if (day in prev && Object.keys(prev).length <= 1) return prev;
if (next.has(day) && next.size <= 1) return next; if (day in prev) {
if (next.has(day)) next.delete(day); const next = { ...prev };
else next.add(day); delete next[day];
return next; return next;
}
// New day gets time from current day
return { ...prev, [day]: prev[currentDay] || cls.time };
}); });
} }
// Compute what changed for the hint function updateDayTime(day: string, time: string) {
const addedDays = [...selectedDays].filter((d) => !groupDays.has(d)); setDayTimes((prev) => ({ ...prev, [day]: time }));
const removedDays = [...groupDays].filter((d) => !selectedDays.has(d)); }
// Compute what changed for the hint (edit mode only)
const originalDays = new Set(Object.keys(initialDayTimes));
const addedDays = [...selectedDays].filter((d) => !originalDays.has(d));
const removedDays = [...originalDays].filter((d) => !selectedDays.has(d));
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
@@ -317,29 +295,68 @@ function ClassModal({
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{/* Day selector — always visible */} {/* Day selector */}
{allDays.length > 1 && ( {allDays.length > 1 && (
<div> <div>
<label className="block text-sm text-neutral-400 mb-2">Дни</label> <label className="block text-sm text-neutral-400 mb-2">Дни</label>
{/* Selected days with per-day time (edit mode) or simple buttons (new mode) */}
{!isNew && selectedDays.size > 0 && (
<div className="space-y-1.5 mb-2">
{allDays.filter((d) => selectedDays.has(d.day)).map((d) => (
<div key={d.day} className="flex items-center gap-2">
<button
type="button"
onClick={() => toggleDay(d.day)}
className="shrink-0 rounded-lg px-2.5 py-1 text-xs font-medium bg-gold/20 text-gold border border-gold/40 min-w-[36px]"
>
{d.dayShort}
</button>
<div className="flex-1">
<TimeRangeField
label=""
value={dayTimes[d.day] || ""}
onChange={(v) => updateDayTime(d.day, v)}
/>
</div>
</div>
))}
</div>
)}
{/* Unselected days — toggle buttons */}
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{allDays.map((d) => { {isNew
const active = selectedDays.has(d.day); ? allDays.map((d) => {
return ( const active = selectedDays.has(d.day);
<button return (
key={d.day} <button
type="button" key={d.day}
onClick={() => toggleDay(d.day)} type="button"
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${ onClick={() => toggleDay(d.day)}
active className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
? "bg-gold/20 text-gold border border-gold/40" active
: "border border-white/10 text-neutral-500 hover:text-white hover:border-white/20" ? "bg-gold/20 text-gold border border-gold/40"
}`} : "border border-white/10 text-neutral-500 hover:text-white hover:border-white/20"
> }`}
{d.dayShort} >
</button> {d.dayShort}
); </button>
})} );
})
: allDays.filter((d) => !selectedDays.has(d.day)).map((d) => (
<button
key={d.day}
type="button"
onClick={() => toggleDay(d.day)}
className="rounded-lg px-3 py-1.5 text-xs font-medium border border-white/10 text-neutral-500 hover:text-white hover:border-white/20 transition-all"
>
+ {d.dayShort}
</button>
))
}
</div> </div>
{!isNew && (addedDays.length > 0 || removedDays.length > 0) && ( {!isNew && (addedDays.length > 0 || removedDays.length > 0) && (
<div className="mt-1.5 flex flex-wrap gap-x-3 text-[11px]"> <div className="mt-1.5 flex flex-wrap gap-x-3 text-[11px]">
{addedDays.length > 0 && ( {addedDays.length > 0 && (
@@ -357,11 +374,23 @@ function ClassModal({
</div> </div>
)} )}
<TimeRangeField {/* Time — only for new class (edit mode has per-day times above) */}
label="Время" {isNew && (
value={draft.time} <TimeRangeField
onChange={(v) => setDraft({ ...draft, time: v })} label="Время"
/> value={draft.time}
onChange={(v) => {
setDraft({ ...draft, time: v });
// Update all selected day times
setDayTimes((prev) => {
const next: Record<string, string> = {};
for (const day of Object.keys(prev)) next[day] = v;
return next;
});
}}
/>
)}
<SelectField <SelectField
label="Тренер" label="Тренер"
value={draft.trainer} value={draft.trainer}
@@ -400,7 +429,7 @@ function ClassModal({
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
onSave(draft, [...selectedDays]); onSave(draft, dayTimes);
onClose(); onClose();
}} }}
className="flex-1 rounded-lg bg-gold px-4 py-2.5 text-sm font-medium text-black hover:opacity-90 transition-opacity" className="flex-1 rounded-lg bg-gold px-4 py-2.5 text-sm font-medium text-black hover:opacity-90 transition-opacity"
@@ -431,16 +460,12 @@ function CalendarGrid({
trainers, trainers,
addresses, addresses,
classTypes, classTypes,
classColors,
onColorChange,
onChange, onChange,
}: { }: {
location: ScheduleLocation; location: ScheduleLocation;
trainers: string[]; trainers: string[];
addresses: string[]; addresses: string[];
classTypes: string[]; classTypes: string[];
classColors: Record<string, string>;
onColorChange: (typeName: string, color: string) => void;
onChange: (loc: ScheduleLocation) => void; onChange: (loc: ScheduleLocation) => void;
}) { }) {
const [editingClass, setEditingClass] = useState<{ const [editingClass, setEditingClass] = useState<{
@@ -452,15 +477,14 @@ function CalendarGrid({
cls: ScheduleClass; cls: ScheduleClass;
} | null>(null); } | null>(null);
// Compute color assignments (explicit + smart fallback) // Compute group-based colors for calendar blocks
const colorAssignments = useMemo( const sortedDaysForColors = sortDaysByWeekday(location.days);
() => buildColorAssignments(classTypes, classColors), const groupColors = useMemo(
[classTypes, classColors] () => buildGroupColors(sortedDaysForColors),
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(sortedDaysForColors.map((d) => d.classes.map((c) => groupKey(c))))]
); );
// Color picker popup
const [colorPicker, setColorPicker] = useState<string | null>(null);
// Hover highlight state // Hover highlight state
const [hover, setHover] = useState<{ dayIndex: number; startMin: number } | null>(null); const [hover, setHover] = useState<{ dayIndex: number; startMin: number } | null>(null);
@@ -683,7 +707,7 @@ function CalendarGrid({
const dragPreview = drag?.moved ? (() => { const dragPreview = drag?.moved ? (() => {
const sourceDay = sortedDays[drag.sourceDayIndex]; const sourceDay = sortedDays[drag.sourceDayIndex];
const cls = sourceDay.classes[drag.classIndex]; const cls = sourceDay.classes[drag.classIndex];
const colors = getTypeColor(cls.type, colorAssignments); const colors = groupColors[groupKey(cls)] ?? "bg-neutral-600/80 border-neutral-500";
const top = minutesToY(drag.previewStartMin); const top = minutesToY(drag.previewStartMin);
const height = (drag.durationMin / 60) * HOUR_HEIGHT; const height = (drag.durationMin / 60) * HOUR_HEIGHT;
const newStart = formatMinutes(drag.previewStartMin); const newStart = formatMinutes(drag.previewStartMin);
@@ -709,61 +733,6 @@ function CalendarGrid({
/> />
</div> </div>
{/* Legend with color picker */}
<div className="flex flex-wrap gap-2">
{classTypes.map((type) => {
const colors = getTypeColor(type, colorAssignments);
const bgClass = colors.split(" ")[0] || "bg-neutral-600/80";
const isOpen = colorPicker === type;
return (
<div key={type} className="relative">
<button
type="button"
onClick={() => setColorPicker(isOpen ? null : type)}
className={`flex items-center gap-1.5 text-xs rounded-md px-2 py-1 transition-colors ${
isOpen
? "bg-white/10 text-white"
: "text-neutral-300 hover:bg-white/5 hover:text-white"
}`}
>
<div className={`h-3 w-3 rounded-sm ${bgClass}`} />
{type}
</button>
{isOpen && (() => {
// Build used set from resolved assignments (includes both explicit & fallback)
const usedColors = new Set(
Object.entries(colorAssignments)
.filter(([t]) => t !== type)
.map(([, cls]) => COLOR_SWATCHES.find((s) => COLOR_MAP[s.value] === cls)?.value)
.filter(Boolean)
);
return (
<div className="absolute top-full left-0 mt-1 z-50 rounded-lg border border-white/10 bg-neutral-800 p-2 shadow-xl">
<div className="flex gap-1.5">
{COLOR_SWATCHES.filter((c) => !usedColors.has(c.value)).map((c) => (
<button
key={c.value}
type="button"
onClick={() => {
onColorChange(type, c.value);
setColorPicker(null);
}}
className={`h-6 w-6 rounded-full ${c.bg} transition-all ${
classColors[type] === c.value
? "ring-2 ring-white ring-offset-1 ring-offset-neutral-800 scale-110"
: "opacity-60 hover:opacity-100"
}`}
/>
))}
</div>
</div>
);
})()}
</div>
);
})}
</div>
{/* Calendar */} {/* Calendar */}
{sortedDays.length > 0 && ( {sortedDays.length > 0 && (
<div className="overflow-x-auto rounded-lg border border-white/10" ref={gridRef}> <div className="overflow-x-auto rounded-lg border border-white/10" ref={gridRef}>
@@ -876,7 +845,7 @@ function CalendarGrid({
drag.classIndex === ci && drag.classIndex === ci &&
drag.moved drag.moved
} }
colorAssignments={colorAssignments} groupColors={groupColors}
onClick={() => { onClick={() => {
if (justDraggedRef.current) return; if (justDraggedRef.current) return;
setEditingClass({ dayIndex: di, classIndex: ci }); setEditingClass({ dayIndex: di, classIndex: ci });
@@ -915,17 +884,15 @@ function CalendarGrid({
classTypes={classTypes} classTypes={classTypes}
allDays={sortedDays} allDays={sortedDays}
currentDay={sortedDays[editingClass.dayIndex]?.day} currentDay={sortedDays[editingClass.dayIndex]?.day}
onSave={(updated, days) => { onSave={(updated, dayTimes) => {
const original = editingData.cls; const original = editingData.cls;
const selectedSet = new Set(days);
const updatedDays = location.days.map((d) => { const updatedDays = location.days.map((d) => {
const inSelected = selectedSet.has(d.day);
// Remove old matching group entries // Remove old matching group entries
let classes = d.classes.filter((c) => !isSameGroup(c, original)); let classes = d.classes.filter((c) => !isSameGroup(c, original));
// Add updated class to selected days // Add updated class with per-day time
if (inSelected) { if (d.day in dayTimes) {
classes = [...classes, updated]; classes = [...classes, { ...updated, time: dayTimes[d.day] }];
} }
return { ...d, classes }; return { ...d, classes };
}); });
@@ -952,11 +919,10 @@ function CalendarGrid({
classTypes={classTypes} classTypes={classTypes}
allDays={sortedDays} allDays={sortedDays}
currentDay={sortedDays[newClass.dayIndex]?.day} currentDay={sortedDays[newClass.dayIndex]?.day}
onSave={(created, days) => { onSave={(created, dayTimes) => {
const targetDayNames = new Set(days);
const updatedDays = location.days.map((d) => { const updatedDays = location.days.map((d) => {
if (targetDayNames.has(d.day)) { if (d.day in dayTimes) {
return { ...d, classes: [...d.classes, created] }; return { ...d, classes: [...d.classes, { ...created, time: dayTimes[d.day] }] };
} }
return d; return d;
}); });
@@ -975,8 +941,6 @@ export default function ScheduleEditorPage() {
const [trainers, setTrainers] = useState<string[]>([]); const [trainers, setTrainers] = useState<string[]>([]);
const [addresses, setAddresses] = useState<string[]>([]); const [addresses, setAddresses] = useState<string[]>([]);
const [classTypes, setClassTypes] = useState<string[]>([]); const [classTypes, setClassTypes] = useState<string[]>([]);
const [classColors, setClassColors] = useState<Record<string, string>>({});
const classesDataRef = useRef<{ title: string; items: { name: string; color?: string; [k: string]: unknown }[] } | null>(null);
useEffect(() => { useEffect(() => {
fetch("/api/admin/team") fetch("/api/admin/team")
@@ -995,36 +959,12 @@ export default function ScheduleEditorPage() {
fetch("/api/admin/sections/classes") fetch("/api/admin/sections/classes")
.then((r) => r.json()) .then((r) => r.json())
.then((classes: { title: string; items?: { name: string; color?: string }[] }) => { .then((classes: { items?: { name: string }[] }) => {
const items = classes.items ?? []; setClassTypes((classes.items ?? []).map((c) => c.name));
classesDataRef.current = { title: classes.title, items };
setClassTypes(items.map((c) => c.name));
const colors: Record<string, string> = {};
for (const item of items) {
if (item.color) colors[item.name] = item.color;
}
setClassColors(colors);
}) })
.catch(() => {}); .catch(() => {});
}, []); }, []);
const handleColorChange = useCallback((typeName: string, color: string) => {
setClassColors((prev) => ({ ...prev, [typeName]: color }));
// Save to classes section
const data = classesDataRef.current;
if (!data) return;
const updatedItems = data.items.map((item) =>
item.name === typeName ? { ...item, color } : item
);
classesDataRef.current = { ...data, items: updatedItems };
fetch("/api/admin/sections/classes", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...data, items: updatedItems }),
}).catch(() => {});
}, []);
return ( return (
<SectionEditor<ScheduleData> sectionKey="schedule" title="Расписание"> <SectionEditor<ScheduleData> sectionKey="schedule" title="Расписание">
{(data, update) => { {(data, update) => {
@@ -1105,8 +1045,6 @@ export default function ScheduleEditorPage() {
trainers={trainers} trainers={trainers}
addresses={addresses} addresses={addresses}
classTypes={classTypes} classTypes={classTypes}
classColors={classColors}
onColorChange={handleColorChange}
onChange={updateLocation} onChange={updateLocation}
/> />
)} )}