fix: date validation, pre-fill on re-edit, MS_PER_DAY constant

- Extract MS_PER_DAY constant to lib/constants.ts
- ConfirmModal date: max=1 year, rejects past dates and malformed years
- ConfirmModal pre-fills existing date + group when re-editing (✎)
- Confirmed date display handles malformed dates gracefully
- Red border + error for invalid dates, submit disabled
This commit is contained in:
2026-03-24 18:26:28 +03:00
parent f6d0491ca5
commit 745d72f36d
3 changed files with 34 additions and 13 deletions

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import { Phone, Instagram, Send, ChevronDown, ChevronRight, Bell, CheckCircle2, XCircle, Clock, Star, Calendar, DoorOpen, X, Plus } from "lucide-react";
import { adminFetch } from "@/lib/csrf";
import { MS_PER_DAY } from "@/lib/constants";
import { type BookingStatus, type BookingFilter, type SearchResult, BOOKING_STATUSES, SHORT_DAYS, fmtDate } from "./types";
import { LoadingSpinner, ContactLinks, BookingCard, StatusBadge, StatusActions, DeleteBtn } from "./BookingComponents";
import { GenericBookingsList } from "./GenericBookingsList";
@@ -40,6 +41,8 @@ function ConfirmModal({
open,
bookingName,
groupInfo,
existingDate,
existingGroup,
allClasses,
onConfirm,
onClose,
@@ -47,6 +50,8 @@ function ConfirmModal({
open: boolean;
bookingName: string;
groupInfo?: string;
existingDate?: string;
existingGroup?: string;
allClasses: ScheduleClassInfo[];
onConfirm: (data: { group: string; date: string; comment?: string }) => void;
onClose: () => void;
@@ -59,10 +64,12 @@ function ConfirmModal({
useEffect(() => {
if (!open) return;
setDate(""); setComment("");
// Try to match groupInfo against schedule to pre-fill
if (groupInfo && allClasses.length > 0) {
const info = groupInfo.toLowerCase();
const tomorrow = new Date(Date.now() + MS_PER_DAY).toISOString().split("T")[0];
setDate(existingDate && existingDate.length === 10 ? existingDate : tomorrow); setComment("");
// Try to match groupInfo or existingGroup against schedule to pre-fill
const matchText = existingGroup || groupInfo;
if (matchText && allClasses.length > 0) {
const info = matchText.toLowerCase();
// Score each class against groupInfo, pick best match
let bestMatch: ScheduleClassInfo | null = null;
let bestScore = 0;
@@ -86,7 +93,7 @@ function ConfirmModal({
}
}
setHall(""); setTrainer(""); setGroup("");
}, [open, groupInfo, allClasses]);
}, [open, groupInfo, existingDate, existingGroup, allClasses]);
// Cascading options
const halls = useMemo(() => [...new Set(allClasses.map((c) => c.hall))], [allClasses]);
@@ -132,7 +139,8 @@ function ConfirmModal({
useEffect(() => { if (!open) initRef.current = false; }, [open]);
// #11: Keyboard submit
const canSubmit = group && date;
const today = open ? new Date().toISOString().split("T")[0] : "";
const canSubmit = group && date && date.length === 10 && date >= today;
const handleSubmit = useCallback(() => {
if (canSubmit) {
const groupLabel = groups.find((g) => g.value === group)?.label || group;
@@ -152,7 +160,6 @@ function ConfirmModal({
if (!open) return null;
const today = new Date().toISOString().split("T")[0];
const selectClass = "w-full rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-sm text-white outline-none focus:border-gold/40 [color-scheme:dark] disabled:opacity-30 disabled:cursor-not-allowed";
return createPortal(
@@ -194,10 +201,14 @@ function ConfirmModal({
type="date"
value={date}
min={today}
max={new Date(Date.now() + MS_PER_DAY * 365).toISOString().split("T")[0]}
disabled={!group}
onChange={(e) => setDate(e.target.value)}
className={selectClass}
className={`${selectClass} ${date && (date < today || date.length !== 10) ? "!border-red-500/50" : ""}`}
/>
{date && (date < today || date.length !== 10) && (
<p className="text-[10px] text-red-400 mt-1">{date < today ? "Дата не может быть в прошлом" : "Неверный формат даты"}</p>
)}
</div>
<div>
<label className="text-[11px] font-medium text-neutral-400 mb-1 block">Комментарий <span className="text-neutral-600">(необязательно)</span></label>
@@ -303,10 +314,15 @@ function GroupBookingsTab({ filter, onDataChange }: { filter: BookingFilter; onD
<>
{b.groupInfo && <span className="text-xs text-neutral-400 bg-neutral-800 rounded-full px-2 py-0.5">{b.groupInfo}</span>}
{(b.confirmedGroup || b.confirmedDate) && (
<span className="text-[10px] text-emerald-400/70">
<button
onClick={(e) => { e.stopPropagation(); setConfirmingId(b.id); }}
className="text-[10px] text-emerald-400/70 hover:text-emerald-300 transition-colors cursor-pointer"
title="Изменить"
>
{b.confirmedGroup}
{b.confirmedDate && ` · ${new Date(b.confirmedDate + "T12:00").toLocaleDateString("ru-RU", { day: "numeric", month: "short" })}`}
</span>
{b.confirmedDate && b.confirmedDate.length === 10 && ` · ${new Date(b.confirmedDate + "T12:00").toLocaleDateString("ru-RU", { day: "numeric", month: "short" })}`}
{" ✎"}
</button>
)}
</>
)}
@@ -316,6 +332,8 @@ function GroupBookingsTab({ filter, onDataChange }: { filter: BookingFilter; onD
open={confirmingId !== null}
bookingName={confirmingBooking?.name ?? ""}
groupInfo={confirmingBooking?.groupInfo}
existingDate={confirmingBooking?.confirmedDate}
existingGroup={confirmingBooking?.confirmedGroup}
allClasses={allClasses}
onClose={() => setConfirmingId(null)}
onConfirm={handleConfirm}
@@ -572,7 +590,7 @@ function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
useEffect(() => {
const today = new Date().toISOString().split("T")[0];
const tomorrow = new Date(Date.now() + 86400000).toISOString().split("T")[0];
const tomorrow = new Date(Date.now() + MS_PER_DAY).toISOString().split("T")[0];
Promise.all([
adminFetch("/api/admin/group-bookings").then((r) => r.json()),