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

@@ -27,6 +27,8 @@ export function CalendarPage() {
const [priceOverrides, setPriceOverrides] = useState<Record<string, Record<string, number>>>({})
// roomId → assigneeName for "убирается" tooltip
const [cleaningAssignees, setCleaningAssignees] = useState<Record<string, string>>({})
// Set of room IDs with active maintenance tasks
const [activeMaintenance, setActiveMaintenance] = useState<Set<string>>(new Set())
useEffect(() => {
if (!slug) return
@@ -55,6 +57,17 @@ export function CalendarPage() {
}
}).catch(console.error)
// Fetch active maintenance tasks
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(() => {})
// Fetch active HK tasks to build "who is cleaning" tooltip map
api.housekeeping.list(slug, { status: 'active', category: 'housekeeping' })
.then(tasks => {
@@ -90,8 +103,23 @@ export function CalendarPage() {
if (msg.roomStatus !== 'cleaning') {
setCleaningAssignees(prev => { const n = { ...prev }; delete n[msg.roomId]; return n })
}
} else if (msg.type === 'housekeeping_task_created') {
const t = msg.task as Record<string, string>
const roomId = t.roomId ?? t.room_id
if (t.category === 'maintenance' && roomId) {
setActiveMaintenance(prev => new Set([...prev, roomId]))
}
} else if (msg.type === 'housekeeping_updated') {
const t = msg.task as Record<string, string>
const roomId = t.roomId ?? t.room_id
// Track maintenance tasks
if (t.category === 'maintenance' && roomId) {
if (t.status === 'done' || t.status === 'cancelled') {
setActiveMaintenance(prev => { const n = new Set(prev); n.delete(roomId); return n })
} else {
setActiveMaintenance(prev => new Set([...prev, roomId]))
}
}
if (t.roomId) {
if (t.status === 'in_progress' && t.assigneeName) {
setCleaningAssignees(prev => ({ ...prev, [t.roomId]: t.assigneeName }))
@@ -258,6 +286,7 @@ export function CalendarPage() {
onRoomUpdate={handleRoomUpdate}
onMaintenanceTaskCreate={handleMaintenanceTaskCreate}
cleaningAssignees={cleaningAssignees}
activeMaintenance={activeMaintenance}
fadingBookingIds={fadingBookings}
rentalObjects={isRentalActive ? rentalObjects as unknown as import('../data/rentalData').RentalObject[] : undefined}
rentalBookings={isRentalActive ? rentalBookings as unknown as RentalBooking[] : undefined}