Move cell/range toggle near arrows, add period picker on date button
- Replaced static "Сегодня" button with a dropdown picker (▾) showing current start date and visible days count - Picker lets user choose any start date + days count (14/30/45/60д) - "Сегодня" quick-reset inside picker + "Показать" confirms navigation - Arrows now step by visibleDays instead of fixed 14 - Moved By-cell / By-range toggle into the left nav group (next to arrows) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useRef, useCallback, useMemo } from 'react'
|
import { useState, useRef, useCallback, useMemo, useEffect } from 'react'
|
||||||
import { addDays, format, parseISO, isWithinInterval, startOfDay, getDay, isSameDay } from 'date-fns'
|
import { addDays, format, parseISO, isWithinInterval, startOfDay, getDay, isSameDay } from 'date-fns'
|
||||||
import { ru } from 'date-fns/locale'
|
import { ru } from 'date-fns/locale'
|
||||||
import {
|
import {
|
||||||
ChevronLeft, ChevronRight, X, Check, CalendarDays,
|
ChevronLeft, ChevronRight, X, Check, CalendarDays,
|
||||||
Plus, Pencil, Trash2, AlertCircle, RefreshCw, MousePointer2, Rows3,
|
Plus, Pencil, Trash2, AlertCircle, RefreshCw, MousePointer2, Rows3, ChevronDown,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '../lib/utils'
|
import { cn } from '../lib/utils'
|
||||||
import {
|
import {
|
||||||
@@ -640,11 +640,26 @@ export function AvailabilityPage() {
|
|||||||
const [periodModal, setPeriodModal] = useState<RatePeriod | null | 'new'>(null)
|
const [periodModal, setPeriodModal] = useState<RatePeriod | null | 'new'>(null)
|
||||||
const [cellEditMode, setCellEditMode] = useState(false)
|
const [cellEditMode, setCellEditMode] = useState(false)
|
||||||
const [selectedCat, setSelectedCat] = useState<string | null>(null)
|
const [selectedCat, setSelectedCat] = useState<string | null>(null)
|
||||||
|
const [visibleDays, setVisibleDays] = useState(45)
|
||||||
|
const [showNavPicker, setShowNavPicker] = useState(false)
|
||||||
|
const [pickerDate, setPickerDate] = useState(format(new Date(), DATE_FMT))
|
||||||
|
const navPickerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showNavPicker) return
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (navPickerRef.current && !navPickerRef.current.contains(e.target as Node)) {
|
||||||
|
setShowNavPicker(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handler)
|
||||||
|
return () => document.removeEventListener('mousedown', handler)
|
||||||
|
}, [showNavPicker])
|
||||||
|
|
||||||
const dates = useMemo(() =>
|
const dates = useMemo(() =>
|
||||||
Array.from({ length: DAYS }, (_, i) =>
|
Array.from({ length: visibleDays }, (_, i) =>
|
||||||
format(addDays(today, offset + i), DATE_FMT),
|
format(addDays(today, offset + i), DATE_FMT),
|
||||||
), [today, offset])
|
), [today, offset, visibleDays])
|
||||||
|
|
||||||
// ── Mouse handlers ──
|
// ── Mouse handlers ──
|
||||||
const handleCellDown = (date: string, catId: string) => {
|
const handleCellDown = (date: string, catId: string) => {
|
||||||
@@ -741,6 +756,7 @@ export function AvailabilityPage() {
|
|||||||
{/* ── Top bar ── */}
|
{/* ── Top bar ── */}
|
||||||
<div className="shrink-0 px-5 py-3 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between gap-4 flex-wrap">
|
<div className="shrink-0 px-5 py-3 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between gap-4 flex-wrap">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Tabs */}
|
||||||
<div className="flex bg-slate-100 dark:bg-slate-700 rounded-lg p-0.5">
|
<div className="flex bg-slate-100 dark:bg-slate-700 rounded-lg p-0.5">
|
||||||
{([['grid', 'Сетка цен'], ['periods', 'Периоды']] as const).map(([t, label]) => (
|
{([['grid', 'Сетка цен'], ['periods', 'Периоды']] as const).map(([t, label]) => (
|
||||||
<button
|
<button
|
||||||
@@ -760,18 +776,114 @@ export function AvailabilityPage() {
|
|||||||
|
|
||||||
{tab === 'grid' && (
|
{tab === 'grid' && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => setOffset(o => o - 14)} className="btn-ghost p-1.5">
|
<button onClick={() => setOffset(o => o - visibleDays)} className="btn-ghost p-1.5">
|
||||||
<ChevronLeft size={16} />
|
<ChevronLeft size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setOffset(0)}
|
{/* Date/period picker trigger */}
|
||||||
className={cn('btn-ghost text-xs px-2 py-1', offset === 0 && 'text-brand-600 font-semibold')}
|
<div className="relative" ref={navPickerRef}>
|
||||||
>
|
<button
|
||||||
Сегодня
|
onClick={() => setShowNavPicker(v => !v)}
|
||||||
</button>
|
className={cn(
|
||||||
<button onClick={() => setOffset(o => o + 14)} className="btn-ghost p-1.5">
|
'btn-ghost flex items-center gap-1 text-xs px-2.5 py-1.5 rounded-lg',
|
||||||
|
showNavPicker && 'bg-slate-100 dark:bg-slate-700',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarDays size={13} className={offset === 0 ? 'text-brand-600' : 'text-slate-400'} />
|
||||||
|
<span className={cn('font-medium', offset === 0 ? 'text-brand-600' : 'text-slate-600 dark:text-slate-300')}>
|
||||||
|
{offset === 0
|
||||||
|
? 'Сегодня'
|
||||||
|
: format(addDays(today, offset), 'd MMM', { locale: ru })}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-400">·</span>
|
||||||
|
<span className="text-slate-400">{visibleDays}д</span>
|
||||||
|
<ChevronDown size={12} className="text-slate-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showNavPicker && (
|
||||||
|
<div className="absolute top-full left-0 mt-1 z-50 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 shadow-lg p-4 w-64">
|
||||||
|
{/* Start date */}
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
||||||
|
Начало периода
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 mb-3">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={pickerDate}
|
||||||
|
onChange={e => setPickerDate(e.target.value)}
|
||||||
|
className="input text-sm py-1.5 flex-1"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setPickerDate(format(today, DATE_FMT))
|
||||||
|
setOffset(0)
|
||||||
|
}}
|
||||||
|
className="btn-secondary text-xs px-2.5 py-1.5 shrink-0"
|
||||||
|
>
|
||||||
|
Сегодня
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Days count */}
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
||||||
|
Количество дней
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-1.5 flex-wrap mb-4">
|
||||||
|
{[14, 30, 45, 60].map(d => (
|
||||||
|
<button
|
||||||
|
key={d}
|
||||||
|
onClick={() => setVisibleDays(d)}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1 rounded-lg text-xs font-medium border transition-colors',
|
||||||
|
visibleDays === d
|
||||||
|
? 'bg-brand-600 border-brand-600 text-white'
|
||||||
|
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:border-brand-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{d}д
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const diff = Math.round(
|
||||||
|
(parseISO(pickerDate).getTime() - startOfDay(today).getTime()) / 86400000
|
||||||
|
)
|
||||||
|
setOffset(diff)
|
||||||
|
setShowNavPicker(false)
|
||||||
|
}}
|
||||||
|
className="btn-primary w-full justify-center text-sm py-1.5"
|
||||||
|
>
|
||||||
|
Показать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={() => setOffset(o => o + visibleDays)} className="btn-ghost p-1.5">
|
||||||
<ChevronRight size={16} />
|
<ChevronRight size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Cell / range mode toggle */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCellEditMode(v => !v)
|
||||||
|
setSelectedCat(null)
|
||||||
|
setSelection(null)
|
||||||
|
setShowEditPanel(false)
|
||||||
|
}}
|
||||||
|
title={cellEditMode ? 'Режим ячейки: редактирует только выбранную категорию' : 'Режим диапазона: редактирует все категории'}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-colors border',
|
||||||
|
cellEditMode
|
||||||
|
? 'bg-brand-50 dark:bg-brand-900/30 border-brand-300 dark:border-brand-700 text-brand-700 dark:text-brand-300'
|
||||||
|
: 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-500 hover:text-slate-700 dark:hover:text-slate-300',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cellEditMode ? <MousePointer2 size={13} /> : <Rows3 size={13} />}
|
||||||
|
{cellEditMode ? 'По ячейке' : 'По диапазону'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -798,27 +910,6 @@ export function AvailabilityPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'grid' && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setCellEditMode(v => !v)
|
|
||||||
setSelectedCat(null)
|
|
||||||
setSelection(null)
|
|
||||||
setShowEditPanel(false)
|
|
||||||
}}
|
|
||||||
title={cellEditMode ? 'Режим ячейки включён — редактирует только выбранную категорию' : 'Режим диапазона — редактирует все категории'}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors border',
|
|
||||||
cellEditMode
|
|
||||||
? 'bg-brand-50 dark:bg-brand-900/30 border-brand-300 dark:border-brand-700 text-brand-700 dark:text-brand-300'
|
|
||||||
: 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-500 hover:text-slate-700 dark:hover:text-slate-300',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{cellEditMode ? <MousePointer2 size={13} /> : <Rows3 size={13} />}
|
|
||||||
{cellEditMode ? 'По ячейке' : 'По диапазону'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === 'periods' && (
|
{tab === 'periods' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setPeriodModal('new')}
|
onClick={() => setPeriodModal('new')}
|
||||||
|
|||||||
Reference in New Issue
Block a user