Add occupancy analytics, housekeeping maintenance notes, and booking widget form flow
- DynamicPricingPage: add 'Нагрузка' tab with monthly occupancy bar chart (2025/2026 data), year selector, summary stats, and editable occupancy-based pricing rules (threshold ranges → price modifiers) - HousekeepingPage: add maintenance note button (wrench icon) on task cards — housekeeper can report broken items with a description that gets sent to technical service; notes displayed in orange badge - BookingWidgetPage: fix 'Забронировать' button with full form flow (browse → form → success), add configurable form fields (required/optional/custom), add additional services toggle in settings and at checkout, add extra beds + children count in guest selector with auto-pricing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save } from 'lucide-react'
|
||||
import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save, Wrench, X as XIcon, Send } from 'lucide-react'
|
||||
import { MOCK_HK_TASKS } from '../data/mockData'
|
||||
import type { HousekeepingTask } from '../types'
|
||||
import { cn } from '../lib/utils'
|
||||
@@ -85,6 +85,10 @@ export function HousekeepingPage() {
|
||||
))
|
||||
}
|
||||
|
||||
const addMaintenanceNote = (id: string, note: string) => {
|
||||
setTasks(prev => prev.map(t => t.id === id ? { ...t, maintenanceNote: note } : t))
|
||||
}
|
||||
|
||||
const total = tasks.length
|
||||
const done = tasks.filter(t => t.status === 'done').length
|
||||
|
||||
@@ -149,7 +153,7 @@ export function HousekeepingPage() {
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{colTasks.map(task => (
|
||||
<TaskCard key={task.id} task={task} onStatusChange={updateStatus} />
|
||||
<TaskCard key={task.id} task={task} onStatusChange={updateStatus} onMaintenanceNote={addMaintenanceNote} />
|
||||
))}
|
||||
{colTasks.length === 0 && (
|
||||
<div className="rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700 py-8 text-center">
|
||||
@@ -361,10 +365,21 @@ export function HousekeepingPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function TaskCard({ task, onStatusChange }: {
|
||||
function TaskCard({ task, onStatusChange, onMaintenanceNote }: {
|
||||
task: HousekeepingTask
|
||||
onStatusChange: (id: string, status: HousekeepingTask['status']) => void
|
||||
onMaintenanceNote: (id: string, note: string) => void
|
||||
}) {
|
||||
const [reportOpen, setReportOpen] = useState(false)
|
||||
const [reportText, setReportText] = useState('')
|
||||
|
||||
const submitReport = () => {
|
||||
if (!reportText.trim()) return
|
||||
onMaintenanceNote(task.id, reportText.trim())
|
||||
setReportText('')
|
||||
setReportOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'card p-3.5 space-y-2.5 transition-all',
|
||||
@@ -391,6 +406,13 @@ function TaskCard({ task, onStatusChange }: {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{task.maintenanceNote && (
|
||||
<div className="flex items-start gap-1.5 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-2">
|
||||
<Wrench size={11} className="text-orange-500 mt-0.5 shrink-0" />
|
||||
<p className="text-xs text-orange-700 dark:text-orange-300">{task.maintenanceNote}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.assignedToName && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
|
||||
<User size={11} />
|
||||
@@ -404,6 +426,36 @@ function TaskCard({ task, onStatusChange }: {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Maintenance report form */}
|
||||
{reportOpen && (
|
||||
<div className="border-t border-slate-100 dark:border-slate-700 pt-2 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-orange-600 dark:text-orange-400 flex items-center gap-1">
|
||||
<Wrench size={11} /> Сообщить о поломке
|
||||
</p>
|
||||
<button onClick={() => setReportOpen(false)} className="text-slate-400 hover:text-slate-600">
|
||||
<XIcon size={13} />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
autoFocus
|
||||
className="w-full text-xs border border-slate-200 dark:border-slate-600 rounded-lg p-2 bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 resize-none focus:outline-none focus:border-orange-400"
|
||||
rows={2}
|
||||
placeholder="Опишите неисправность (напр. Не работает кондиционер, сломана ручка двери...)"
|
||||
value={reportText}
|
||||
onChange={e => setReportText(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && e.ctrlKey) submitReport() }}
|
||||
/>
|
||||
<button
|
||||
onClick={submitReport}
|
||||
disabled={!reportText.trim()}
|
||||
className="w-full flex items-center justify-center gap-1 text-xs py-1.5 rounded-lg bg-orange-500 text-white font-medium hover:bg-orange-600 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Send size={11} /> Отправить технической службе
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-1.5 pt-1">
|
||||
{task.status === 'pending' && (
|
||||
@@ -430,6 +482,20 @@ function TaskCard({ task, onStatusChange }: {
|
||||
Вернуть
|
||||
</button>
|
||||
)}
|
||||
{!reportOpen && (
|
||||
<button
|
||||
onClick={() => setReportOpen(true)}
|
||||
title="Сообщить о поломке"
|
||||
className={cn(
|
||||
'text-xs py-1.5 px-2.5 rounded-lg font-medium transition-colors flex items-center gap-1',
|
||||
task.maintenanceNote
|
||||
? 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400'
|
||||
: 'bg-slate-50 dark:bg-slate-700 text-slate-500 dark:text-slate-400 hover:bg-orange-50 dark:hover:bg-orange-900/20 hover:text-orange-600',
|
||||
)}
|
||||
>
|
||||
<Wrench size={11} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user