fix: room number + photos in task cards; time tracking for tasks
- Backend POST housekeeping: include photos in INSERT, return task with room JOIN - Backend PATCH housekeeping: set started_at on in_progress, return task with room JOIN - WS broadcasts now include room_number/assignee_name from JOIN - Frontend: normalizeTask() helper maps snake_case WS fields to camelCase - TechnicalPage: fix room number display (roomNumber || room_number) - TechnicalPage: show time spent (started_at → completed_at) on task cards - HousekeepingPage: history tab now includes maintenance reports (no category filter) - HousekeepingPage: HistoryCard shows photos, notes, time spent, maintenance styling - Migration 028: started_at TIMESTAMPTZ column on housekeeping_tasks - types: add startedAt? to HousekeepingTask Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,18 @@ import type { HousekeepingTask } from '../types'
|
||||
import { cn } from '../lib/utils'
|
||||
import { Modal } from '../components/ui/Modal'
|
||||
|
||||
function normalizeTask(raw: Record<string, unknown>): Record<string, unknown> {
|
||||
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<string, { label: string; color: string }> = {
|
||||
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' },
|
||||
@@ -52,14 +64,14 @@ export function TechnicalPage() {
|
||||
|
||||
const handleWsMessage = useCallback((msg: WsMessage) => {
|
||||
if (msg.type === 'housekeeping_task_created') {
|
||||
const task = msg.task as unknown as HousekeepingTask & { category?: string }
|
||||
const task = normalizeTask(msg.task as Record<string, unknown>)
|
||||
if (task.category === 'maintenance') {
|
||||
setTasks(prev => prev.some(t => t.id === task.id) ? prev : [task, ...prev])
|
||||
setTasks(prev => prev.some(t => t.id === task.id) ? prev : [task as unknown as HousekeepingTask, ...prev])
|
||||
}
|
||||
} else if (msg.type === 'housekeeping_updated') {
|
||||
const task = msg.task as unknown as HousekeepingTask & { category?: string }
|
||||
const task = normalizeTask(msg.task as Record<string, unknown>)
|
||||
if (task.category === 'maintenance') {
|
||||
setTasks(prev => prev.map(t => t.id === task.id ? task : t))
|
||||
setTasks(prev => prev.map(t => t.id === task.id ? task as unknown as HousekeepingTask : t))
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
@@ -169,12 +181,18 @@ export function TechnicalPage() {
|
||||
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.roomNumber}`
|
||||
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<string, string>).started_at
|
||||
const completedAt = task.completedAt ?? (taskRecord as Record<string, string>).completed_at
|
||||
const durationMin = startedAt && completedAt
|
||||
? Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 60000)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div key={task.id} className="card p-4 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -190,7 +208,7 @@ export function TechnicalPage() {
|
||||
{pMeta.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-slate-500 dark:text-slate-400 flex-wrap">
|
||||
<span>{roomLabel}</span>
|
||||
{task.dueDate && (
|
||||
<span>{format(new Date(task.dueDate), 'd MMM', { locale: ru })}</span>
|
||||
@@ -198,6 +216,14 @@ export function TechnicalPage() {
|
||||
{taskRecord.assigneeName && (
|
||||
<span>→ {taskRecord.assigneeName}</span>
|
||||
)}
|
||||
{durationMin !== null && durationMin > 0 && (
|
||||
<span className="flex items-center gap-1 text-slate-400">
|
||||
<Clock size={10} />
|
||||
{durationMin < 60
|
||||
? `${durationMin} мин`
|
||||
: `${Math.floor(durationMin / 60)} ч ${durationMin % 60} мин`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user