import { useState, useEffect, useCallback, useRef } from 'react' import { Plus, Wrench, Clock, CheckCircle2, AlertTriangle, Trash2, Camera, X as XIcon, Loader2 } from 'lucide-react' import { format } from 'date-fns' import { ru } from 'date-fns/locale' import { api } from '../lib/api' import { useAuth } from '../contexts/AuthContext' import { useHotelSocket } from '../hooks/useHotelSocket' import type { WsMessage } from '../hooks/useHotelSocket' import type { HousekeepingTask } from '../types' import { cn } from '../lib/utils' import { Modal } from '../components/ui/Modal' function normalizeTask(raw: Record): Record { return { ...raw, roomNumber: raw.roomNumber ?? raw.room_number, roomId: raw.roomId ?? raw.room_id, assigneeName: raw.assigneeName ?? raw.assignee_name, dueDate: raw.dueDate ?? raw.due_date, completedAt: raw.completedAt ?? raw.completed_at, startedAt: raw.startedAt ?? raw.started_at, } } const PRIORITY_LABELS: Record = { urgent: { label: 'Срочно', color: 'text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 border border-red-200 dark:border-red-800' }, medium: { label: 'Средний', color: 'text-yellow-600 bg-yellow-50 dark:bg-yellow-900/20 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800' }, low: { label: 'Низкий', color: 'text-slate-500 bg-slate-100 dark:bg-slate-700 dark:text-slate-400 border border-slate-200 dark:border-slate-600' }, } const STATUS_LABELS: Record = { pending: { label: 'Ожидает', icon: Clock, color: 'text-slate-500' }, in_progress: { label: 'В работе', icon: Wrench, color: 'text-blue-600' }, done: { label: 'Завершено', icon: CheckCircle2, color: 'text-emerald-600' }, cancelled: { label: 'Отменено', icon: AlertTriangle, color: 'text-slate-400' }, } export function TechnicalPage() { const { user, session } = useAuth() const slug = user?.hotelSlug ?? '' const [tasks, setTasks] = useState([]) const [filterStatus, setFilterStatus] = useState('active') const [showModal, setShowModal] = useState(false) const [uploadingFor, setUploadingFor] = useState(null) const [completingTask, setCompletingTask] = useState(null) const [requirePhoto, setRequirePhoto] = useState(false) const load = useCallback(() => { if (!slug) return api.housekeeping.list(slug, { category: 'maintenance' }) .then(setTasks) .catch(console.error) }, [slug]) useEffect(() => { load() }, [load]) useEffect(() => { if (!slug) return api.housekeeping.getSettings(slug) .then(s => setRequirePhoto(s.require_completion_photo ?? false)) .catch(() => {}) }, [slug]) const handleWsMessage = useCallback((msg: WsMessage) => { if (msg.type === 'housekeeping_task_created') { const task = normalizeTask(msg.task as Record) if (task.category === 'maintenance') { setTasks(prev => prev.some(t => t.id === task.id) ? prev : [task as unknown as HousekeepingTask, ...prev]) } } else if (msg.type === 'housekeeping_updated') { const task = normalizeTask(msg.task as Record) if (task.category === 'maintenance') { setTasks(prev => prev.map(t => t.id === task.id ? task as unknown as HousekeepingTask : t)) } } }, []) useHotelSocket({ slug, token: session?.token ?? null, onMessage: handleWsMessage }) const handleStatusChange = async (taskId: string, status: string) => { try { const updated = await api.housekeeping.update(slug, taskId, { status }) setTasks(prev => prev.map(t => t.id === taskId ? updated : t)) } catch (err) { console.error(err) } } const handleDelete = async (taskId: string) => { if (!confirm('Удалить задачу?')) return try { await api.housekeeping.delete(slug, taskId) setTasks(prev => prev.filter(t => t.id !== taskId)) } catch (err) { console.error(err) } } const handlePhotoUpload = async (taskId: string, file: File) => { setUploadingFor(taskId) try { const url = await api.upload.photo(file, 'tasks') const task = tasks.find(t => t.id === taskId) const currentPhotos = task?.photos ?? [] const updated = await api.housekeeping.update(slug, taskId, { photos: [...currentPhotos, url] }) setTasks(prev => prev.map(t => t.id === taskId ? updated : t)) } catch (err) { console.error('Photo upload failed:', err) } finally { setUploadingFor(null) } } const handlePhotoRemove = async (taskId: string, photoUrl: string) => { const task = tasks.find(t => t.id === taskId) if (!task) return const newPhotos = (task.photos ?? []).filter(p => p !== photoUrl) try { const updated = await api.housekeeping.update(slug, taskId, { photos: newPhotos }) setTasks(prev => prev.map(t => t.id === taskId ? updated : t)) } catch (err) { console.error(err) } } const filtered = tasks.filter(t => { const s = t.status as string if (filterStatus === 'active') return s !== 'done' && s !== 'cancelled' if (filterStatus === 'done') return s === 'done' return true }) return (

Технические задачи

Заявки для технического персонала

{/* Filters */}
{[ { id: 'active', label: 'Активные' }, { id: 'done', label: 'Выполненные' }, { id: 'all', label: 'Все' }, ].map(f => ( ))}
{/* Task list */}
{filtered.length === 0 ? (

Нет технических задач

) : filtered.map(task => { const taskRecord = task as unknown as Record const pMeta = PRIORITY_LABELS[taskRecord.priority ?? 'medium'] ?? PRIORITY_LABELS.medium const sMeta = STATUS_LABELS[task.status ?? 'pending'] const SIcon = sMeta.icon const roomLabel = (taskRecord.roomNumber || taskRecord.room_number) ? `Номер ${taskRecord.roomNumber ?? taskRecord.room_number}` : 'Без номера' const photos = task.photos ?? [] const isUploading = uploadingFor === task.id const startedAt = task.startedAt ?? (taskRecord as Record).started_at const completedAt = task.completedAt ?? (taskRecord as Record).completed_at const durationMin = startedAt && completedAt ? Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 60000) : null return (
{task.notes ?? task.type ?? 'Задача'} {pMeta.label}
{roomLabel} {task.dueDate && ( {format(new Date(task.dueDate), 'd MMM', { locale: ru })} )} {taskRecord.assigneeName && ( → {taskRecord.assigneeName} )} {durationMin !== null && durationMin > 0 && ( {durationMin < 60 ? `${durationMin} мин` : `${Math.floor(durationMin / 60)} ч ${durationMin % 60} мин`} )}
{/* Photos */} {(photos.length > 0 || true) && (
{photos.map(url => (
))} handlePhotoUpload(task.id, file)} />
)}
) })}
{showModal && ( setShowModal(false)} onCreated={task => { setTasks(prev => [task, ...prev]) setShowModal(false) }} /> )} {completingTask && ( setCompletingTask(null)} onConfirm={async (completionPhotos) => { const task = tasks.find(t => t.id === completingTask) const currentPhotos = task?.photos ?? [] const allPhotos = [...currentPhotos, ...completionPhotos] const updated = await api.housekeeping.update(slug, completingTask, { status: 'done', photos: allPhotos, }) setTasks(prev => prev.map(t => t.id === completingTask ? updated : t)) setCompletingTask(null) }} /> )}
) } function PhotoUploadButton({ isUploading, onFile }: { isUploading: boolean; onFile: (f: File) => void }) { const ref = useRef(null) return ( <> { const f = e.target.files?.[0]; if (f) { onFile(f); e.target.value = '' } }} /> ) } function TaskCreateModal({ slug, onClose, onCreated, }: { slug: string onClose: () => void onCreated: (t: HousekeepingTask) => void }) { const [form, setForm] = useState({ notes: '', priority: 'medium', room_id: '', due_date: format(new Date(), 'yyyy-MM-dd'), assignee_id: '', }) const [rooms, setRooms] = useState<{ id: string; number: string }[]>([]) const [users, setUsers] = useState<{ id: string; name: string }[]>([]) const [saving, setSaving] = useState(false) useEffect(() => { api.rooms.list(slug).then(r => setRooms(r.map(rm => ({ id: rm.id, number: rm.number })))).catch(() => {}) api.users.list(slug).then(u => setUsers(u)).catch(() => {}) }, [slug]) const handleSave = async () => { if (!form.notes.trim()) return setSaving(true) try { const task = await api.housekeeping.create(slug, { room_id: form.room_id || undefined, type: 'maintenance', priority: form.priority, notes: form.notes, due_date: form.due_date || undefined, assignee_id: form.assignee_id || undefined, category: 'maintenance', }) onCreated(task) } catch (err) { console.error(err) } finally { setSaving(false) } } return (