feat: cleaning tooltip, report photos, fix tech task creation, completion photos

- Calendar: hover tooltip on '🧹 убирается' shows who is cleaning (assignee name)
  fetched from active HK tasks on mount, updated via WS housekeeping_updated events
- HousekeepingPage: 'Сообщить о неисправности' now creates a real maintenance task
  in the DB (category=maintenance) + sends WS so TechnicalPage receives it instantly;
  photo upload added to the report form
- RoomContextMenu: photo upload added to tech task creation form (camera button,
  preview thumbnails); photos passed through to API on task creation
- TechnicalPage: selecting 'Выполнено' now opens CompletionModal with photo upload;
  technician can attach completion photos before confirming; if require_completion_photo
  setting is on — at least one photo is required
- Settings (HousekeepingPage plans tab): new toggle 'Обязательное фото при завершении
  тех. задачи' saved to housekeeping_settings.require_completion_photo (migration 027)
- Backend: housekeeping-settings updated with require_completion_photo field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 11:07:28 +03:00
parent 66009d345f
commit 2ebef940b9
8 changed files with 310 additions and 37 deletions

View File

@@ -25,6 +25,8 @@ export function CalendarPage() {
const [rentalBookings, setRentalBookings] = useState<RentalBookingApi[]>([])
const [locks, setLocks] = useState<Map<string, BookingLock>>(new Map())
const [priceOverrides, setPriceOverrides] = useState<Record<string, Record<string, number>>>({})
// roomId → assigneeName for "убирается" tooltip
const [cleaningAssignees, setCleaningAssignees] = useState<Record<string, string>>({})
useEffect(() => {
if (!slug) return
@@ -52,6 +54,20 @@ export function CalendarPage() {
setRentalBookings(rb as RentalBookingApi[])
}
}).catch(console.error)
// Fetch active HK tasks to build "who is cleaning" tooltip map
api.housekeeping.list(slug, { status: 'active', category: 'housekeeping' })
.then(tasks => {
const assignees: Record<string, string> = {}
for (const t of tasks) {
const tAny = t as unknown as Record<string, string>
if (tAny.status === 'in_progress' && tAny.roomId && tAny.assigneeName) {
assignees[tAny.roomId] = tAny.assigneeName
}
}
setCleaningAssignees(assignees)
})
.catch(() => {})
}, [slug, isRentalActive])
const handleWsMessage = useCallback((msg: WsMessage) => {
@@ -70,6 +86,19 @@ export function CalendarPage() {
setRooms(prev => prev.map(r =>
r.id === msg.roomId ? { ...r, housekeepingStatus: msg.roomStatus as Room['housekeepingStatus'] } : r
))
// If room is no longer being cleaned, remove from tooltip map
if (msg.roomStatus !== 'cleaning') {
setCleaningAssignees(prev => { const n = { ...prev }; delete n[msg.roomId]; return n })
}
} else if (msg.type === 'housekeeping_updated') {
const t = msg.task as Record<string, string>
if (t.roomId) {
if (t.status === 'in_progress' && t.assigneeName) {
setCleaningAssignees(prev => ({ ...prev, [t.roomId]: t.assigneeName }))
} else if (t.status !== 'in_progress') {
setCleaningAssignees(prev => { const n = { ...prev }; delete n[t.roomId]; return n })
}
}
}
}, [])
@@ -174,7 +203,7 @@ export function CalendarPage() {
}
}, [slug, send])
const handleMaintenanceTaskCreate = useCallback(async (roomId: string, description: string, priority: HkPriority) => {
const handleMaintenanceTaskCreate = useCallback(async (roomId: string, description: string, priority: HkPriority, photos: string[] = []) => {
try {
const today = new Date().toISOString().slice(0, 10)
const task = await api.housekeeping.create(slug, {
@@ -184,6 +213,7 @@ export function CalendarPage() {
notes: description,
due_date: today,
category: 'maintenance',
photos,
})
send({ type: 'housekeeping_task_created', task: task as unknown as Record<string, unknown> })
} catch (err) {
@@ -227,6 +257,7 @@ export function CalendarPage() {
onBookingBulkUpdate={handleBulkUpdate}
onRoomUpdate={handleRoomUpdate}
onMaintenanceTaskCreate={handleMaintenanceTaskCreate}
cleaningAssignees={cleaningAssignees}
fadingBookingIds={fadingBookings}
rentalObjects={isRentalActive ? rentalObjects as unknown as import('../data/rentalData').RentalObject[] : undefined}
rentalBookings={isRentalActive ? rentalBookings as unknown as RentalBooking[] : undefined}