delete project

This commit is contained in:
Jurgis Sakalauskas
2025-10-16 08:48:31 +03:00
parent 343f48de66
commit cea323ec3b
4 changed files with 74 additions and 0 deletions
+4
View File
@@ -120,3 +120,7 @@ export const updateProject = async (
return response.data; return response.data;
}; };
export const deleteProject = async (projectId: string): Promise<void> => {
await apiFetch<void>(`/projects/${projectId}`, { method: 'DELETE' });
};
+45
View File
@@ -2,16 +2,20 @@ import { FormEvent, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { import {
useCreateProjectMutation, useCreateProjectMutation,
useDeleteProjectMutation,
useProjectsQuery useProjectsQuery
} from '../query/projects'; } from '../query/projects';
function ProjectsPage() { function ProjectsPage() {
const { data, isLoading, isError, error } = useProjectsQuery(); const { data, isLoading, isError, error } = useProjectsQuery();
const createProjectMutation = useCreateProjectMutation(); const createProjectMutation = useCreateProjectMutation();
const deleteProjectMutation = useDeleteProjectMutation();
const navigate = useNavigate(); const navigate = useNavigate();
const [newProjectName, setNewProjectName] = useState(''); const [newProjectName, setNewProjectName] = useState('');
const [createError, setCreateError] = useState<string | null>(null); const [createError, setCreateError] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [pendingDeletionId, setPendingDeletionId] = useState<string | null>(null);
const projects = data?.data ?? []; 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 ( return (
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-10">
<section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"> <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
@@ -100,6 +125,11 @@ function ProjectsPage() {
{listError ? ( {listError ? (
<p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{listError}</p> <p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{listError}</p>
) : null} ) : null}
{deleteError ? (
<p className="mt-3 rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">
{deleteError}
</p>
) : null}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200 text-sm"> <table className="min-w-full divide-y divide-slate-200 text-sm">
@@ -117,6 +147,9 @@ function ProjectsPage() {
<th className="px-4 py-3 text-left font-medium uppercase tracking-wide text-slate-500"> <th className="px-4 py-3 text-left font-medium uppercase tracking-wide text-slate-500">
Updated Updated
</th> </th>
<th className="px-4 py-3 text-right font-medium uppercase tracking-wide text-slate-500">
Actions
</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-200"> <tbody className="divide-y divide-slate-200">
@@ -142,6 +175,18 @@ function ProjectsPage() {
<td className="px-4 py-3 text-xs text-slate-500"> <td className="px-4 py-3 text-xs text-slate-500">
{new Date(project.updatedAt).toLocaleString()} {new Date(project.updatedAt).toLocaleString()}
</td> </td>
<td className="px-4 py-3 text-right">
<button
type="button"
className="inline-flex items-center justify-center rounded-md border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-600 transition hover:bg-red-50 disabled:cursor-not-allowed disabled:border-red-100 disabled:text-red-300"
onClick={() => handleDeleteProject(project.id, project.name)}
disabled={deleteProjectMutation.isPending}
>
{deleteProjectMutation.isPending && pendingDeletionId === project.id
? 'Deleting…'
: 'Delete'}
</button>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
+14
View File
@@ -3,6 +3,7 @@ import {
fetchProject, fetchProject,
fetchProjects, fetchProjects,
updateProject, updateProject,
deleteProject,
type CreateProjectPayload, type CreateProjectPayload,
type ProjectsListResponse, type ProjectsListResponse,
type UpdateProjectPayload 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) });
}
});
};
+11
View File
@@ -188,8 +188,19 @@ export const updateProject = async (
}; };
export const deleteProject = async (id: string): Promise<boolean> => { export const deleteProject = async (id: string): Promise<boolean> => {
// Remove dependent items and prices explicitly while leaving shared URLs untouched.
const result = await query( 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 DELETE FROM projects
WHERE id = $1 WHERE id = $1
`, `,