Initial commit: HotelSync PMS v0.1.0

- React 18 + TypeScript + Vite + Tailwind CSS
- Шахматка бронирований (drag-to-book)
- Страницы: Calendar, Bookings, Rooms, Housekeeping, Channels, API Docs, Settings
- Роли: super_admin, hotel_manager, housekeeper
- Светлая/тёмная тема
- Docker + Nginx конфигурация
- Лендинг hotelsync.ru
This commit is contained in:
2026-03-10 20:38:32 +03:00
commit 420d55d57e
45 changed files with 7274 additions and 0 deletions

128
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,128 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
import type { BookingStatus, BookingSource, HousekeepingStatus, RoomStatus, Role, HotelPlan } from '../types'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatCurrency(amount: number, currency = 'RUB') {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency,
maximumFractionDigits: 0,
}).format(amount)
}
export function formatDate(date: string, opts?: Intl.DateTimeFormatOptions) {
return new Intl.DateTimeFormat('ru-RU', opts ?? { day: 'numeric', month: 'short' }).format(new Date(date))
}
// ─── Booking status ───────────────────────────────────────────────────────────
export const BOOKING_STATUS_LABELS: Record<BookingStatus, string> = {
inquiry: 'Запрос',
confirmed: 'Подтверждён',
checked_in: 'Заселён',
checked_out: 'Выехал',
cancelled: 'Отменён',
no_show: 'Неявка',
}
export const BOOKING_STATUS_COLORS: Record<BookingStatus, string> = {
inquiry: 'bg-amber-400 border-amber-600',
confirmed: 'bg-brand-500 border-brand-700',
checked_in: 'bg-emerald-500 border-emerald-700',
checked_out: 'bg-slate-400 border-slate-600',
cancelled: 'bg-red-400 border-red-600',
no_show: 'bg-orange-400 border-orange-600',
}
export const BOOKING_STATUS_BADGE: Record<BookingStatus, string> = {
inquiry: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
confirmed: 'bg-brand-100 text-brand-800 dark:bg-brand-900/30 dark:text-brand-300',
checked_in: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300',
checked_out: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300',
cancelled: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
no_show: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
}
// ─── Source ───────────────────────────────────────────────────────────────────
export const SOURCE_LABELS: Record<BookingSource, string> = {
direct: 'Прямое',
booking_com: 'Booking.com',
airbnb: 'Airbnb',
expedia: 'Expedia',
other: 'Другое',
}
export const SOURCE_COLORS: Record<BookingSource, string> = {
direct: 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-200',
booking_com: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
airbnb: 'bg-rose-100 text-rose-800 dark:bg-rose-900/30 dark:text-rose-300',
expedia: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
other: 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-200',
}
// ─── Housekeeping ─────────────────────────────────────────────────────────────
export const HK_STATUS_LABELS: Record<HousekeepingStatus, string> = {
clean: 'Чистый',
dirty: 'Грязный',
cleaning: 'Убирается',
inspect: 'Проверка',
}
export const HK_STATUS_COLORS: Record<HousekeepingStatus, string> = {
clean: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
dirty: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
cleaning: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
inspect: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
}
// ─── Room status ──────────────────────────────────────────────────────────────
export const ROOM_STATUS_LABELS: Record<RoomStatus, string> = {
available: 'Свободен',
occupied: 'Занят',
maintenance: 'Ремонт',
blocked: 'Заблокирован',
}
export const ROOM_STATUS_COLORS: Record<RoomStatus, string> = {
available: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
occupied: 'bg-brand-100 text-brand-700 dark:bg-brand-900/30 dark:text-brand-300',
maintenance: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
blocked: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400',
}
// ─── Role ─────────────────────────────────────────────────────────────────────
export const ROLE_LABELS: Record<Role, string> = {
super_admin: 'Супер-администратор',
hotel_manager: 'Менеджер отеля',
housekeeper: 'Горничная',
}
// ─── Plan ─────────────────────────────────────────────────────────────────────
export const PLAN_LABELS: Record<HotelPlan, string> = {
starter: 'Starter',
pro: 'Pro',
enterprise: 'Enterprise',
}
export const PLAN_COLORS: Record<HotelPlan, string> = {
starter: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300',
pro: 'bg-brand-100 text-brand-700 dark:bg-brand-900/30 dark:text-brand-300',
enterprise: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
}
// ─── Nights calc ─────────────────────────────────────────────────────────────
export function nightsCount(checkIn: string, checkOut: string): number {
const a = new Date(checkIn)
const b = new Date(checkOut)
return Math.round((b.getTime() - a.getTime()) / 86400000)
}