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:
2026-03-16 16:04:19 +03:00
parent f62ccdd3f7
commit c431ea1486
6 changed files with 461 additions and 157 deletions

View File

@@ -0,0 +1,133 @@
import { createContext, useContext, useState, type ReactNode } from 'react'
export type NotifType =
| 'booking_new'
| 'booking_cancelled'
| 'booking_checkin'
| 'booking_checkout'
| 'housekeeping'
| 'channel_error'
| 'payment'
| 'review'
| 'system'
| 'maintenance'
export interface Notification {
id: string
type: NotifType
title: string
body: string
time: string
isRead: boolean
link?: string
}
const INITIAL_NOTIFICATIONS: Notification[] = [
{
id: 'n1', type: 'booking_new',
title: 'Новое бронирование',
body: 'Алексей Смирнов забронировал номер 205 (Делюкс) на 1418 марта',
time: new Date(Date.now() - 4 * 60000).toISOString(),
isRead: false, link: '/bookings',
},
{
id: 'n2', type: 'channel_error',
title: 'Ошибка синхронизации',
body: 'Booking.com: не удалось обновить доступность. Проверьте подключение.',
time: new Date(Date.now() - 18 * 60000).toISOString(),
isRead: false, link: '/channels',
},
{
id: 'n3', type: 'booking_checkin',
title: 'Заезд сегодня',
body: 'Мария Иванова · номер 101 · заезд в 14:00',
time: new Date(Date.now() - 45 * 60000).toISOString(),
isRead: false, link: '/bookings',
},
{
id: 'n4', type: 'review',
title: 'Новый отзыв требует модерации',
body: 'Гость оставил оценку 2/5 — отзыв ждёт вашего ответа',
time: new Date(Date.now() - 2 * 3600000).toISOString(),
isRead: false, link: '/reviews',
},
{
id: 'n5', type: 'housekeeping',
title: 'Уборка завершена',
body: 'Горничная Козлова Н. завершила уборку номеров 101, 102, 203',
time: new Date(Date.now() - 3 * 3600000).toISOString(),
isRead: true, link: '/housekeeping',
},
{
id: 'n6', type: 'payment',
title: 'Оплата получена',
body: 'Иван Петров оплатил бронирование #B-2847 — 12 400 ₽',
time: new Date(Date.now() - 5 * 3600000).toISOString(),
isRead: true,
},
{
id: 'n7', type: 'booking_cancelled',
title: 'Бронирование отменено',
body: 'Дмитрий Волков отменил бронь номера 304 на 2022 марта',
time: new Date(Date.now() - 26 * 3600000).toISOString(),
isRead: true, link: '/bookings',
},
{
id: 'n8', type: 'booking_checkout',
title: 'Выезд завершён',
body: 'Семья Соколовых выехала из номера 206. Нужна уборка.',
time: new Date(Date.now() - 28 * 3600000).toISOString(),
isRead: true, link: '/housekeeping',
},
{
id: 'n9', type: 'system',
title: 'Обновление системы',
body: 'HotelSync обновлён до версии 0.2.0. Что нового — в журнале изменений.',
time: new Date(Date.now() - 3 * 86400000).toISOString(),
isRead: true,
},
]
interface NotificationsContextValue {
notifications: Notification[]
addNotification: (n: Omit<Notification, 'id' | 'time' | 'isRead'>) => void
markRead: (id: string) => void
markAllRead: () => void
removeNotif: (id: string) => void
}
const NotificationsContext = createContext<NotificationsContextValue | null>(null)
export function NotificationsProvider({ children }: { children: ReactNode }) {
const [notifications, setNotifications] = useState<Notification[]>(INITIAL_NOTIFICATIONS)
const addNotification = (n: Omit<Notification, 'id' | 'time' | 'isRead'>) => {
setNotifications(prev => [{
...n,
id: `n-${Date.now()}`,
time: new Date().toISOString(),
isRead: false,
}, ...prev])
}
const markRead = (id: string) =>
setNotifications(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n))
const markAllRead = () =>
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })))
const removeNotif = (id: string) =>
setNotifications(prev => prev.filter(n => n.id !== id))
return (
<NotificationsContext.Provider value={{ notifications, addNotification, markRead, markAllRead, removeNotif }}>
{children}
</NotificationsContext.Provider>
)
}
export function useNotifications() {
const ctx = useContext(NotificationsContext)
if (!ctx) throw new Error('useNotifications must be used within NotificationsProvider')
return ctx
}