feat: group-based day management in schedule editor

- Auto-detect class groups (same trainer + type + time across days)
- Edit modal shows all group days pre-selected (e.g., ВТ/ПТ both lit)
- Toggle days to add/remove class from specific days
- Delete removes class from all days in the group
- New class modal lets you pick multiple days at once
- Visual hints: green +day / red −day for pending changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 20:51:20 +03:00
parent 5c23b622f9
commit b5262b4adc

View File

@@ -238,6 +238,11 @@ function ClassBlock({
}
// ---------- Edit Modal ----------
/** Check if two classes are the same "group" (same trainer + type + time) */
function isSameGroup(a: ScheduleClass, b: ScheduleClass): boolean {
return a.trainer === b.trainer && a.type === b.type && a.time === b.time;
}
function ClassModal({
cls,
trainers,
@@ -245,17 +250,56 @@ function ClassModal({
onSave,
onDelete,
onClose,
allDays,
currentDay,
}: {
cls: ScheduleClass;
trainers: string[];
classTypes: string[];
onSave: (cls: ScheduleClass) => void;
/** For edit: saves to current day + manages group days. For new: creates on selected days. */
onSave: (cls: ScheduleClass, days: string[]) => void;
onDelete?: () => void;
onClose: () => void;
/** All schedule days (sorted) */
allDays: ScheduleDay[];
/** Current day name */
currentDay: string;
}) {
const [draft, setDraft] = useState<ScheduleClass>(cls);
const trainerOptions = trainers.map((t) => ({ value: t, label: t }));
const typeOptions = classTypes.map((t) => ({ value: t, label: t }));
const isNew = !onDelete;
// Find which days already have this exact group (for edit mode)
const groupDays = useMemo(() => {
if (isNew) return new Set<string>();
const days = new Set<string>();
for (const day of allDays) {
if (day.classes.some((c) => isSameGroup(c, cls))) {
days.add(day.day);
}
}
return days;
}, [allDays, cls, isNew]);
const [selectedDays, setSelectedDays] = useState<Set<string>>(
() => isNew ? new Set([currentDay]) : new Set(groupDays)
);
function toggleDay(day: string) {
setSelectedDays((prev) => {
const next = new Set(prev);
// Must have at least one day selected
if (next.has(day) && next.size <= 1) return next;
if (next.has(day)) next.delete(day);
else next.add(day);
return next;
});
}
// Compute what changed for the hint
const addedDays = [...selectedDays].filter((d) => !groupDays.has(d));
const removedDays = [...groupDays].filter((d) => !selectedDays.has(d));
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
@@ -265,7 +309,7 @@ function ClassModal({
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-white">
{onDelete ? "Редактировать занятие" : "Новое занятие"}
{isNew ? "Новое занятие" : "Редактировать занятие"}
</h3>
<button type="button" onClick={onClose} className="text-neutral-400 hover:text-white">
<X size={20} />
@@ -273,6 +317,46 @@ function ClassModal({
</div>
<div className="space-y-4">
{/* Day selector — always visible */}
{allDays.length > 1 && (
<div>
<label className="block text-sm text-neutral-400 mb-2">Дни</label>
<div className="flex flex-wrap gap-1.5">
{allDays.map((d) => {
const active = selectedDays.has(d.day);
return (
<button
key={d.day}
type="button"
onClick={() => toggleDay(d.day)}
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
active
? "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>
);
})}
</div>
{!isNew && (addedDays.length > 0 || removedDays.length > 0) && (
<div className="mt-1.5 flex flex-wrap gap-x-3 text-[11px]">
{addedDays.length > 0 && (
<span className="text-emerald-400">
+ {addedDays.map((d) => allDays.find((ad) => ad.day === d)?.dayShort).join(", ")}
</span>
)}
{removedDays.length > 0 && (
<span className="text-red-400">
{removedDays.map((d) => allDays.find((ad) => ad.day === d)?.dayShort).join(", ")}
</span>
)}
</div>
)}
</div>
)}
<TimeRangeField
label="Время"
value={draft.time}
@@ -316,7 +400,7 @@ function ClassModal({
<button
type="button"
onClick={() => {
onSave(draft);
onSave(draft, [...selectedDays]);
onClose();
}}
className="flex-1 rounded-lg bg-gold px-4 py-2.5 text-sm font-medium text-black hover:opacity-90 transition-opacity"
@@ -829,16 +913,32 @@ function CalendarGrid({
cls={editingData.cls}
trainers={trainers}
classTypes={classTypes}
onSave={(updated) => {
const day = sortedDays[editingClass.dayIndex];
const classes = [...day.classes];
classes[editingClass.classIndex] = updated;
updateDay(editingClass.dayIndex, { ...day, classes });
allDays={sortedDays}
currentDay={sortedDays[editingClass.dayIndex]?.day}
onSave={(updated, days) => {
const original = editingData.cls;
const selectedSet = new Set(days);
const updatedDays = location.days.map((d) => {
const inSelected = selectedSet.has(d.day);
// Remove old matching group entries
let classes = d.classes.filter((c) => !isSameGroup(c, original));
// Add updated class to selected days
if (inSelected) {
classes = [...classes, updated];
}
return { ...d, classes };
});
onChange({ ...location, days: updatedDays });
}}
onDelete={() => {
const day = sortedDays[editingClass.dayIndex];
const classes = day.classes.filter((_, i) => i !== editingClass.classIndex);
updateDay(editingClass.dayIndex, { ...day, classes });
// Delete from ALL days that have this group
const original = editingData.cls;
const updatedDays = location.days.map((d) => ({
...d,
classes: d.classes.filter((c) => !isSameGroup(c, original)),
}));
onChange({ ...location, days: updatedDays });
}}
onClose={() => setEditingClass(null)}
/>
@@ -850,10 +950,17 @@ function CalendarGrid({
cls={newClass.cls}
trainers={trainers}
classTypes={classTypes}
onSave={(created) => {
const day = sortedDays[newClass.dayIndex];
const classes = [...day.classes, created];
updateDay(newClass.dayIndex, { ...day, classes });
allDays={sortedDays}
currentDay={sortedDays[newClass.dayIndex]?.day}
onSave={(created, days) => {
const targetDayNames = new Set(days);
const updatedDays = location.days.map((d) => {
if (targetDayNames.has(d.day)) {
return { ...d, classes: [...d.classes, created] };
}
return d;
});
onChange({ ...location, days: updatedDays });
}}
onClose={() => setNewClass(null)}
/>