fix: housekeeping tasks not appearing + add history tab + fix task type
- Fix root cause: CalendarPage created tasks with type='regular' which violated DB CHECK constraint (allowed: cleaning/turnover/inspection/maintenance/amenities) → INSERT silently failed. Fixed to type='cleaning'.
- Fix: HousekeepingPage now loads ALL active tasks (status=active = pending+in_progress) without date filter — tasks appear regardless of when they were created
- Backend: support ?status=active filter (translates to IN ('pending','in_progress'))
- Add "История" tab in HousekeepingPage: shows completed tasks for a selected date with prev/next navigation, assignee and completion time displayed
- WS handler: when task becomes done, move it from active list instead of keeping in place
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,11 @@ const housekeeping: FastifyPluginAsync = async (fastify) => {
|
|||||||
let idx = 2
|
let idx = 2
|
||||||
|
|
||||||
const { status, date, category } = request.query
|
const { status, date, category } = request.query
|
||||||
if (status) { conditions.push(`t.status = $${idx}`); values.push(status); idx++ }
|
if (status === 'active') {
|
||||||
|
conditions.push(`t.status IN ('pending', 'in_progress')`)
|
||||||
|
} else if (status) {
|
||||||
|
conditions.push(`t.status = $${idx}`); values.push(status); idx++
|
||||||
|
}
|
||||||
if (date) { conditions.push(`t.due_date = $${idx}`); values.push(date); idx++ }
|
if (date) { conditions.push(`t.due_date = $${idx}`); values.push(date); idx++ }
|
||||||
if (category) { conditions.push(`t.category = $${idx}`); values.push(category); idx++ }
|
if (category) { conditions.push(`t.category = $${idx}`); values.push(category); idx++ }
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function CalendarPage() {
|
|||||||
const today = new Date().toISOString().slice(0, 10)
|
const today = new Date().toISOString().slice(0, 10)
|
||||||
const task = await api.housekeeping.create(slug, {
|
const task = await api.housekeeping.create(slug, {
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
type: 'regular',
|
type: 'cleaning',
|
||||||
priority: priority ?? 'medium',
|
priority: priority ?? 'medium',
|
||||||
due_date: today,
|
due_date: today,
|
||||||
category: 'housekeeping',
|
category: 'housekeeping',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save, Wrench, X as XIcon, Send, AlertTriangle, BanIcon, Loader2 } from 'lucide-react'
|
import { CheckCircle2, Clock, Sparkles, User, ListChecks, Settings2, Save, Wrench, X as XIcon, Send, AlertTriangle, BanIcon, Loader2, History, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { useHotelSocket } from '../hooks/useHotelSocket'
|
import { useHotelSocket } from '../hooks/useHotelSocket'
|
||||||
import type { WsMessage } from '../hooks/useHotelSocket'
|
import type { WsMessage } from '../hooks/useHotelSocket'
|
||||||
@@ -9,7 +9,8 @@ import type { HousekeepingTask } from '../types'
|
|||||||
import { cn } from '../lib/utils'
|
import { cn } from '../lib/utils'
|
||||||
import { Badge } from '../components/ui/Badge'
|
import { Badge } from '../components/ui/Badge'
|
||||||
import { useNotifications } from '../contexts/NotificationsContext'
|
import { useNotifications } from '../contexts/NotificationsContext'
|
||||||
import { format } from 'date-fns'
|
import { format, subDays, addDays, parseISO } from 'date-fns'
|
||||||
|
import { ru } from 'date-fns/locale'
|
||||||
|
|
||||||
function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
|
function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
|
||||||
return (
|
return (
|
||||||
@@ -38,51 +39,61 @@ const PRIORITY_COLORS: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PRIORITY_LABELS: Record<string, string> = {
|
const PRIORITY_LABELS: Record<string, string> = {
|
||||||
urgent: 'Экстренно', high: 'Срочно', medium: 'Обычный', low: 'Низкий',
|
urgent: 'Срочно', high: 'Срочно', medium: 'Обычный', low: 'Низкий',
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_COLORS = {
|
const TYPE_COLORS: Record<string, string> = {
|
||||||
cleaning: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
cleaning: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||||
|
turnover: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
|
||||||
inspection: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
inspection: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
||||||
maintenance: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
|
maintenance: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_LABELS = {
|
const TYPE_LABELS: Record<string, string> = {
|
||||||
cleaning: 'Уборка', inspection: 'Проверка', maintenance: 'Ремонт',
|
cleaning: 'Уборка', turnover: 'Уборка при выезде', inspection: 'Проверка', maintenance: 'Ремонт',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HousekeepingPage() {
|
export function HousekeepingPage() {
|
||||||
const { user, session } = useAuth()
|
const { user, session } = useAuth()
|
||||||
const slug = user?.hotelSlug ?? ''
|
const slug = user?.hotelSlug ?? ''
|
||||||
|
|
||||||
const [tasks, setTasks] = useState<HousekeepingTask[]>([])
|
const [activeTasks, setActiveTasks] = useState<HousekeepingTask[]>([])
|
||||||
|
const [historyTasks, setHistoryTasks] = useState<HousekeepingTask[]>([])
|
||||||
|
const [historyDate, setHistoryDate] = useState(format(new Date(), 'yyyy-MM-dd'))
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [activeTab, setActiveTab] = useState<'tasks' | 'plans'>('tasks')
|
const [historyLoading, setHistoryLoading] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState<'tasks' | 'history' | 'plans'>('tasks')
|
||||||
const [planSaved, setPlanSaved] = useState(false)
|
const [planSaved, setPlanSaved] = useState(false)
|
||||||
|
|
||||||
const handleWsMessage = useCallback((msg: WsMessage) => {
|
const handleWsMessage = useCallback((msg: WsMessage) => {
|
||||||
if (msg.type === 'housekeeping_task_created') {
|
if (msg.type === 'housekeeping_task_created') {
|
||||||
const task = msg.task as unknown as HousekeepingTask & { category?: string }
|
const task = msg.task as unknown as HousekeepingTask & { category?: string }
|
||||||
if (task.category !== 'maintenance') {
|
if (task.category !== 'maintenance') {
|
||||||
setTasks(prev => prev.some(t => t.id === task.id) ? prev : [task, ...prev])
|
setActiveTasks(prev => prev.some(t => t.id === task.id) ? prev : [task, ...prev])
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'housekeeping_updated') {
|
} else if (msg.type === 'housekeeping_updated') {
|
||||||
const task = msg.task as unknown as HousekeepingTask & { category?: string }
|
const task = msg.task as unknown as HousekeepingTask & { category?: string }
|
||||||
if (task.category !== 'maintenance') {
|
if (task.category !== 'maintenance') {
|
||||||
setTasks(prev => prev.map(t => t.id === task.id ? task : t))
|
// If task is now done → remove from active, add to history if same date
|
||||||
|
if (task.status === 'done') {
|
||||||
|
setActiveTasks(prev => prev.filter(t => t.id !== task.id))
|
||||||
|
} else {
|
||||||
|
setActiveTasks(prev => prev.map(t => t.id === task.id ? task : t))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useHotelSocket({ slug, token: session?.token ?? null, onMessage: handleWsMessage })
|
useHotelSocket({ slug, token: session?.token ?? null, onMessage: handleWsMessage })
|
||||||
|
|
||||||
|
// Load active tasks (all pending/in_progress — no date filter)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!slug) return
|
if (!slug) return
|
||||||
Promise.all([
|
Promise.all([
|
||||||
api.housekeeping.list(slug, { date: format(new Date(), 'yyyy-MM-dd'), category: 'housekeeping' }),
|
api.housekeeping.list(slug, { status: 'active', category: 'housekeeping' }),
|
||||||
api.housekeeping.getSettings(slug).catch(() => null),
|
api.housekeeping.getSettings(slug).catch(() => null),
|
||||||
]).then(([t, s]) => {
|
]).then(([t, s]) => {
|
||||||
setTasks(t)
|
setActiveTasks(t)
|
||||||
if (s) setPlans(prev => ({
|
if (s) setPlans(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
checkoutAuto: s.checkout_auto,
|
checkoutAuto: s.checkout_auto,
|
||||||
@@ -93,6 +104,16 @@ export function HousekeepingPage() {
|
|||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [slug])
|
}, [slug])
|
||||||
|
|
||||||
|
// Load history when tab opens or date changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab !== 'history' || !slug) return
|
||||||
|
setHistoryLoading(true)
|
||||||
|
api.housekeeping.list(slug, { status: 'done', date: historyDate, category: 'housekeeping' })
|
||||||
|
.then(setHistoryTasks)
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => setHistoryLoading(false))
|
||||||
|
}, [activeTab, historyDate, slug])
|
||||||
|
|
||||||
// Cleaning plan settings
|
// Cleaning plan settings
|
||||||
const [plans, setPlans] = useState({
|
const [plans, setPlans] = useState({
|
||||||
checkoutAuto: true, checkoutPriority: 'high' as 'high' | 'medium',
|
checkoutAuto: true, checkoutPriority: 'high' as 'high' | 'medium',
|
||||||
@@ -127,7 +148,14 @@ export function HousekeepingPage() {
|
|||||||
const updateStatus = async (id: string, status: HousekeepingTask['status']) => {
|
const updateStatus = async (id: string, status: HousekeepingTask['status']) => {
|
||||||
try {
|
try {
|
||||||
const updated = await api.housekeeping.update(slug, id, { status })
|
const updated = await api.housekeeping.update(slug, id, { status })
|
||||||
setTasks(prev => prev.map(t => t.id === id ? updated : t))
|
if (updated.status === 'done') {
|
||||||
|
setActiveTasks(prev => prev.filter(t => t.id !== id))
|
||||||
|
if (activeTab === 'history' && updated.completedAt?.startsWith(historyDate)) {
|
||||||
|
setHistoryTasks(prev => [updated, ...prev])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setActiveTasks(prev => prev.map(t => t.id === id ? updated : t))
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update task:', err)
|
console.error('Failed to update task:', err)
|
||||||
}
|
}
|
||||||
@@ -136,9 +164,9 @@ export function HousekeepingPage() {
|
|||||||
const { addNotification } = useNotifications()
|
const { addNotification } = useNotifications()
|
||||||
|
|
||||||
const addMaintenanceReport = (id: string, note: string, severity: 'low' | 'medium' | 'high') => {
|
const addMaintenanceReport = (id: string, note: string, severity: 'low' | 'medium' | 'high') => {
|
||||||
const task = tasks.find(t => t.id === id)
|
const task = activeTasks.find(t => t.id === id)
|
||||||
const roomBlocked = severity === 'high'
|
const roomBlocked = severity === 'high'
|
||||||
setTasks(prev => prev.map(t =>
|
setActiveTasks(prev => prev.map(t =>
|
||||||
t.id === id ? { ...t, maintenanceNote: note, maintenanceSeverity: severity, roomBlocked } : t,
|
t.id === id ? { ...t, maintenanceNote: note, maintenanceSeverity: severity, roomBlocked } : t,
|
||||||
))
|
))
|
||||||
const severityLabel = severity === 'high' ? 'Экстренно' : severity === 'medium' ? 'Средняя срочность' : 'Не срочно'
|
const severityLabel = severity === 'high' ? 'Экстренно' : severity === 'medium' ? 'Средняя срочность' : 'Не срочно'
|
||||||
@@ -152,8 +180,8 @@ export function HousekeepingPage() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = tasks.length
|
const total = activeTasks.length
|
||||||
const done = tasks.filter(t => t.status === 'done').length
|
const done = activeTasks.filter(t => t.status === 'done').length
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -188,7 +216,8 @@ export function HousekeepingPage() {
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex border-b border-slate-200 dark:border-slate-700">
|
<div className="flex border-b border-slate-200 dark:border-slate-700">
|
||||||
{([
|
{([
|
||||||
{ id: 'tasks' as const, label: 'Задачи на сегодня', icon: ListChecks },
|
{ id: 'tasks' as const, label: 'Задачи', icon: ListChecks },
|
||||||
|
{ id: 'history' as const, label: 'История', icon: History },
|
||||||
{ id: 'plans' as const, label: 'Планы уборки', icon: Settings2 },
|
{ id: 'plans' as const, label: 'Планы уборки', icon: Settings2 },
|
||||||
]).map(t => (
|
]).map(t => (
|
||||||
<button
|
<button
|
||||||
@@ -210,14 +239,15 @@ export function HousekeepingPage() {
|
|||||||
{/* ── Tasks tab ── */}
|
{/* ── Tasks tab ── */}
|
||||||
{activeTab === 'tasks' && (
|
{activeTab === 'tasks' && (
|
||||||
<>
|
<>
|
||||||
{tasks.length === 0 ? (
|
{activeTasks.length === 0 ? (
|
||||||
<div className="text-center py-16 text-slate-400 dark:text-slate-500">
|
<div className="text-center py-16 text-slate-400 dark:text-slate-500">
|
||||||
Задач на сегодня нет
|
Активных задач нет
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{COLUMNS.map(col => {
|
{(['pending', 'in_progress'] as Column[]).map(colId => {
|
||||||
const colTasks = tasks.filter(t => t.status === col.id)
|
const col = COLUMNS.find(c => c.id === colId)!
|
||||||
|
const colTasks = activeTasks.filter(t => t.status === colId)
|
||||||
const Icon = col.icon
|
const Icon = col.icon
|
||||||
return (
|
return (
|
||||||
<div key={col.id} className="space-y-3">
|
<div key={col.id} className="space-y-3">
|
||||||
@@ -246,6 +276,53 @@ export function HousekeepingPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── History tab ── */}
|
||||||
|
{activeTab === 'history' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Date picker */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setHistoryDate(format(subDays(parseISO(historyDate), 1), 'yyyy-MM-dd'))}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-500"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={16} />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={historyDate}
|
||||||
|
onChange={e => setHistoryDate(e.target.value)}
|
||||||
|
className="input text-sm w-auto"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setHistoryDate(format(addDays(parseISO(historyDate), 1), 'yyyy-MM-dd'))}
|
||||||
|
disabled={historyDate >= format(new Date(), 'yyyy-MM-dd')}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-500 disabled:opacity-30"
|
||||||
|
>
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{format(parseISO(historyDate), 'd MMMM yyyy', { locale: ru })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{historyLoading ? (
|
||||||
|
<div className="flex justify-center py-12">
|
||||||
|
<Loader2 size={20} className="animate-spin text-brand-600" />
|
||||||
|
</div>
|
||||||
|
) : historyTasks.length === 0 ? (
|
||||||
|
<div className="text-center py-16 text-slate-400 dark:text-slate-500">
|
||||||
|
Завершённых задач за этот день нет
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{historyTasks.map(task => (
|
||||||
|
<HistoryCard key={task.id} task={task} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Plans tab ── */}
|
{/* ── Plans tab ── */}
|
||||||
{activeTab === 'plans' && (
|
{activeTab === 'plans' && (
|
||||||
<div className="max-w-2xl space-y-5">
|
<div className="max-w-2xl space-y-5">
|
||||||
@@ -444,11 +521,11 @@ function TaskCard({ task, onStatusChange, onReport }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sev = task.maintenanceSeverity ? SEVERITY_CONFIG[task.maintenanceSeverity] : null
|
const sev = task.maintenanceSeverity ? SEVERITY_CONFIG[task.maintenanceSeverity] : null
|
||||||
|
const taskAny = task as unknown as Record<string, string>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'card p-3.5 space-y-2.5 transition-all',
|
'card p-3.5 space-y-2.5 transition-all',
|
||||||
task.status === 'done' && 'opacity-70',
|
|
||||||
task.roomBlocked && 'ring-2 ring-red-400 dark:ring-red-600',
|
task.roomBlocked && 'ring-2 ring-red-400 dark:ring-red-600',
|
||||||
)}>
|
)}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -468,9 +545,14 @@ function TaskCard({ task, onStatusChange, onReport }: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1.5 flex-wrap">
|
<div className="flex gap-1.5 flex-wrap">
|
||||||
<Badge className={TYPE_COLORS[task.type]}>
|
<Badge className={TYPE_COLORS[task.type] ?? TYPE_COLORS.cleaning}>
|
||||||
{TYPE_LABELS[task.type]}
|
{TYPE_LABELS[task.type] ?? task.type}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{task.dueDate && (
|
||||||
|
<span className="text-[11px] text-slate-400 dark:text-slate-500 self-center">
|
||||||
|
до {format(parseISO(task.dueDate), 'd MMM', { locale: ru })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{task.notes && (
|
{task.notes && (
|
||||||
@@ -492,19 +574,14 @@ function TaskCard({ task, onStatusChange, onReport }: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{task.assignedToName && (
|
{/* Assignee */}
|
||||||
|
{(task.assignedToName || taskAny.assigneeName) && (
|
||||||
<div className="flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
|
<div className="flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
|
||||||
<User size={11} />
|
<User size={11} />
|
||||||
{task.assignedToName}
|
<span>{task.assignedToName || taskAny.assigneeName}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{task.completedAt && (
|
|
||||||
<p className="text-xs text-emerald-600 dark:text-emerald-400">
|
|
||||||
Завершено в {new Date(task.completedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{reportOpen && (
|
{reportOpen && (
|
||||||
<div className="border-t border-slate-100 dark:border-slate-700 pt-2.5 space-y-2">
|
<div className="border-t border-slate-100 dark:border-slate-700 pt-2.5 space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -578,14 +655,6 @@ function TaskCard({ task, onStatusChange, onReport }: {
|
|||||||
Завершить
|
Завершить
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{task.status === 'done' && (
|
|
||||||
<button
|
|
||||||
onClick={() => onStatusChange(task.id, 'pending')}
|
|
||||||
className="flex-1 text-xs py-1.5 rounded-lg bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-400 font-medium hover:bg-slate-100 dark:hover:bg-slate-600 transition-colors"
|
|
||||||
>
|
|
||||||
Вернуть
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!reportOpen && (
|
{!reportOpen && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setReportOpen(true)}
|
onClick={() => setReportOpen(true)}
|
||||||
@@ -604,3 +673,46 @@ function TaskCard({ task, onStatusChange, onReport }: {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HistoryCard({ task }: { task: HousekeepingTask }) {
|
||||||
|
const taskAny = task as unknown as Record<string, string>
|
||||||
|
const completedTime = task.completedAt
|
||||||
|
? new Date(task.completedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card p-3.5 opacity-90">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<CheckCircle2 size={16} className="text-emerald-500 shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-semibold text-slate-900 dark:text-slate-100">№{task.roomNumber}</span>
|
||||||
|
<Badge className={TYPE_COLORS[task.type] ?? TYPE_COLORS.cleaning}>
|
||||||
|
{TYPE_LABELS[task.type] ?? task.type}
|
||||||
|
</Badge>
|
||||||
|
<Badge className={PRIORITY_COLORS[task.priority] ?? PRIORITY_COLORS.medium}>
|
||||||
|
{PRIORITY_LABELS[task.priority] ?? task.priority}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{task.notes && (
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1 truncate">{task.notes}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-3 mt-1.5 text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{(task.assignedToName || taskAny.assigneeName) && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<User size={11} />
|
||||||
|
{task.assignedToName || taskAny.assigneeName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{completedTime && (
|
||||||
|
<span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
|
||||||
|
<CheckCircle2 size={11} />
|
||||||
|
Завершено в {completedTime}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user