Add maintenance severity levels, notifications context, and widget payment step
- HousekeepingPage: 3-level severity for maintenance reports (Не срочно / Средняя / Экстренно!); Экстренно automatically blocks the room and triggers an urgent notification; severity displayed as colored badge on task card - NotificationsContext: extract shared notifications state from Topbar into context so any page can push notifications; add 'maintenance' type with wrench icon - Topbar: migrate to NotificationsContext; add maintenance notification type with red Wrench icon - BookingWidgetPage: add payment step after guest form when payment provider is connected (card number/expiry/CVV/name form); Без оплаты mode skips payment and shows "pay on site" message; hourly services (unit='ч') show date + time-from/time-to picker in form step Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save, Wrench, X as XIcon, Send } from 'lucide-react'
|
||||
import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save, Wrench, X as XIcon, Send, AlertTriangle, BanIcon } from 'lucide-react'
|
||||
import { MOCK_HK_TASKS } from '../data/mockData'
|
||||
import type { HousekeepingTask } from '../types'
|
||||
import { cn } from '../lib/utils'
|
||||
import { Badge } from '../components/ui/Badge'
|
||||
import { useNotifications } from '../contexts/NotificationsContext'
|
||||
|
||||
function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
|
||||
return (
|
||||
@@ -85,8 +86,30 @@ export function HousekeepingPage() {
|
||||
))
|
||||
}
|
||||
|
||||
const addMaintenanceNote = (id: string, note: string) => {
|
||||
setTasks(prev => prev.map(t => t.id === id ? { ...t, maintenanceNote: note } : t))
|
||||
const { addNotification } = useNotifications()
|
||||
|
||||
const addMaintenanceReport = (
|
||||
id: string,
|
||||
note: string,
|
||||
severity: 'low' | 'medium' | 'high',
|
||||
) => {
|
||||
const task = tasks.find(t => t.id === id)
|
||||
const roomBlocked = severity === 'high'
|
||||
setTasks(prev => prev.map(t =>
|
||||
t.id === id
|
||||
? { ...t, maintenanceNote: note, maintenanceSeverity: severity, roomBlocked }
|
||||
: t,
|
||||
))
|
||||
|
||||
const severityLabel = severity === 'high' ? 'Экстренно' : severity === 'medium' ? 'Средняя срочность' : 'Не срочно'
|
||||
addNotification({
|
||||
type: 'maintenance',
|
||||
title: severity === 'high'
|
||||
? `⚠️ Экстренная поломка — Номер ${task?.roomNumber}`
|
||||
: `Поломка в номере ${task?.roomNumber}`,
|
||||
body: `[${severityLabel}] ${note}${roomBlocked ? ' · Номер закрыт для бронирования.' : ''}`,
|
||||
link: '/housekeeping',
|
||||
})
|
||||
}
|
||||
|
||||
const total = tasks.length
|
||||
@@ -153,7 +176,7 @@ export function HousekeepingPage() {
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{colTasks.map(task => (
|
||||
<TaskCard key={task.id} task={task} onStatusChange={updateStatus} onMaintenanceNote={addMaintenanceNote} />
|
||||
<TaskCard key={task.id} task={task} onStatusChange={updateStatus} onReport={addMaintenanceReport} />
|
||||
))}
|
||||
{colTasks.length === 0 && (
|
||||
<div className="rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700 py-8 text-center">
|
||||
@@ -365,30 +388,47 @@ export function HousekeepingPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function TaskCard({ task, onStatusChange, onMaintenanceNote }: {
|
||||
const SEVERITY_CONFIG = {
|
||||
low: { label: 'Не срочно', bg: 'bg-emerald-100 dark:bg-emerald-900/30', text: 'text-emerald-700 dark:text-emerald-300', border: 'border-emerald-200 dark:border-emerald-800', dot: 'bg-emerald-500' },
|
||||
medium: { label: 'Средняя', bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300', border: 'border-amber-200 dark:border-amber-800', dot: 'bg-amber-500' },
|
||||
high: { label: 'Экстренно!', bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300', border: 'border-red-200 dark:border-red-800', dot: 'bg-red-500' },
|
||||
}
|
||||
|
||||
function TaskCard({ task, onStatusChange, onReport }: {
|
||||
task: HousekeepingTask
|
||||
onStatusChange: (id: string, status: HousekeepingTask['status']) => void
|
||||
onMaintenanceNote: (id: string, note: string) => void
|
||||
onReport: (id: string, note: string, severity: 'low' | 'medium' | 'high') => void
|
||||
}) {
|
||||
const [reportOpen, setReportOpen] = useState(false)
|
||||
const [reportText, setReportText] = useState('')
|
||||
const [severity, setSeverity] = useState<'low' | 'medium' | 'high'>('medium')
|
||||
|
||||
const submitReport = () => {
|
||||
if (!reportText.trim()) return
|
||||
onMaintenanceNote(task.id, reportText.trim())
|
||||
onReport(task.id, reportText.trim(), severity)
|
||||
setReportText('')
|
||||
setReportOpen(false)
|
||||
}
|
||||
|
||||
const sev = task.maintenanceSeverity ? SEVERITY_CONFIG[task.maintenanceSeverity] : null
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'card p-3.5 space-y-2.5 transition-all',
|
||||
task.status === 'done' && 'opacity-70',
|
||||
task.roomBlocked && 'ring-2 ring-red-400 dark:ring-red-600',
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
№{task.roomNumber}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
№{task.roomNumber}
|
||||
</span>
|
||||
{task.roomBlocked && (
|
||||
<span className="flex items-center gap-0.5 text-[10px] font-bold text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 px-1.5 py-0.5 rounded">
|
||||
<BanIcon size={10} /> Закрыт
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Badge className={PRIORITY_COLORS[task.priority]}>
|
||||
{PRIORITY_LABELS[task.priority]}
|
||||
</Badge>
|
||||
@@ -406,10 +446,16 @@ function TaskCard({ task, onStatusChange, onMaintenanceNote }: {
|
||||
</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>
|
||||
{task.maintenanceNote && sev && (
|
||||
<div className={cn('rounded-lg p-2.5 border space-y-1', sev.bg, sev.border)}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<AlertTriangle size={11} className={sev.text} />
|
||||
<span className={cn('text-[10px] font-bold uppercase tracking-wide', sev.text)}>{sev.label}</span>
|
||||
{task.roomBlocked && (
|
||||
<span className={cn('text-[10px] font-semibold ml-auto', sev.text)}>Номер закрыт</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={cn('text-xs', sev.text)}>{task.maintenanceNote}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -428,15 +474,41 @@ function TaskCard({ task, onStatusChange, onMaintenanceNote }: {
|
||||
|
||||
{/* Maintenance report form */}
|
||||
{reportOpen && (
|
||||
<div className="border-t border-slate-100 dark:border-slate-700 pt-2 space-y-1.5">
|
||||
<div className="border-t border-slate-100 dark:border-slate-700 pt-2.5 space-y-2">
|
||||
<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 className="text-xs font-semibold text-slate-700 dark:text-slate-300 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>
|
||||
|
||||
{/* Severity selector */}
|
||||
<div className="flex gap-1.5">
|
||||
{(Object.entries(SEVERITY_CONFIG) as [typeof severity, typeof SEVERITY_CONFIG['low']][]).map(([key, cfg]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setSeverity(key)}
|
||||
className={cn(
|
||||
'flex-1 text-xs py-1.5 rounded-lg font-medium border transition-colors',
|
||||
severity === key
|
||||
? `${cfg.bg} ${cfg.text} ${cfg.border}`
|
||||
: 'bg-white dark:bg-slate-700 text-slate-500 dark:text-slate-400 border-slate-200 dark:border-slate-600',
|
||||
)}
|
||||
>
|
||||
{cfg.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{severity === 'high' && (
|
||||
<p className="text-[11px] text-red-600 dark:text-red-400 flex items-center gap-1 bg-red-50 dark:bg-red-900/20 rounded-lg px-2 py-1.5">
|
||||
<AlertTriangle size={11} className="shrink-0" />
|
||||
Номер будет закрыт для бронирования до устранения поломки
|
||||
</p>
|
||||
)}
|
||||
|
||||
<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"
|
||||
@@ -449,7 +521,12 @@ function TaskCard({ task, onStatusChange, onMaintenanceNote }: {
|
||||
<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"
|
||||
className={cn(
|
||||
'w-full flex items-center justify-center gap-1 text-xs py-1.5 rounded-lg text-white font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed',
|
||||
severity === 'high' ? 'bg-red-600 hover:bg-red-700' :
|
||||
severity === 'medium' ? 'bg-amber-500 hover:bg-amber-600' :
|
||||
'bg-emerald-600 hover:bg-emerald-700',
|
||||
)}
|
||||
>
|
||||
<Send size={11} /> Отправить технической службе
|
||||
</button>
|
||||
@@ -485,11 +562,11 @@ function TaskCard({ task, onStatusChange, onMaintenanceNote }: {
|
||||
{!reportOpen && (
|
||||
<button
|
||||
onClick={() => setReportOpen(true)}
|
||||
title="Сообщить о поломке"
|
||||
title="Сообщить о неисправности"
|
||||
className={cn(
|
||||
'text-xs py-1.5 px-2.5 rounded-lg font-medium transition-colors flex items-center gap-1',
|
||||
'text-xs py-1.5 px-2.5 rounded-lg font-medium transition-colors flex items-center gap-1 shrink-0',
|
||||
task.maintenanceNote
|
||||
? 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400'
|
||||
? `${sev?.bg} ${sev?.text}`
|
||||
: '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',
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user