ae30be8f9d
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
133 lines
5.5 KiB
TypeScript
133 lines
5.5 KiB
TypeScript
import { Clock, User, MapPin } from "lucide-react";
|
|
import { shortAddress, findStatusConfig } from "./constants";
|
|
import { ScheduleBadge } from "@/components/ui/ScheduleBadge";
|
|
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
|
|
|
|
interface DayCardProps {
|
|
day: ScheduleDayMerged;
|
|
typeDots: Record<string, string>;
|
|
showLocation?: boolean;
|
|
filterTrainerSet: Set<string>;
|
|
toggleFilterTrainer: (trainer: string | null) => void;
|
|
filterTypes: Set<string>;
|
|
toggleFilterType: (type: string) => void;
|
|
statuses?: { key: string; label: string; description: string }[];
|
|
}
|
|
|
|
function ClassRow({
|
|
cls,
|
|
typeDots,
|
|
filterTrainerSet,
|
|
toggleFilterTrainer,
|
|
filterTypes,
|
|
toggleFilterType,
|
|
statuses,
|
|
}: {
|
|
cls: ScheduleClassWithLocation;
|
|
typeDots: Record<string, string>;
|
|
filterTrainerSet: Set<string>;
|
|
toggleFilterTrainer: (trainer: string | null) => void;
|
|
filterTypes: Set<string>;
|
|
toggleFilterType: (type: string) => void;
|
|
statuses?: { key: string; label: string; description: string }[];
|
|
}) {
|
|
return (
|
|
<div className="px-5 py-3.5">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex items-center gap-2 text-sm text-neutral-500 dark:text-white/40">
|
|
<Clock size={13} />
|
|
<span className="font-semibold">{cls.time}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
{cls.status && (() => {
|
|
const cfg = findStatusConfig(statuses, cls.status);
|
|
return <ScheduleBadge>{cfg?.label || cls.status}</ScheduleBadge>;
|
|
})()}
|
|
{cls.level && <ScheduleBadge>{cls.level}</ScheduleBadge>}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => window.dispatchEvent(new CustomEvent("openTrainerProfile", { detail: cls.trainer }))}
|
|
className="mt-1.5 flex items-center gap-2 text-sm font-medium cursor-pointer active:opacity-60 text-neutral-800 dark:text-white/80 hover:text-gold transition-colors"
|
|
>
|
|
<User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" />
|
|
{cls.trainer}
|
|
</button>
|
|
<div className="mt-2 flex items-center gap-2 flex-wrap">
|
|
<button
|
|
onClick={() => toggleFilterType(cls.type)}
|
|
className="flex items-center gap-2 cursor-pointer active:opacity-60"
|
|
>
|
|
<span className={`h-2 w-2 shrink-0 rounded-full ${typeDots[cls.type] ?? "bg-white/30"}`} />
|
|
<span className="text-xs text-neutral-500 dark:text-white/40">{cls.type}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DayCard({ day, typeDots, showLocation, filterTrainerSet, toggleFilterTrainer, filterTypes, toggleFilterType, statuses }: DayCardProps) {
|
|
// Group classes by location when showLocation is true
|
|
const locationGroups = showLocation
|
|
? Array.from(
|
|
day.classes.reduce((map, cls) => {
|
|
const loc = cls.locationName ?? "";
|
|
if (!map.has(loc)) {
|
|
map.set(loc, { address: cls.locationAddress, classes: [] });
|
|
}
|
|
map.get(loc)!.classes.push(cls);
|
|
return map;
|
|
}, new Map<string, { address?: string; classes: ScheduleClassWithLocation[] }>())
|
|
)
|
|
: null;
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-neutral-200 bg-white dark:border-white/[0.06] dark:bg-[#0a0a0a] overflow-hidden">
|
|
{/* Day header */}
|
|
<div className="border-b border-neutral-100 bg-neutral-50 px-5 py-4 dark:border-white/[0.04] dark:bg-white/[0.02]">
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gold/10 text-sm font-bold text-gold-dark dark:bg-gold/10 dark:text-gold-light">
|
|
{day.dayShort}
|
|
</span>
|
|
<span className="text-base font-semibold text-neutral-900 dark:text-white/90">
|
|
{day.day}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Classes */}
|
|
{locationGroups ? (
|
|
// Split by location
|
|
<div>
|
|
{locationGroups.map(([locName, { address, classes }], gi) => (
|
|
<div key={locName}>
|
|
{/* Location sub-header */}
|
|
<div className={`flex items-center gap-1.5 px-5 py-2 bg-gold/10 ${gi > 0 ? "border-t border-gold/10" : ""}`}>
|
|
<MapPin size={11} className="shrink-0 text-gold" />
|
|
<span className="text-[11px] font-medium text-white">
|
|
{locName}
|
|
{address && shortAddress(address) !== locName && (
|
|
<span className="text-white/50"> · {shortAddress(address)}</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
|
|
{classes.map((cls, i) => (
|
|
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainerSet={filterTrainerSet} toggleFilterTrainer={toggleFilterTrainer} filterTypes={filterTypes} toggleFilterType={toggleFilterType} statuses={statuses} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
// Single location — no sub-headers
|
|
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
|
|
{day.classes.map((cls, i) => (
|
|
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainerSet={filterTrainerSet} toggleFilterTrainer={toggleFilterTrainer} filterTypes={filterTypes} toggleFilterType={toggleFilterType} statuses={statuses} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|