diff --git a/src/pages/HousekeepingPage.tsx b/src/pages/HousekeepingPage.tsx index 49bc4cd..c2f3b22 100644 --- a/src/pages/HousekeepingPage.tsx +++ b/src/pages/HousekeepingPage.tsx @@ -371,7 +371,7 @@ export function HousekeepingPage() { ) : (
{historyTasks.map(task => ( - + ))}
)} @@ -1153,21 +1153,43 @@ function TaskCard({ task, onStatusChange, onReport, slug }: { ) } -function HistoryCard({ task }: { task: HousekeepingTask }) { +function HistoryCard({ task, slug }: { task: HousekeepingTask; slug: string }) { const taskAny = task as unknown as Record const isMaintenance = task.type === 'maintenance' || (taskAny.category === 'maintenance') const isActive = task.status === 'pending' || task.status === 'in_progress' + const [expanded, setExpanded] = useState(false) + const [detailLoading, setDetailLoading] = useState(false) + const [checklist, setChecklist] = useState(null) + const [consumptions, setConsumptions] = useState(null) + + const toggleExpand = async () => { + if (!expanded && checklist === null) { + setDetailLoading(true) + try { + const [cl, cons] = await Promise.all([ + api.checklists.getTaskChecklist(slug, task.id), + api.minibar.getTaskConsumptions(slug, task.id), + ]) + setChecklist(cl) + setConsumptions(cons) + } catch { /* ignore */ } finally { + setDetailLoading(false) + } + } + setExpanded(e => !e) + } + const completedTime = task.completedAt ? new Date(task.completedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) : null - // Duration: started_at → completed_at const durationMin = task.startedAt && task.completedAt ? Math.round((new Date(task.completedAt).getTime() - new Date(task.startedAt).getTime()) / 60000) : null const photos = task.photos ?? [] + const minibarTotal = consumptions?.reduce((s, c) => s + Number(c.pricePerUnit) * c.quantity, 0) ?? 0 return (
@@ -1228,20 +1250,88 @@ function HistoryCard({ task }: { task: HousekeepingTask }) { : `${Math.floor(durationMin / 60)} ч ${durationMin % 60} мин`} )} + {task.minibarChecked && ( + + Минибар ✓ + + )}
{photos.length > 0 && (
{photos.map(url => ( - + ))}
)} + + {/* Expand button */} + + + {/* Expanded details */} + {expanded && !detailLoading && ( +
+ {/* Checklist */} + {checklist && checklist.items.length > 0 && (() => { + const done = checklist.items.filter(i => i.completedAt).length + const total = checklist.items.length + return ( +
+

+ + Чек-лист + + {done}/{total} + +

+ {checklist.items.map(item => ( +
+
+ {item.completedAt && } +
+ {item.text} +
+ ))} +
+ ) + })()} + {checklist && checklist.items.length === 0 && ( +

Чек-лист не настроен

+ )} + + {/* Minibar */} +
+

+ Минибар +

+ {consumptions && consumptions.length > 0 ? ( + <> + {consumptions.map(c => ( +
+ {c.itemName} × {c.quantity} + {(Number(c.pricePerUnit) * c.quantity).toFixed(0)} руб. +
+ ))} +
+ Итого + {minibarTotal.toFixed(0)} руб. +
+ + ) : ( +

{task.minibarChecked ? 'Проверен, расхода нет' : 'Нет данных'}

+ )} +
+
+ )} diff --git a/src/pages/MinibarSettingsPage.tsx b/src/pages/MinibarSettingsPage.tsx index 235ea77..a4e47c2 100644 --- a/src/pages/MinibarSettingsPage.tsx +++ b/src/pages/MinibarSettingsPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { Plus, Trash2, Pencil, Check, X, Loader2, ShoppingCart, ShieldAlert, ToggleLeft, ToggleRight } from 'lucide-react' +import { Plus, Trash2, Pencil, Check, X, Loader2, ShoppingCart, ShieldAlert, ToggleLeft, ToggleRight, Tag } from 'lucide-react' import { api, type MinibarItem } from '../lib/api' import { useAuth } from '../contexts/AuthContext' import { cn } from '../lib/utils' @@ -62,6 +62,8 @@ export function MinibarSettingsPage() { const [addForm, setAddForm] = useState({ name: '', price: '', category: '' }) const [addingRow, setAddingRow] = useState(false) const [requireMinibarCheck, setRequireMinibarCheck] = useState(false) + const [editingCat, setEditingCat] = useState(null) + const [editCatValue, setEditCatValue] = useState('') useEffect(() => { if (!slug) return @@ -87,6 +89,19 @@ export function MinibarSettingsPage() { } } + const renameCategory = async (oldName: string, newName: string) => { + const trimmed = newName.trim() + if (!trimmed || trimmed === oldName) { setEditingCat(null); return } + const toUpdate = items.filter(i => i.category === oldName) + try { + for (const item of toUpdate) { + await api.minibar.updateItem(slug, item.id, { name: item.name, price: item.price, category: trimmed }) + } + setItems(prev => prev.map(i => i.category === oldName ? { ...i, category: trimmed } : i)) + } catch { /* ignore */ } + setEditingCat(null) + } + const handleAdd = async (v: EditRow) => { if (!v.name.trim()) return setAddingRow(true) @@ -261,6 +276,50 @@ export function MinibarSettingsPage() { + {/* Category management */} + {items.length > 0 && (() => { + const cats = [...new Set(items.map(i => i.category).filter(Boolean) as string[])] + if (cats.length === 0) return null + return ( +
+
+ + Категории +
+
+ {cats.map(cat => ( +
+ {editingCat === cat ? ( + <> + setEditCatValue(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') renameCategory(cat, editCatValue); if (e.key === 'Escape') setEditingCat(null) }} + className="input flex-1 py-1 text-sm" + /> + + + + ) : ( + <> + {cat} + {items.filter(i => i.category === cat).length} поз. + + + )} +
+ ))} +
+
+ ) + })()} + {/* Require minibar check setting */}