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:
@@ -238,6 +238,11 @@ function ClassBlock({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Edit Modal ----------
|
// ---------- 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({
|
function ClassModal({
|
||||||
cls,
|
cls,
|
||||||
trainers,
|
trainers,
|
||||||
@@ -245,17 +250,56 @@ function ClassModal({
|
|||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
onDelete,
|
||||||
onClose,
|
onClose,
|
||||||
|
allDays,
|
||||||
|
currentDay,
|
||||||
}: {
|
}: {
|
||||||
cls: ScheduleClass;
|
cls: ScheduleClass;
|
||||||
trainers: string[];
|
trainers: string[];
|
||||||
classTypes: 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;
|
onDelete?: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** All schedule days (sorted) */
|
||||||
|
allDays: ScheduleDay[];
|
||||||
|
/** Current day name */
|
||||||
|
currentDay: string;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState<ScheduleClass>(cls);
|
const [draft, setDraft] = useState<ScheduleClass>(cls);
|
||||||
const trainerOptions = trainers.map((t) => ({ value: t, label: t }));
|
const trainerOptions = trainers.map((t) => ({ value: t, label: t }));
|
||||||
const typeOptions = classTypes.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 (
|
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}>
|
||||||
@@ -265,7 +309,7 @@ function ClassModal({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-bold text-white">
|
<h3 className="text-lg font-bold text-white">
|
||||||
{onDelete ? "Редактировать занятие" : "Новое занятие"}
|
{isNew ? "Новое занятие" : "Редактировать занятие"}
|
||||||
</h3>
|
</h3>
|
||||||
<button type="button" onClick={onClose} className="text-neutral-400 hover:text-white">
|
<button type="button" onClick={onClose} className="text-neutral-400 hover:text-white">
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
@@ -273,6 +317,46 @@ function ClassModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<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
|
<TimeRangeField
|
||||||
label="Время"
|
label="Время"
|
||||||
value={draft.time}
|
value={draft.time}
|
||||||
@@ -316,7 +400,7 @@ function ClassModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSave(draft);
|
onSave(draft, [...selectedDays]);
|
||||||
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"
|
||||||
@@ -829,16 +913,32 @@ function CalendarGrid({
|
|||||||
cls={editingData.cls}
|
cls={editingData.cls}
|
||||||
trainers={trainers}
|
trainers={trainers}
|
||||||
classTypes={classTypes}
|
classTypes={classTypes}
|
||||||
onSave={(updated) => {
|
allDays={sortedDays}
|
||||||
const day = sortedDays[editingClass.dayIndex];
|
currentDay={sortedDays[editingClass.dayIndex]?.day}
|
||||||
const classes = [...day.classes];
|
onSave={(updated, days) => {
|
||||||
classes[editingClass.classIndex] = updated;
|
const original = editingData.cls;
|
||||||
updateDay(editingClass.dayIndex, { ...day, classes });
|
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={() => {
|
onDelete={() => {
|
||||||
const day = sortedDays[editingClass.dayIndex];
|
// Delete from ALL days that have this group
|
||||||
const classes = day.classes.filter((_, i) => i !== editingClass.classIndex);
|
const original = editingData.cls;
|
||||||
updateDay(editingClass.dayIndex, { ...day, classes });
|
const updatedDays = location.days.map((d) => ({
|
||||||
|
...d,
|
||||||
|
classes: d.classes.filter((c) => !isSameGroup(c, original)),
|
||||||
|
}));
|
||||||
|
onChange({ ...location, days: updatedDays });
|
||||||
}}
|
}}
|
||||||
onClose={() => setEditingClass(null)}
|
onClose={() => setEditingClass(null)}
|
||||||
/>
|
/>
|
||||||
@@ -850,10 +950,17 @@ function CalendarGrid({
|
|||||||
cls={newClass.cls}
|
cls={newClass.cls}
|
||||||
trainers={trainers}
|
trainers={trainers}
|
||||||
classTypes={classTypes}
|
classTypes={classTypes}
|
||||||
onSave={(created) => {
|
allDays={sortedDays}
|
||||||
const day = sortedDays[newClass.dayIndex];
|
currentDay={sortedDays[newClass.dayIndex]?.day}
|
||||||
const classes = [...day.classes, created];
|
onSave={(created, days) => {
|
||||||
updateDay(newClass.dayIndex, { ...day, classes });
|
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)}
|
onClose={() => setNewClass(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user