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:
@@ -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()),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { NavLink } from "@/types";
|
||||
|
||||
export const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
export const BRAND = {
|
||||
name: "BLACK HEART DANCE HOUSE",
|
||||
shortName: "Blackheart",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import type { SiteContent, TeamMember, RichListItem, VictoryItem } from "@/types/content";
|
||||
import { MS_PER_DAY } from "@/lib/constants";
|
||||
|
||||
const DB_PATH =
|
||||
process.env.DATABASE_PATH ||
|
||||
@@ -728,7 +729,7 @@ export function setGroupBookingStatus(
|
||||
if (status === "confirmed" && confirmation) {
|
||||
// Auto-set reminder to 'coming' only if confirmed for today/tomorrow
|
||||
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];
|
||||
const reminderStatus = (confirmation.date === today || confirmation.date === tomorrow) ? "coming" : null;
|
||||
db.prepare(
|
||||
"UPDATE group_bookings SET status = ?, confirmed_date = ?, confirmed_group = ?, confirmed_comment = ?, notified_confirm = 1, reminder_status = ? WHERE id = ?"
|
||||
|
||||
Reference in New Issue
Block a user