diff --git a/client/src/api/projects.ts b/client/src/api/projects.ts index 73fa097..6515769 100644 --- a/client/src/api/projects.ts +++ b/client/src/api/projects.ts @@ -120,3 +120,7 @@ export const updateProject = async ( return response.data; }; + +export const deleteProject = async (projectId: string): Promise => { + await apiFetch(`/projects/${projectId}`, { method: 'DELETE' }); +}; diff --git a/client/src/pages/projects-page.tsx b/client/src/pages/projects-page.tsx index 858aa69..af9e343 100644 --- a/client/src/pages/projects-page.tsx +++ b/client/src/pages/projects-page.tsx @@ -2,16 +2,20 @@ import { FormEvent, useMemo, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useCreateProjectMutation, + useDeleteProjectMutation, useProjectsQuery } from '../query/projects'; function ProjectsPage() { const { data, isLoading, isError, error } = useProjectsQuery(); const createProjectMutation = useCreateProjectMutation(); + const deleteProjectMutation = useDeleteProjectMutation(); const navigate = useNavigate(); const [newProjectName, setNewProjectName] = useState(''); const [createError, setCreateError] = useState(null); + const [deleteError, setDeleteError] = useState(null); + const [pendingDeletionId, setPendingDeletionId] = useState(null); const projects = data?.data ?? []; @@ -49,6 +53,27 @@ function ProjectsPage() { } }; + const handleDeleteProject = async (projectId: string, projectName: string) => { + const confirmed = window.confirm( + `Delete project "${projectName}"? This will remove all items and their prices, but any saved URLs will be kept.` + ); + + if (!confirmed) { + return; + } + + setDeleteError(null); + setPendingDeletionId(projectId); + + try { + await deleteProjectMutation.mutateAsync(projectId); + } catch (submitError) { + setDeleteError((submitError as Error).message); + } finally { + setPendingDeletionId(null); + } + }; + return (
@@ -100,6 +125,11 @@ function ProjectsPage() { {listError ? (

{listError}

) : null} + {deleteError ? ( +

+ {deleteError} +

+ ) : null}
@@ -117,6 +147,9 @@ function ProjectsPage() { + @@ -142,6 +175,18 @@ function ProjectsPage() { + ))} diff --git a/client/src/query/projects.ts b/client/src/query/projects.ts index e558c6d..93b9215 100644 --- a/client/src/query/projects.ts +++ b/client/src/query/projects.ts @@ -3,6 +3,7 @@ import { fetchProject, fetchProjects, updateProject, + deleteProject, type CreateProjectPayload, type ProjectsListResponse, type UpdateProjectPayload @@ -116,3 +117,16 @@ export const useUpdateItemMutation = (projectId: string | undefined) => { } }); }; + +export const useDeleteProjectMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (projectId: string) => deleteProject(projectId), + onSuccess: (_, projectId) => { + queryClient.invalidateQueries({ queryKey: projectsKeys.list() }); + queryClient.removeQueries({ queryKey: projectsKeys.detail(projectId) }); + queryClient.removeQueries({ queryKey: projectsKeys.items(projectId) }); + } + }); +}; diff --git a/server/src/db/projects-repository.ts b/server/src/db/projects-repository.ts index adb6cd9..8210c6a 100644 --- a/server/src/db/projects-repository.ts +++ b/server/src/db/projects-repository.ts @@ -188,8 +188,19 @@ export const updateProject = async ( }; export const deleteProject = async (id: string): Promise => { + // Remove dependent items and prices explicitly while leaving shared URLs untouched. const result = await query( ` + WITH deleted_prices AS ( + DELETE FROM item_prices ip + USING items i + WHERE ip.item_id = i.id + AND i.project_id = $1 + ), + deleted_items AS ( + DELETE FROM items + WHERE project_id = $1 + ) DELETE FROM projects WHERE id = $1 `,
Updated + Actions +
{new Date(project.updatedAt).toLocaleString()} + +