diff --git a/backend/src/routes/housekeeping.ts b/backend/src/routes/housekeeping.ts index ab41146..5cebd7d 100644 --- a/backend/src/routes/housekeeping.ts +++ b/backend/src/routes/housekeeping.ts @@ -32,7 +32,11 @@ const housekeeping: FastifyPluginAsync = async (fastify) => { let idx = 2 const { status, date, category } = request.query - if (status) { conditions.push(`t.status = $${idx}`); values.push(status); idx++ } + if (status === 'active') { + conditions.push(`t.status IN ('pending', 'in_progress')`) + } else if (status) { + conditions.push(`t.status = $${idx}`); values.push(status); idx++ + } if (date) { conditions.push(`t.due_date = $${idx}`); values.push(date); idx++ } if (category) { conditions.push(`t.category = $${idx}`); values.push(category); idx++ } diff --git a/src/pages/CalendarPage.tsx b/src/pages/CalendarPage.tsx index a196509..5b518ec 100644 --- a/src/pages/CalendarPage.tsx +++ b/src/pages/CalendarPage.tsx @@ -162,7 +162,7 @@ export function CalendarPage() { const today = new Date().toISOString().slice(0, 10) const task = await api.housekeeping.create(slug, { room_id: roomId, - type: 'regular', + type: 'cleaning', priority: priority ?? 'medium', due_date: today, category: 'housekeeping', diff --git a/src/pages/HousekeepingPage.tsx b/src/pages/HousekeepingPage.tsx index 62f120a..50bede7 100644 --- a/src/pages/HousekeepingPage.tsx +++ b/src/pages/HousekeepingPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save, Wrench, X as XIcon, Send, AlertTriangle, BanIcon, Loader2 } from 'lucide-react' +import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save, Wrench, X as XIcon, Send, AlertTriangle, BanIcon, Loader2, History, ChevronLeft, ChevronRight } from 'lucide-react' import { useAuth } from '../contexts/AuthContext' import { useHotelSocket } from '../hooks/useHotelSocket' import type { WsMessage } from '../hooks/useHotelSocket' @@ -9,7 +9,8 @@ import type { HousekeepingTask } from '../types' import { cn } from '../lib/utils' import { Badge } from '../components/ui/Badge' import { useNotifications } from '../contexts/NotificationsContext' -import { format } from 'date-fns' +import { format, subDays, addDays, parseISO } from 'date-fns' +import { ru } from 'date-fns/locale' function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) { return ( @@ -38,51 +39,61 @@ const PRIORITY_COLORS: Record = { } const PRIORITY_LABELS: Record = { - urgent: 'Экстренно', high: 'Срочно', medium: 'Обычный', low: 'Низкий', + urgent: 'Срочно', high: 'Срочно', medium: 'Обычный', low: 'Низкий', } -const TYPE_COLORS = { +const TYPE_COLORS: Record = { cleaning: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', + turnover: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-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: 'Ремонт', +const TYPE_LABELS: Record = { + cleaning: 'Уборка', turnover: 'Уборка при выезде', inspection: 'Проверка', maintenance: 'Ремонт', } export function HousekeepingPage() { const { user, session } = useAuth() const slug = user?.hotelSlug ?? '' - const [tasks, setTasks] = useState([]) - const [loading, setLoading] = useState(true) - const [activeTab, setActiveTab] = useState<'tasks' | 'plans'>('tasks') - const [planSaved, setPlanSaved] = useState(false) + const [activeTasks, setActiveTasks] = useState([]) + const [historyTasks, setHistoryTasks] = useState([]) + const [historyDate, setHistoryDate] = useState(format(new Date(), 'yyyy-MM-dd')) + const [loading, setLoading] = useState(true) + const [historyLoading, setHistoryLoading] = useState(false) + const [activeTab, setActiveTab] = useState<'tasks' | 'history' | 'plans'>('tasks') + const [planSaved, setPlanSaved] = useState(false) const handleWsMessage = useCallback((msg: WsMessage) => { if (msg.type === 'housekeeping_task_created') { const task = msg.task as unknown as HousekeepingTask & { category?: string } if (task.category !== 'maintenance') { - setTasks(prev => prev.some(t => t.id === task.id) ? prev : [task, ...prev]) + setActiveTasks(prev => prev.some(t => t.id === task.id) ? prev : [task, ...prev]) } } else if (msg.type === 'housekeeping_updated') { const task = msg.task as unknown as HousekeepingTask & { category?: string } if (task.category !== 'maintenance') { - setTasks(prev => prev.map(t => t.id === task.id ? task : t)) + // If task is now done → remove from active, add to history if same date + if (task.status === 'done') { + setActiveTasks(prev => prev.filter(t => t.id !== task.id)) + } else { + setActiveTasks(prev => prev.map(t => t.id === task.id ? task : t)) + } } } }, []) useHotelSocket({ slug, token: session?.token ?? null, onMessage: handleWsMessage }) + // Load active tasks (all pending/in_progress — no date filter) useEffect(() => { if (!slug) return Promise.all([ - api.housekeeping.list(slug, { date: format(new Date(), 'yyyy-MM-dd'), category: 'housekeeping' }), + api.housekeeping.list(slug, { status: 'active', category: 'housekeeping' }), api.housekeeping.getSettings(slug).catch(() => null), ]).then(([t, s]) => { - setTasks(t) + setActiveTasks(t) if (s) setPlans(prev => ({ ...prev, checkoutAuto: s.checkout_auto, @@ -93,6 +104,16 @@ export function HousekeepingPage() { .finally(() => setLoading(false)) }, [slug]) + // Load history when tab opens or date changes + useEffect(() => { + if (activeTab !== 'history' || !slug) return + setHistoryLoading(true) + api.housekeeping.list(slug, { status: 'done', date: historyDate, category: 'housekeeping' }) + .then(setHistoryTasks) + .catch(console.error) + .finally(() => setHistoryLoading(false)) + }, [activeTab, historyDate, slug]) + // Cleaning plan settings const [plans, setPlans] = useState({ checkoutAuto: true, checkoutPriority: 'high' as 'high' | 'medium', @@ -127,7 +148,14 @@ export function HousekeepingPage() { const updateStatus = async (id: string, status: HousekeepingTask['status']) => { try { const updated = await api.housekeeping.update(slug, id, { status }) - setTasks(prev => prev.map(t => t.id === id ? updated : t)) + if (updated.status === 'done') { + setActiveTasks(prev => prev.filter(t => t.id !== id)) + if (activeTab === 'history' && updated.completedAt?.startsWith(historyDate)) { + setHistoryTasks(prev => [updated, ...prev]) + } + } else { + setActiveTasks(prev => prev.map(t => t.id === id ? updated : t)) + } } catch (err) { console.error('Failed to update task:', err) } @@ -136,9 +164,9 @@ export function HousekeepingPage() { const { addNotification } = useNotifications() const addMaintenanceReport = (id: string, note: string, severity: 'low' | 'medium' | 'high') => { - const task = tasks.find(t => t.id === id) + const task = activeTasks.find(t => t.id === id) const roomBlocked = severity === 'high' - setTasks(prev => prev.map(t => + setActiveTasks(prev => prev.map(t => t.id === id ? { ...t, maintenanceNote: note, maintenanceSeverity: severity, roomBlocked } : t, )) const severityLabel = severity === 'high' ? 'Экстренно' : severity === 'medium' ? 'Средняя срочность' : 'Не срочно' @@ -152,8 +180,8 @@ export function HousekeepingPage() { }) } - const total = tasks.length - const done = tasks.filter(t => t.status === 'done').length + const total = activeTasks.length + const done = activeTasks.filter(t => t.status === 'done').length if (loading) { return ( @@ -188,8 +216,9 @@ export function HousekeepingPage() { {/* Tabs */}
{([ - { id: 'tasks' as const, label: 'Задачи на сегодня', icon: ListChecks }, - { id: 'plans' as const, label: 'Планы уборки', icon: Settings2 }, + { id: 'tasks' as const, label: 'Задачи', icon: ListChecks }, + { id: 'history' as const, label: 'История', icon: History }, + { id: 'plans' as const, label: 'Планы уборки', icon: Settings2 }, ]).map(t => ( + setHistoryDate(e.target.value)} + className="input text-sm w-auto" + /> + + + {format(parseISO(historyDate), 'd MMMM yyyy', { locale: ru })} + +
+ + {historyLoading ? ( +
+ +
+ ) : historyTasks.length === 0 ? ( +
+ Завершённых задач за этот день нет +
+ ) : ( +
+ {historyTasks.map(task => ( + + ))} +
+ )} + + )} + {/* ── Plans tab ── */} {activeTab === 'plans' && (
@@ -444,11 +521,11 @@ function TaskCard({ task, onStatusChange, onReport }: { } const sev = task.maintenanceSeverity ? SEVERITY_CONFIG[task.maintenanceSeverity] : null + const taskAny = task as unknown as Record return (
@@ -468,9 +545,14 @@ function TaskCard({ task, onStatusChange, onReport }: {
- - {TYPE_LABELS[task.type]} + + {TYPE_LABELS[task.type] ?? task.type} + {task.dueDate && ( + + до {format(parseISO(task.dueDate), 'd MMM', { locale: ru })} + + )}
{task.notes && ( @@ -492,19 +574,14 @@ function TaskCard({ task, onStatusChange, onReport }: {
)} - {task.assignedToName && ( + {/* Assignee */} + {(task.assignedToName || taskAny.assigneeName) && (
- {task.assignedToName} + {task.assignedToName || taskAny.assigneeName}
)} - {task.completedAt && ( -

- Завершено в {new Date(task.completedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })} -

- )} - {reportOpen && (
@@ -578,14 +655,6 @@ function TaskCard({ task, onStatusChange, onReport }: { Завершить )} - {task.status === 'done' && ( - - )} {!reportOpen && (
) } + +function HistoryCard({ task }: { task: HousekeepingTask }) { + const taskAny = task as unknown as Record + const completedTime = task.completedAt + ? new Date(task.completedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) + : null + + return ( +
+
+ +
+
+ №{task.roomNumber} + + {TYPE_LABELS[task.type] ?? task.type} + + + {PRIORITY_LABELS[task.priority] ?? task.priority} + +
+ {task.notes && ( +

{task.notes}

+ )} +
+ {(task.assignedToName || taskAny.assigneeName) && ( + + + {task.assignedToName || taskAny.assigneeName} + + )} + {completedTime && ( + + + Завершено в {completedTime} + + )} +
+
+
+
+ ) +}