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:
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Wrench, Ban, CheckCircle, Sparkles, ClipboardList, Pencil, ChevronRight, X as XIcon, CalendarDays, AlertTriangle, Zap } from 'lucide-react'
|
||||
import { Wrench, Ban, CheckCircle, Sparkles, ClipboardList, Pencil, ChevronRight, X as XIcon, CalendarDays, AlertTriangle, Zap, Camera, Loader2 } from 'lucide-react'
|
||||
import { format } from 'date-fns'
|
||||
import type { Room, RoomStatus, HousekeepingStatus } from '../../types'
|
||||
import { cn } from '../../lib/utils'
|
||||
import { api } from '../../lib/api'
|
||||
|
||||
const today = () => format(new Date(), 'yyyy-MM-dd')
|
||||
|
||||
@@ -16,7 +17,7 @@ interface RoomContextMenuProps {
|
||||
onClose: () => void
|
||||
onStatusChange: (roomId: string, status: RoomStatus, maintenanceFrom?: string | null, maintenanceTo?: string | null) => void
|
||||
onHkStatusChange: (roomId: string, status: HousekeepingStatus, priority?: HkPriority) => void
|
||||
onMaintenanceTask?: (roomId: string, description: string, priority: HkPriority) => void
|
||||
onMaintenanceTask?: (roomId: string, description: string, priority: HkPriority, photos: string[]) => void
|
||||
onEdit?: (room: Room) => void
|
||||
}
|
||||
|
||||
@@ -43,6 +44,9 @@ export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatu
|
||||
const [pendingHkStatus, setPendingHkStatus] = useState<HousekeepingStatus | null>(null)
|
||||
const [techDesc, setTechDesc] = useState('')
|
||||
const [techPriority, setTechPriority] = useState<HkPriority>('medium')
|
||||
const [techPhotos, setTechPhotos] = useState<string[]>([])
|
||||
const [photoUploading, setPhotoUploading] = useState(false)
|
||||
const photoInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
@@ -98,9 +102,19 @@ export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatu
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleTechPhotoUpload = async (file: File) => {
|
||||
setPhotoUploading(true)
|
||||
try {
|
||||
const url = await api.upload.photo(file, 'tasks')
|
||||
setTechPhotos(prev => [...prev, url])
|
||||
} catch { /* ignore */ } finally {
|
||||
setPhotoUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTechSubmit = () => {
|
||||
if (onMaintenanceTask && techDesc.trim()) {
|
||||
onMaintenanceTask(room.id, techDesc.trim(), techPriority)
|
||||
onMaintenanceTask(room.id, techDesc.trim(), techPriority, techPhotos)
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
@@ -274,10 +288,38 @@ export function RoomContextMenu({ room, x, y, onClose, onStatusChange, onHkStatu
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Photo upload */}
|
||||
<div>
|
||||
<input
|
||||
ref={photoInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={e => { const f = e.target.files?.[0]; if (f) { handleTechPhotoUpload(f); e.target.value = '' } }}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{techPhotos.map((url, i) => (
|
||||
<div key={i} className="relative group">
|
||||
<img src={url} alt="" className="w-10 h-10 object-cover rounded border border-slate-200 dark:border-slate-600" />
|
||||
<button
|
||||
onClick={() => setTechPhotos(prev => prev.filter((_, j) => j !== i))}
|
||||
className="absolute -top-1 -right-1 w-3.5 h-3.5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
><XIcon size={8} /></button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => photoInputRef.current?.click()}
|
||||
disabled={photoUploading}
|
||||
className="w-10 h-10 rounded border-2 border-dashed border-slate-300 dark:border-slate-600 flex items-center justify-center text-slate-400 hover:border-brand-400 hover:text-brand-500 transition-colors"
|
||||
>
|
||||
{photoUploading ? <Loader2 size={12} className="animate-spin" /> : <Camera size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1.5 pt-0.5">
|
||||
<button
|
||||
onClick={handleTechSubmit}
|
||||
disabled={!techDesc.trim()}
|
||||
disabled={!techDesc.trim() || photoUploading}
|
||||
className="flex-1 py-1 rounded-lg text-xs font-medium bg-brand-600 text-white hover:bg-brand-700 transition-colors disabled:opacity-40"
|
||||
>
|
||||
Создать задачу
|
||||
|
||||
Reference in New Issue
Block a user