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:
2026-03-11 14:58:29 +03:00
parent 01449dcdfa
commit 4e615359eb

View File

@@ -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')}