Add calendar compact mode, Tetris login animation, maintenance rental support
- BookingCalendar: sort rooms by category, compact row height toggle - LoginPage: Tetris tile animation after delay (module names fall & stack) - MaintenancePage: rental objects support with filter tab and icons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,17 @@ import { useState } from 'react'
|
||||
import {
|
||||
Plus, Edit2, Trash2, Wrench, AlertTriangle, CheckCircle2,
|
||||
Clock, BedDouble, Dumbbell, Waves, UtensilsCrossed, Wifi,
|
||||
Car, Sparkles, Package, X,
|
||||
Car, Sparkles, Package, X, CalendarClock,
|
||||
} from 'lucide-react'
|
||||
import { Modal } from '../components/ui/Modal'
|
||||
import { cn } from '../lib/utils'
|
||||
import { format } from 'date-fns'
|
||||
import { ru } from 'date-fns/locale'
|
||||
import { RENTAL_OBJECTS } from '../data/rentalData'
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type MaintenanceTarget = 'room' | 'service'
|
||||
type MaintenanceTarget = 'room' | 'service' | 'rental'
|
||||
type MaintenanceStatus = 'scheduled' | 'in_progress' | 'done' | 'cancelled'
|
||||
|
||||
interface MaintenanceRecord {
|
||||
@@ -118,6 +119,32 @@ const MOCK_RECORDS: MaintenanceRecord[] = [
|
||||
notes: 'Полный ремонт: полы, потолки, мебель',
|
||||
status: 'scheduled',
|
||||
},
|
||||
{
|
||||
id: 'm-6',
|
||||
type: 'rental',
|
||||
targetId: 'sauna',
|
||||
targetLabel: '🛁 Баня',
|
||||
startDate: '2026-03-16',
|
||||
endDate: '2026-03-16',
|
||||
startTime: '14:00',
|
||||
endTime: '16:00',
|
||||
reason: 'Подготовка между гостями — проветривание и уборка',
|
||||
notes: 'Плановый тех. перерыв 2 часа после каждого заезда. Температурное охлаждение + мытьё полков.',
|
||||
status: 'scheduled',
|
||||
assignedTo: 'Горничная',
|
||||
},
|
||||
{
|
||||
id: 'm-7',
|
||||
type: 'rental',
|
||||
targetId: 'court',
|
||||
targetLabel: '🎾 Теннисный корт',
|
||||
startDate: '2026-03-18',
|
||||
endDate: '2026-03-19',
|
||||
reason: 'Замена покрытия',
|
||||
notes: 'Полная замена резинового покрытия. Корт закрыт на 2 дня.',
|
||||
status: 'scheduled',
|
||||
assignedTo: 'Хозяйственная служба',
|
||||
},
|
||||
]
|
||||
|
||||
// ─── Status helpers ───────────────────────────────────────────────────────────
|
||||
@@ -170,7 +197,11 @@ function MaintenanceModal({
|
||||
const set = <K extends keyof typeof form>(k: K, v: typeof form[K]) =>
|
||||
setForm(prev => ({ ...prev, [k]: v }))
|
||||
|
||||
const targets = form.type === 'room' ? MOCK_ROOMS : SERVICES.map(s => ({ id: s.id, label: s.label }))
|
||||
const targets = form.type === 'room'
|
||||
? MOCK_ROOMS
|
||||
: form.type === 'rental'
|
||||
? RENTAL_OBJECTS.map(o => ({ id: o.id, label: `${o.icon} ${o.name}` }))
|
||||
: SERVICES.map(s => ({ id: s.id, label: s.label }))
|
||||
|
||||
const handleTargetChange = (id: string) => {
|
||||
const t = targets.find(t => t.id === id)
|
||||
@@ -200,17 +231,21 @@ function MaintenanceModal({
|
||||
{/* Type selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Тип</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{([
|
||||
{ id: 'room' as const, label: 'Номер', icon: BedDouble },
|
||||
{ id: 'service' as const, label: 'Услуга', icon: Wrench },
|
||||
{ id: 'rental' as const, label: 'Аренда', icon: CalendarClock },
|
||||
]).map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
set('type', t.id)
|
||||
const def = t.id === 'room' ? MOCK_ROOMS[0] : { id: SERVICES[0].id, label: SERVICES[0].label }
|
||||
let def: { id: string; label: string }
|
||||
if (t.id === 'room') def = MOCK_ROOMS[0]
|
||||
else if (t.id === 'rental') def = { id: RENTAL_OBJECTS[0].id, label: `${RENTAL_OBJECTS[0].icon} ${RENTAL_OBJECTS[0].name}` }
|
||||
else def = { id: SERVICES[0].id, label: SERVICES[0].label }
|
||||
set('targetId', def.id); set('targetLabel', def.label)
|
||||
}}
|
||||
className={cn(
|
||||
@@ -250,8 +285,8 @@ function MaintenanceModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time (for services) */}
|
||||
{form.type === 'service' && (
|
||||
{/* Time (for services and rental) */}
|
||||
{(form.type === 'service' || form.type === 'rental') && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Начало (время)</label>
|
||||
@@ -309,7 +344,7 @@ export function MaintenancePage() {
|
||||
const [records, setRecords] = useState<MaintenanceRecord[]>(MOCK_RECORDS)
|
||||
const [modal, setModal] = useState<'create' | MaintenanceRecord | null>(null)
|
||||
const [delTarget, setDel] = useState<MaintenanceRecord | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'room' | 'service'>('all')
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'room' | 'service' | 'rental'>('all')
|
||||
|
||||
const save = (data: Omit<MaintenanceRecord, 'id'>) => {
|
||||
if (typeof modal === 'object' && modal !== null) {
|
||||
@@ -345,10 +380,10 @@ export function MaintenancePage() {
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'Всего записей', value: records.length, cls: '' },
|
||||
{ label: 'Активных', value: active.length, cls: 'text-amber-600 dark:text-amber-400' },
|
||||
{ label: 'Номеров на ремонте', value: records.filter(r => r.type === 'room' && r.status !== 'done' && r.status !== 'cancelled').length, cls: 'text-red-600 dark:text-red-400' },
|
||||
{ label: 'Услуг недоступно', value: records.filter(r => r.type === 'service' && r.status !== 'done' && r.status !== 'cancelled').length, cls: 'text-blue-600 dark:text-blue-400' },
|
||||
{ label: 'Всего записей', value: records.length, cls: '' },
|
||||
{ label: 'Активных', value: active.length, cls: 'text-amber-600 dark:text-amber-400' },
|
||||
{ label: 'Номеров на ремонте', value: records.filter(r => r.type === 'room' && r.status !== 'done' && r.status !== 'cancelled').length, cls: 'text-red-600 dark:text-red-400' },
|
||||
{ label: 'Услуг / аренды', value: records.filter(r => r.type !== 'room' && r.status !== 'done' && r.status !== 'cancelled').length, cls: 'text-blue-600 dark:text-blue-400' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="card p-4 text-center">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{s.label}</p>
|
||||
@@ -378,6 +413,7 @@ export function MaintenancePage() {
|
||||
{ id: 'all' as const, label: `Все (${records.length})` },
|
||||
{ id: 'room' as const, label: `Номера (${records.filter(r => r.type === 'room').length})` },
|
||||
{ id: 'service' as const, label: `Услуги (${records.filter(r => r.type === 'service').length})` },
|
||||
{ id: 'rental' as const, label: `Аренда (${records.filter(r => r.type === 'rental').length})` },
|
||||
]).map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
@@ -416,11 +452,15 @@ export function MaintenancePage() {
|
||||
'w-10 h-10 rounded-xl flex items-center justify-center shrink-0',
|
||||
record.type === 'room'
|
||||
? 'bg-slate-100 dark:bg-slate-700'
|
||||
: 'bg-blue-50 dark:bg-blue-900/20',
|
||||
: record.type === 'rental'
|
||||
? 'bg-emerald-50 dark:bg-emerald-900/20'
|
||||
: 'bg-blue-50 dark:bg-blue-900/20',
|
||||
)}>
|
||||
{record.type === 'room'
|
||||
? <BedDouble size={18} className="text-slate-500 dark:text-slate-400" />
|
||||
: svc ? <svc.icon size={18} className="text-blue-500" /> : <Wrench size={18} className="text-blue-500" />
|
||||
: record.type === 'rental'
|
||||
? <CalendarClock size={18} className="text-emerald-500" />
|
||||
: svc ? <svc.icon size={18} className="text-blue-500" /> : <Wrench size={18} className="text-blue-500" />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user