feat(products): add reset functionality to delete all products and related data
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
|
HttpCode,
|
||||||
Param,
|
Param,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
Patch,
|
Patch,
|
||||||
@@ -109,4 +110,10 @@ export class ProductsController {
|
|||||||
restore(@Param('id', ParseIntPipe) id: number) {
|
restore(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.productsService.restore(id);
|
return this.productsService.restore(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete('reset-all')
|
||||||
|
@HttpCode(200)
|
||||||
|
resetAll() {
|
||||||
|
return this.productsService.resetAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -381,4 +381,20 @@ export class ProductsService {
|
|||||||
async findAllTags() {
|
async findAllTags() {
|
||||||
return this.prisma.tag.findMany({ orderBy: { name: 'asc' } });
|
return this.prisma.tag.findMany({ orderBy: { name: 'asc' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resetAll() {
|
||||||
|
await this.prisma.$transaction([
|
||||||
|
this.prisma.receiptAlias.deleteMany(),
|
||||||
|
this.prisma.inventoryConsumption.deleteMany(),
|
||||||
|
this.prisma.inventoryItem.deleteMany(),
|
||||||
|
this.prisma.pantryItem.deleteMany(),
|
||||||
|
this.prisma.nutrition.deleteMany(),
|
||||||
|
this.prisma.productTag.deleteMany(),
|
||||||
|
this.prisma.tag.deleteMany(),
|
||||||
|
this.prisma.userProduct.deleteMany(),
|
||||||
|
this.prisma.recipeIngredient.deleteMany(),
|
||||||
|
this.prisma.product.deleteMany(),
|
||||||
|
]);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
import { resetAllProducts } from './actions';
|
||||||
|
|
||||||
|
export default function ResetProductsButton() {
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
'⚠️ Detta raderar ALLA produkter, inventory, taggar, kvitto-alias och pantry.\n\nKategorier och användare behålls.\n\nÄr du säker?',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await resetAllProducts();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={isPending}
|
||||||
|
style={{
|
||||||
|
padding: '0.6rem 1.25rem',
|
||||||
|
background: isPending ? '#ccc' : '#fff',
|
||||||
|
color: '#c00',
|
||||||
|
border: '1px solid #c00',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: isPending ? 'not-allowed' : 'pointer',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPending ? 'Återställer...' : '🗑 Återställ alla produkter'}
|
||||||
|
</button>
|
||||||
|
{error && (
|
||||||
|
<p style={{ color: 'crimson', marginTop: '0.5rem', fontSize: '0.9rem' }}>{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -73,3 +73,18 @@ export async function deleteProduct(id: number) {
|
|||||||
|
|
||||||
revalidatePath('/admin/products');
|
revalidatePath('/admin/products');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetAllProducts() {
|
||||||
|
const res = await fetch(`${API_BASE}/api/products/reset-all`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { ...(await getAuthHeaders()) },
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`Kunde inte återställa produkter: ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/admin/products');
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import MergePreviewForm from './MergePreviewForm';
|
|||||||
import AdminProductList from './AdminProductList';
|
import AdminProductList from './AdminProductList';
|
||||||
import Navigation from '../../Navigation';
|
import Navigation from '../../Navigation';
|
||||||
import ExpandableCreateProductSection from './ExpandableCreateProductSection';
|
import ExpandableCreateProductSection from './ExpandableCreateProductSection';
|
||||||
|
import ResetProductsButton from './ResetProductsButton';
|
||||||
|
|
||||||
export default async function AdminProductsPage() {
|
export default async function AdminProductsPage() {
|
||||||
const products = await fetchJson<Product[]>('/api/products');
|
const products = await fetchJson<Product[]>('/api/products');
|
||||||
@@ -16,6 +17,8 @@ export default async function AdminProductsPage() {
|
|||||||
|
|
||||||
<ExpandableCreateProductSection />
|
<ExpandableCreateProductSection />
|
||||||
|
|
||||||
|
<ResetProductsButton />
|
||||||
|
|
||||||
<MergePreviewForm products={products} />
|
<MergePreviewForm products={products} />
|
||||||
|
|
||||||
<AdminProductList products={products} />
|
<AdminProductList products={products} />
|
||||||
|
|||||||
Reference in New Issue
Block a user