feat: task problem/solution split + maintenance icons on rooms/calendar

- TechnicalPage: redesign task cards with separate "Задача" and "Решение" sections
  - "Задача": problem description + problem photos (editable before done)
  - "Решение": technician resolution comment + resolution photos (green section)
  - Resolution comment in CompletionModal now saves to resolution_notes field
  - Resolution photos save to resolution_photos (no longer mixed with problem photos)
- RoomModal history tab: maintenance entries show separate problem/resolution sections
- Backend: add resolution_notes, resolution_photos to PATCH allowed fields
- Migration 029: resolution_notes TEXT + resolution_photos TEXT[] columns
- RoomsPage: load active maintenance tasks, show 🔧 неисправность badge on room cards
- CalendarPage: load active maintenance tasks, show 🔧 неиспр. label in calendar row
- BookingCalendar: activeMaintenance prop, WS handler updates maintenance set on task changes
- CalendarPage WS: handle housekeeping_task_created to update activeMaintenance set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 11:45:24 +03:00
parent 14f0b8e0f2
commit c83a9ca714
10 changed files with 237 additions and 107 deletions

View File

@@ -17,6 +17,7 @@ export function RoomsPage() {
const [rooms, setRooms] = useState<Room[]>([])
const [categories, setCategories] = useState<RoomCategory[]>([])
const [loading, setLoading] = useState(true)
const [activeMaintenance, setActiveMaintenance] = useState<Set<string>>(new Set())
const [search, setSearch] = useState('')
const [floorFilter, setFloorFilter] = useState<number | 'all'>('all')
const [modalOpen, setModalOpen] = useState(false)
@@ -38,6 +39,16 @@ export function RoomsPage() {
.then(([r, cats]) => { setRooms(r); setCategories(cats) })
.catch(console.error)
.finally(() => setLoading(false))
api.housekeeping.list(slug, { status: 'active', category: 'maintenance' })
.then(tasks => {
const ids = new Set(tasks.map(t => {
const tAny = t as unknown as Record<string, string>
return tAny.roomId ?? tAny.room_id ?? ''
}).filter(Boolean))
setActiveMaintenance(ids)
})
.catch(() => {})
}, [slug])
const floors = [...new Set(rooms.map(r => r.floor))].sort()
@@ -228,6 +239,7 @@ export function RoomsPage() {
<RoomCard
key={room.id}
room={room}
hasActiveMaintenance={activeMaintenance.has(room.id)}
onEdit={() => openEdit(room)}
onContextMenu={(e) => { e.preventDefault(); setCtxMenu({ room, x: e.clientX, y: e.clientY }) }}
/>
@@ -273,7 +285,7 @@ export function RoomsPage() {
)
}
function RoomCard({ room, onEdit, onContextMenu }: { room: Room; onEdit: () => void; onContextMenu: (e: React.MouseEvent) => void }) {
function RoomCard({ room, hasActiveMaintenance, onEdit, onContextMenu }: { room: Room; hasActiveMaintenance?: boolean; onEdit: () => void; onContextMenu: (e: React.MouseEvent) => void }) {
return (
<div
className="card p-4 hover:shadow-card-hover transition-shadow cursor-pointer group relative"
@@ -285,11 +297,18 @@ function RoomCard({ room, onEdit, onContextMenu }: { room: Room; onEdit: () => v
>
<Pencil size={13} />
</button>
{room.status === 'maintenance' && (
<div className="absolute top-3 left-3">
<span className="flex items-center gap-1 text-[10px] font-medium text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20 px-1.5 py-0.5 rounded-md border border-orange-200 dark:border-orange-800">
<Wrench size={10} />ремонт
</span>
{(room.status === 'maintenance' || hasActiveMaintenance) && (
<div className="absolute top-3 left-3 flex gap-1">
{room.status === 'maintenance' && (
<span className="flex items-center gap-1 text-[10px] font-medium text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20 px-1.5 py-0.5 rounded-md border border-orange-200 dark:border-orange-800">
<Wrench size={10} />ремонт
</span>
)}
{hasActiveMaintenance && room.status !== 'maintenance' && (
<span title="Есть активная тех. задача" className="flex items-center gap-1 text-[10px] font-medium text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 px-1.5 py-0.5 rounded-md border border-red-200 dark:border-red-800">
<Wrench size={10} />неисправность
</span>
)}
</div>
)}