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 ----------
|
||||
/** 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)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user