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:
181
src/pages/HousekeepingPage.tsx
Normal file
181
src/pages/HousekeepingPage.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2, Clock, AlertCircle, Sparkles, User } 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'
|
||||
|
||||
type Column = 'pending' | 'in_progress' | 'done'
|
||||
|
||||
const COLUMNS: { id: Column; label: string; icon: React.ElementType; color: string }[] = [
|
||||
{ id: 'pending', label: 'Ожидают', icon: Clock, color: 'text-amber-600 dark:text-amber-400' },
|
||||
{ id: 'in_progress', label: 'В процессе', icon: Sparkles, color: 'text-blue-600 dark:text-blue-400' },
|
||||
{ id: 'done', label: 'Готово', icon: CheckCircle2, color: 'text-emerald-600 dark:text-emerald-400' },
|
||||
]
|
||||
|
||||
const PRIORITY_COLORS = {
|
||||
high: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||
normal: 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300',
|
||||
low: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||
}
|
||||
|
||||
const PRIORITY_LABELS = { high: 'Срочно', normal: 'Обычный', low: 'Низкий' }
|
||||
|
||||
const TYPE_COLORS = {
|
||||
cleaning: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
inspection: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
maintenance: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
}
|
||||
|
||||
const TYPE_LABELS = {
|
||||
cleaning: 'Уборка',
|
||||
inspection: 'Проверка',
|
||||
maintenance: 'Ремонт',
|
||||
}
|
||||
|
||||
export function HousekeepingPage() {
|
||||
const [tasks, setTasks] = useState<HousekeepingTask[]>(MOCK_HK_TASKS)
|
||||
|
||||
const updateStatus = (id: string, status: HousekeepingTask['status']) => {
|
||||
setTasks(prev => prev.map(t =>
|
||||
t.id === id
|
||||
? { ...t, status, completedAt: status === 'done' ? new Date().toISOString() : undefined }
|
||||
: t,
|
||||
))
|
||||
}
|
||||
|
||||
const total = tasks.length
|
||||
const done = tasks.filter(t => t.status === 'done').length
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6 space-y-5">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Уборка и обслуживание</h1>
|
||||
<div className="flex items-center gap-3 mt-1.5">
|
||||
<div className="flex-1 bg-slate-200 dark:bg-slate-700 rounded-full h-2">
|
||||
<div
|
||||
className="h-2 rounded-full bg-emerald-500 transition-all duration-500"
|
||||
style={{ width: `${(done / total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400 shrink-0">
|
||||
{done} / {total} завершено
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kanban columns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{COLUMNS.map(col => {
|
||||
const colTasks = tasks.filter(t => t.status === col.id)
|
||||
const Icon = col.icon
|
||||
return (
|
||||
<div key={col.id} className="space-y-3">
|
||||
{/* Column header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={16} className={col.color} />
|
||||
<h3 className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
{col.label}
|
||||
</h3>
|
||||
<span className="ml-auto text-xs font-bold px-2 py-0.5 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-400">
|
||||
{colTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="space-y-2.5">
|
||||
{colTasks.map(task => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
onStatusChange={updateStatus}
|
||||
/>
|
||||
))}
|
||||
{colTasks.length === 0 && (
|
||||
<div className="rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700 py-8 text-center">
|
||||
<p className="text-sm text-slate-400 dark:text-slate-500">Пусто</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskCard({ task, onStatusChange }: {
|
||||
task: HousekeepingTask
|
||||
onStatusChange: (id: string, status: HousekeepingTask['status']) => void
|
||||
}) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'card p-3.5 space-y-2.5 transition-all',
|
||||
task.status === 'done' && 'opacity-70',
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
№{task.roomNumber}
|
||||
</span>
|
||||
<Badge className={PRIORITY_COLORS[task.priority]}>
|
||||
{PRIORITY_LABELS[task.priority]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
<Badge className={TYPE_COLORS[task.type]}>
|
||||
{TYPE_LABELS[task.type]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{task.notes && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-700/40 rounded-lg p-2">
|
||||
{task.notes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{task.assignedToName && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
|
||||
<User size={11} />
|
||||
{task.assignedToName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.completedAt && (
|
||||
<p className="text-xs text-emerald-600 dark:text-emerald-400">
|
||||
Завершено в {new Date(task.completedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-1.5 pt-1">
|
||||
{task.status === 'pending' && (
|
||||
<button
|
||||
onClick={() => onStatusChange(task.id, 'in_progress')}
|
||||
className="flex-1 text-xs py-1.5 rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 font-medium hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
|
||||
>
|
||||
Начать
|
||||
</button>
|
||||
)}
|
||||
{task.status === 'in_progress' && (
|
||||
<button
|
||||
onClick={() => onStatusChange(task.id, 'done')}
|
||||
className="flex-1 text-xs py-1.5 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300 font-medium hover:bg-emerald-100 dark:hover:bg-emerald-900/30 transition-colors"
|
||||
>
|
||||
Завершить
|
||||
</button>
|
||||
)}
|
||||
{task.status === 'done' && (
|
||||
<button
|
||||
onClick={() => onStatusChange(task.id, 'pending')}
|
||||
className="flex-1 text-xs py-1.5 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-400 font-medium hover:bg-slate-100 dark:hover:bg-slate-600 transition-colors"
|
||||
>
|
||||
Вернуть
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user