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:
parent
03d1cd2bb6
commit
ca006f9aea
3 changed files with 118 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue