diff --git a/backend/src/routes/tasks.js b/backend/src/routes/tasks.js index 8e58381..0763282 100644 --- a/backend/src/routes/tasks.js +++ b/backend/src/routes/tasks.js @@ -4,6 +4,11 @@ import { createTask, logEvent, TRANSITIONS, PRIORITIES, STATUSES } from '../lib/ import { fetchBookings } from '../lib/newbook.js' import { notifyAssignment } from '../lib/mailer.js' import { notifyAssignmentPush } from '../lib/push.js' +import { unlink } from 'fs/promises' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' + +const UPLOADS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'uploads') const PRIORITY_ORDER = `CASE t.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END` @@ -222,6 +227,18 @@ export async function taskRoutes(app) { return rows[0] }) + // DELETE /api/tasks/:id — hard delete; photo files removed from disk; events/photos cascade + app.delete('/api/tasks/:id', { preHandler: requireCap('update') }, async (req, reply) => { + const { rows: existing } = await pool.query('SELECT id FROM tasks WHERE id = $1', [req.params.id]) + if (!existing.length) return reply.status(404).send({ error: 'Task not found' }) + const taskId = existing[0].id + + const { rows: photos } = await pool.query('SELECT file_path FROM task_photos WHERE task_id = $1', [taskId]) + await pool.query('DELETE FROM tasks WHERE id = $1', [taskId]) + for (const p of photos) await unlink(join(UPLOADS_DIR, p.file_path)).catch(() => {}) + return { ok: true } + }) + // POST /api/tasks/:id/resolve — temporary_fix or fixed, with completed-by and cost app.post('/api/tasks/:id/resolve', { preHandler: requireCap('resolve') }, async (req, reply) => { const b = req.body || {} diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 9ac50d8..9e61531 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -75,6 +75,9 @@ export function createTask(body: Record): Promise { export function updateTask(id: number, body: Record): Promise { 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 diff --git a/frontend/src/components/TaskModal.tsx b/frontend/src/components/TaskModal.tsx index 8e3fcc5..3fde73e 100644 --- a/frontend/src/components/TaskModal.tsx +++ b/frontend/src/components/TaskModal.tsx @@ -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(null) + const [showEdit, setShowEdit] = useState(false) + const [editTitle, setEditTitle] = useState('') + const [editDescription, setEditDescription] = useState('') + const [editPriority, setEditPriority] = useState('medium') + const [editUnusable, setEditUnusable] = useState(false) + const [editDueDate, setEditDueDate] = useState('') + const [confirmDelete, setConfirmDelete] = useState(false) + const [showResolve, setShowResolve] = useState(null) const [resolveUsers, setResolveUsers] = useState([]) 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) { setBusy(true) setError(null) @@ -149,6 +175,16 @@ export default function TaskModal({ taskId, onClose, onChanged }: { {task.template_id && Recurring} + {canUpdate && ( + + )} + {user.is_admin && ( + + )} @@ -171,7 +207,64 @@ export default function TaskModal({ taskId, onClose, onChanged }: { {showCosts && task.cost != null && Cost £{task.cost}{task.cost_notes ? ` (${task.cost_notes})` : ''}} - {task.description &&

{task.description}

} + {!showEdit && task.description &&

{task.description}

} + + {/* Edit form */} + {showEdit && ( +
+
Edit task
+
+ + setEditTitle(e.target.value)} /> +
+
+ +