Add edit and delete to task modal

Edit button (update cap): inline form for title, description, priority, unusable flag, due date. Delete button (admin only): hard delete with confirm step, cascades photos/events and cleans files from disk. Backend adds DELETE /api/tasks/:id route.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-22 10:17:57 +00:00
parent 03d1cd2bb6
commit ca006f9aea
3 changed files with 118 additions and 5 deletions

View file

@ -75,6 +75,9 @@ export function createTask(body: Record<string, unknown>): Promise<Task> {
export function updateTask(id: number, body: Record<string, unknown>): Promise<Task> {
return request(`/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteTask(id: number): Promise<{ ok: boolean }> {
return request(`/tasks/${id}`, { method: 'DELETE' })
}
export function resolveTask(id: number, body: {
status: 'temporary_fix' | 'fixed'
completed_by?: string

View file

@ -1,12 +1,12 @@
import { useEffect, useState } from 'react'
import {
X, Camera, MessageSquare, ArrowRight, RotateCcw, PlusCircle,
CheckCircle2, Image as ImageIcon, PoundSterling, UserRound,
CheckCircle2, Image as ImageIcon, PoundSterling, UserRound, Pencil, Trash2,
} from 'lucide-react'
import type { TaskDetail, TaskStatus, TaskEvent, AuthUser } from '../types'
import { STATUS_LABELS, TRANSITIONS, can } from '../types'
import type { TaskDetail, TaskStatus, TaskEvent, AuthUser, Priority } from '../types'
import { STATUS_LABELS, TRANSITIONS, PRIORITIES, PRIORITY_LABELS, can } from '../types'
import {
fetchTask, updateTask, resolveTask, addComment, uploadTaskPhoto, deletePhoto,
fetchTask, updateTask, deleteTask, resolveTask, addComment, uploadTaskPhoto, deletePhoto,
photoUrl, fetchAssignableUsers,
} from '../api'
import { useAuth } from './AuthGate'
@ -51,6 +51,14 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
const [showReassign, setShowReassign] = useState(false)
const [assignment, setAssignment] = useState<Assignment | null>(null)
const [showEdit, setShowEdit] = useState(false)
const [editTitle, setEditTitle] = useState('')
const [editDescription, setEditDescription] = useState('')
const [editPriority, setEditPriority] = useState<Priority>('medium')
const [editUnusable, setEditUnusable] = useState(false)
const [editDueDate, setEditDueDate] = useState('')
const [confirmDelete, setConfirmDelete] = useState(false)
const [showResolve, setShowResolve] = useState<null | 'temporary_fix' | 'fixed'>(null)
const [resolveUsers, setResolveUsers] = useState<AuthUser[]>([])
const [completedBy, setCompletedBy] = useState('')
@ -77,6 +85,24 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
}
}, [showResolve])
function openEdit() {
if (!task) return
setEditTitle(task.title)
setEditDescription(task.description ?? '')
setEditPriority(task.priority)
setEditUnusable(task.unusable)
setEditDueDate(task.due_date ?? '')
setShowEdit(true)
}
async function doDelete() {
await run(async () => {
await deleteTask(task!.id)
onChanged()
onClose()
})
}
async function run(fn: () => Promise<unknown>) {
setBusy(true)
setError(null)
@ -149,6 +175,16 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
{task.template_id && <span className="badge badge-outline">Recurring</span>}
</div>
</div>
{canUpdate && (
<button className="btn btn-sm" onClick={openEdit} title="Edit task">
<Pencil size={13} strokeWidth={1.75} /> Edit
</button>
)}
{user.is_admin && (
<button className="btn btn-sm btn-danger" onClick={() => setConfirmDelete(true)} title="Delete task">
<Trash2 size={13} strokeWidth={1.75} />
</button>
)}
<button className="modal-close" onClick={onClose}><X size={18} strokeWidth={1.75} /></button>
</div>
@ -171,7 +207,64 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
{showCosts && task.cost != null && <span>Cost £{task.cost}{task.cost_notes ? ` (${task.cost_notes})` : ''}</span>}
</div>
{task.description && <p style={{ whiteSpace: 'pre-wrap', margin: '0 0 12px' }}>{task.description}</p>}
{!showEdit && task.description && <p style={{ whiteSpace: 'pre-wrap', margin: '0 0 12px' }}>{task.description}</p>}
{/* Edit form */}
{showEdit && (
<div className="card" style={{ marginBottom: 12 }}>
<div className="section-title" style={{ marginTop: 0 }}>Edit task</div>
<div className="field">
<label>Title</label>
<input type="text" value={editTitle} onChange={e => setEditTitle(e.target.value)} />
</div>
<div className="field">
<label>Description</label>
<textarea value={editDescription} onChange={e => setEditDescription(e.target.value)} style={{ minHeight: 60 }} />
</div>
<div className="field-row">
<div className="field">
<label>Priority</label>
<select value={editPriority} onChange={e => setEditPriority(e.target.value as Priority)}>
{PRIORITIES.map(p => <option key={p} value={p}>{PRIORITY_LABELS[p]}</option>)}
</select>
</div>
<div className="field">
<label>Due date</label>
<input type="date" value={editDueDate} onChange={e => setEditDueDate(e.target.value)} />
</div>
</div>
<label className="field-check">
<input type="checkbox" checked={editUnusable} onChange={e => setEditUnusable(e.target.checked)} />
Mark as unusable (room/area out of service)
</label>
<div className="modal-actions" style={{ marginTop: 10 }}>
<button className="btn btn-sm" onClick={() => setShowEdit(false)}>Cancel</button>
<button className="btn btn-sm btn-primary" disabled={busy || !editTitle.trim()} onClick={() => {
run(() => updateTask(task!.id, {
title: editTitle.trim(),
description: editDescription.trim() || null,
priority: editPriority,
unusable: editUnusable,
due_date: editDueDate || null,
}))
setShowEdit(false)
}}>Save changes</button>
</div>
</div>
)}
{/* Delete confirm */}
{confirmDelete && (
<div className="card" style={{ background: 'var(--danger-bg)', marginBottom: 12 }}>
<p style={{ margin: '0 0 10px', fontWeight: 500 }}>Delete this task permanently? This cannot be undone.</p>
<div className="modal-actions">
<button className="btn btn-sm" onClick={() => setConfirmDelete(false)}>Cancel</button>
<button className="btn btn-sm btn-danger" disabled={busy} onClick={doDelete}>
{busy ? 'Deleting…' : 'Yes, delete'}
</button>
</div>
</div>
)}
{/* Photos */}
<div className="section-title">Photos</div>