feat: housekeeper can leave completion comment when finishing a cleaning task

This commit is contained in:
2026-03-24 12:24:49 +03:00
parent de43eca9ff
commit 68e05895e3

View File

@@ -171,9 +171,11 @@ export function HousekeepingPage() {
} }
} }
const updateStatus = async (id: string, status: HousekeepingTask['status']) => { const updateStatus = async (id: string, status: HousekeepingTask['status'], resolutionNotes?: string) => {
try { try {
const updated = await api.housekeeping.update(slug, id, { status }) const payload: Record<string, unknown> = { status }
if (resolutionNotes) payload.resolution_notes = resolutionNotes
const updated = await api.housekeeping.update(slug, id, payload)
if (updated.status === 'done') { if (updated.status === 'done') {
setActiveTasks(prev => prev.filter(t => t.id !== id)) setActiveTasks(prev => prev.filter(t => t.id !== id))
if (activeTab === 'history' && updated.completedAt?.startsWith(historyDate)) { if (activeTab === 'history' && updated.completedAt?.startsWith(historyDate)) {
@@ -582,7 +584,7 @@ const SEVERITY_CONFIG = {
function TaskCard({ task, onStatusChange, onReport }: { function TaskCard({ task, onStatusChange, onReport }: {
task: HousekeepingTask task: HousekeepingTask
onStatusChange: (id: string, status: HousekeepingTask['status']) => void onStatusChange: (id: string, status: HousekeepingTask['status'], resolutionNotes?: string) => void
onReport: (id: string, note: string, severity: 'low' | 'medium' | 'high', photos: string[]) => void onReport: (id: string, note: string, severity: 'low' | 'medium' | 'high', photos: string[]) => void
}) { }) {
const [reportOpen, setReportOpen] = useState(false) const [reportOpen, setReportOpen] = useState(false)
@@ -591,6 +593,8 @@ function TaskCard({ task, onStatusChange, onReport }: {
const [reportPhotos, setReportPhotos] = useState<string[]>([]) const [reportPhotos, setReportPhotos] = useState<string[]>([])
const [photoUploading, setPhotoUploading] = useState(false) const [photoUploading, setPhotoUploading] = useState(false)
const photoInputRef = useRef<HTMLInputElement>(null) const photoInputRef = useRef<HTMLInputElement>(null)
const [completingOpen, setCompletingOpen] = useState(false)
const [completionComment, setCompletionComment] = useState('')
const uploadReportPhoto = async (file: File) => { const uploadReportPhoto = async (file: File) => {
setPhotoUploading(true) setPhotoUploading(true)
@@ -755,6 +759,42 @@ function TaskCard({ task, onStatusChange, onReport }: {
</div> </div>
)} )}
{completingOpen && (
<div className="border-t border-slate-100 dark:border-slate-700 pt-2.5 space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold text-slate-700 dark:text-slate-300 flex items-center gap-1">
<CheckCircle2 size={11} className="text-emerald-500" /> Завершение уборки
</p>
<button onClick={() => setCompletingOpen(false)} className="text-slate-400 hover:text-slate-600">
<XIcon size={13} />
</button>
</div>
<textarea
autoFocus
className="w-full text-xs border border-slate-200 dark:border-slate-600 rounded-lg p-2 bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 resize-none focus:outline-none focus:border-emerald-400"
rows={2}
placeholder="Комментарий (необязательно)..."
value={completionComment}
onChange={e => setCompletionComment(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && e.ctrlKey) {
onStatusChange(task.id, 'done', completionComment.trim() || undefined)
setCompletingOpen(false)
}
}}
/>
<button
onClick={() => {
onStatusChange(task.id, 'done', completionComment.trim() || undefined)
setCompletingOpen(false)
}}
className="w-full flex items-center justify-center gap-1 text-xs py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-medium transition-colors"
>
<CheckCircle2 size={11} /> Завершить уборку
</button>
</div>
)}
<div className="flex gap-1.5 pt-1"> <div className="flex gap-1.5 pt-1">
{task.status === 'pending' && ( {task.status === 'pending' && (
<button <button
@@ -764,15 +804,15 @@ function TaskCard({ task, onStatusChange, onReport }: {
Начать Начать
</button> </button>
)} )}
{task.status === 'in_progress' && ( {task.status === 'in_progress' && !completingOpen && (
<button <button
onClick={() => onStatusChange(task.id, 'done')} onClick={() => { setCompletingOpen(true); setReportOpen(false) }}
className="flex-1 text-xs py-1.5 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300 font-medium hover:bg-emerald-100 dark:hover:bg-emerald-900/30 transition-colors" className="flex-1 text-xs py-1.5 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300 font-medium hover:bg-emerald-100 dark:hover:bg-emerald-900/30 transition-colors"
> >
Завершить Завершить
</button> </button>
)} )}
{!reportOpen && ( {!reportOpen && !completingOpen && (
<button <button
onClick={() => setReportOpen(true)} onClick={() => setReportOpen(true)}
title="Сообщить о неисправности" title="Сообщить о неисправности"
@@ -828,6 +868,12 @@ function HistoryCard({ task }: { task: HousekeepingTask }) {
{task.notes} {task.notes}
</p> </p>
)} )}
{task.resolutionNotes && (
<p className="text-xs text-emerald-700 dark:text-emerald-300 mt-1.5 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg px-2 py-1.5 flex items-start gap-1.5">
<CheckCircle2 size={11} className="shrink-0 mt-0.5" />
{task.resolutionNotes}
</p>
)}
<div className="flex items-center gap-3 mt-1.5 text-xs text-slate-500 dark:text-slate-400 flex-wrap"> <div className="flex items-center gap-3 mt-1.5 text-xs text-slate-500 dark:text-slate-400 flex-wrap">
{(task.assignedToName || taskAny.assigneeName) && ( {(task.assignedToName || taskAny.assigneeName) && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">