mirror of
https://github.com/sakaljurgis/best-choice.git
synced 2026-07-08 21:47:40 +00:00
delete project
This commit is contained in:
@@ -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' });
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
`,
|
`,
|
||||||
|
|||||||
Reference in New Issue
Block a user