feat(api): implement new API routes for bulk category updates, inventory consumption, and product management with authentication
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useMemo, useEffect, useTransition, useCallback } from 'react';
|
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
import type { Product, Category } from '../../../features/inventory/types';
|
import type { Product, Category } from '../../../features/inventory/types';
|
||||||
import EditProductForm from './EditProductForm';
|
import EditProductForm from './EditProductForm';
|
||||||
import { bulkSetCategory } from './actions';
|
|
||||||
|
|
||||||
type CategoryNode = Category & { children: CategoryNode[] };
|
type CategoryNode = Category & { children: CategoryNode[] };
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ export default function AdminProductList() {
|
|||||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||||
const [bulkCategoryId, setBulkCategoryId] = useState<string>('');
|
const [bulkCategoryId, setBulkCategoryId] = useState<string>('');
|
||||||
const [categoryTree, setCategoryTree] = useState<CategoryNode[]>([]);
|
const [categoryTree, setCategoryTree] = useState<CategoryNode[]>([]);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isBulkPending, setIsBulkPending] = useState(false);
|
||||||
const [bulkError, setBulkError] = useState<string | null>(null);
|
const [bulkError, setBulkError] = useState<string | null>(null);
|
||||||
|
|
||||||
// AI-kategorisering state
|
// AI-kategorisering state
|
||||||
@@ -132,21 +131,30 @@ export default function AdminProductList() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkApply = () => {
|
const handleBulkApply = async () => {
|
||||||
setBulkError(null);
|
setBulkError(null);
|
||||||
const ids = Array.from(selectedIds);
|
const ids = Array.from(selectedIds);
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
const categoryId = bulkCategoryId === '' ? null : bulkCategoryId === '__remove__' ? null : Number(bulkCategoryId);
|
const categoryId = bulkCategoryId === '' ? null : bulkCategoryId === '__remove__' ? null : Number(bulkCategoryId);
|
||||||
startTransition(async () => {
|
setIsBulkPending(true);
|
||||||
try {
|
try {
|
||||||
await bulkSetCategory(ids, categoryId);
|
const res = await fetch('/api/admin/bulk-set-category', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ids, categoryId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Fel vid uppdatering');
|
||||||
|
}
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
setBulkCategoryId('');
|
setBulkCategoryId('');
|
||||||
refetchProducts();
|
refetchProducts();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setBulkError(err instanceof Error ? err.message : 'Fel vid uppdatering');
|
setBulkError(err instanceof Error ? err.message : 'Fel vid uppdatering');
|
||||||
|
} finally {
|
||||||
|
setIsBulkPending(false);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAiCategorize = async () => {
|
const handleAiCategorize = async () => {
|
||||||
@@ -178,7 +186,15 @@ export default function AdminProductList() {
|
|||||||
grouped.get(cid)!.push(s.productId);
|
grouped.get(cid)!.push(s.productId);
|
||||||
}
|
}
|
||||||
for (const [categoryId, ids] of grouped.entries()) {
|
for (const [categoryId, ids] of grouped.entries()) {
|
||||||
await bulkSetCategory(ids, categoryId);
|
const res = await fetch('/api/admin/bulk-set-category', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ids, categoryId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Fel vid tillämpning');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setAiSuggestions(null);
|
setAiSuggestions(null);
|
||||||
setAiApproved(new Set());
|
setAiApproved(new Set());
|
||||||
@@ -287,10 +303,10 @@ export default function AdminProductList() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleBulkApply}
|
onClick={handleBulkApply}
|
||||||
disabled={isPending}
|
disabled={isBulkPending}
|
||||||
style={{ padding: '0.4rem 0.9rem', background: '#0070f3', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '0.9rem' }}
|
style={{ padding: '0.4rem 0.9rem', background: '#0070f3', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '0.9rem' }}
|
||||||
>
|
>
|
||||||
{isPending ? 'Sparar…' : 'Sätt kategori'}
|
{isBulkPending ? 'Sparar…' : 'Sätt kategori'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition, useEffect } from 'react';
|
import { useState, useTransition, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import type { MergePreview, Product } from '../../../features/inventory/types';
|
import type { MergePreview, Product } from '../../../features/inventory/types';
|
||||||
import { mergeProducts } from '../../inventory/actions';
|
|
||||||
|
|
||||||
export default function MergePreviewForm() {
|
export default function MergePreviewForm() {
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
@@ -14,6 +14,7 @@ export default function MergePreviewForm() {
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [isConfirming, setIsConfirming] = useState(false);
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isExpanded && products.length === 0) {
|
if (isExpanded && products.length === 0) {
|
||||||
@@ -74,11 +75,19 @@ export default function MergePreviewForm() {
|
|||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const res = await fetch('/api/admin/merge-products', {
|
||||||
formData.set('sourceProductId', String(preview.source.id));
|
method: 'POST',
|
||||||
formData.set('targetProductId', String(preview.target.id));
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sourceProductId: preview.source.id,
|
||||||
|
targetProductId: preview.target.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
await mergeProducts(formData);
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Sammanslagning misslyckades');
|
||||||
|
}
|
||||||
|
|
||||||
setSuccessMessage(
|
setSuccessMessage(
|
||||||
`Produkten "${preview.source.canonicalName || preview.source.name}" slogs ihop med "${preview.target.canonicalName || preview.target.name}".`,
|
`Produkten "${preview.source.canonicalName || preview.source.name}" slogs ihop med "${preview.target.canonicalName || preview.target.name}".`,
|
||||||
@@ -87,6 +96,7 @@ export default function MergePreviewForm() {
|
|||||||
setIsConfirming(false);
|
setIsConfirming(false);
|
||||||
setSourceProductId('');
|
setSourceProductId('');
|
||||||
setTargetProductId('');
|
setTargetProductId('');
|
||||||
|
router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState } from 'react';
|
||||||
import { resetAllProducts } from './actions';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function ResetProductsButton() {
|
export default function ResetProductsButton() {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
function handleClick() {
|
async function handleClick() {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
'⚠️ Detta raderar ALLA produkter, inventory, taggar, kvitto-alias och pantry.\n\nKategorier och användare behålls.\n\nÄr du säker?',
|
'⚠️ Detta raderar ALLA produkter, inventory, taggar, kvitto-alias och pantry.\n\nKategorier och användare behålls.\n\nÄr du säker?',
|
||||||
@@ -16,13 +17,19 @@ export default function ResetProductsButton() {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
startTransition(async () => {
|
setIsPending(true);
|
||||||
try {
|
try {
|
||||||
await resetAllProducts();
|
const res = await fetch('/api/admin/reset-products', { method: 'POST' });
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Återställning misslyckades');
|
||||||
|
}
|
||||||
|
router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const { ids, categoryId } = body as { ids: number[]; categoryId: number | null };
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/products/bulk-update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ids, categoryId }),
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Bulk-uppdatering misslyckades' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/inventory/${id}/consume`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Kunde inte förbruka inventory-rad' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/inventory/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Kunde inte uppdatera inventory-rad' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/inventory`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Kunde inte skapa inventory-rad' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const { sourceProductId, targetProductId } = body as {
|
||||||
|
sourceProductId: number;
|
||||||
|
targetProductId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/products/merge`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ sourceProductId, targetProductId }),
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Sammanslagning misslyckades' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/pantry/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Kunde inte ta bort baslager-vara' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const { productId } = body as { productId: number };
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/pantry`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ productId }),
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Kunde inte lägga till baslager-vara' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { auth } from '../../../../auth';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.accessToken) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/api/products/reset-all`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
return NextResponse.json({ error: text || 'Återställning misslyckades' }, { status: res.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import type { Product } from '../../features/inventory/types';
|
import type { Product } from '../../features/inventory/types';
|
||||||
import { addPantryItem } from './actions';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
products: Product[];
|
products: Product[];
|
||||||
@@ -11,23 +11,34 @@ type Props = {
|
|||||||
|
|
||||||
export default function AddToPantryForm({ products, pantryProductIds }: Props) {
|
export default function AddToPantryForm({ products, pantryProductIds }: Props) {
|
||||||
const [selectedId, setSelectedId] = useState('');
|
const [selectedId, setSelectedId] = useState('');
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const available = products.filter((p) => !pantryProductIds.has(p.id));
|
const available = products.filter((p) => !pantryProductIds.has(p.id));
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!selectedId) return;
|
if (!selectedId) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
startTransition(async () => {
|
setIsPending(true);
|
||||||
try {
|
try {
|
||||||
await addPantryItem(Number(selectedId));
|
const res = await fetch('/api/admin/pantry-item', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ productId: Number(selectedId) }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Kunde inte lägga till');
|
||||||
|
}
|
||||||
setSelectedId('');
|
setSelectedId('');
|
||||||
|
router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useTransition } from 'react';
|
import { useRouter } from 'next/navigation';
|
||||||
import { removePantryItem } from './actions';
|
|
||||||
|
|
||||||
type PantryItem = {
|
type PantryItem = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -20,13 +19,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PantryList({ items, inventoryByProductId }: Props) {
|
export default function PantryList({ items, inventoryByProductId }: Props) {
|
||||||
const [isPending, startTransition] = useTransition();
|
const router = useRouter();
|
||||||
|
|
||||||
function handleRemove(id: number, name: string) {
|
async function handleRemove(id: number, name: string) {
|
||||||
if (!confirm(`Ta bort "${name}" från baslagret?`)) return;
|
if (!confirm(`Ta bort "${name}" från baslagret?`)) return;
|
||||||
startTransition(async () => {
|
const res = await fetch(`/api/admin/pantry-item/${id}`, { method: 'DELETE' });
|
||||||
await removePantryItem(id);
|
if (res.ok) router.refresh();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState } from 'react';
|
||||||
import { consumeInventoryItem } from './actions';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -10,8 +10,9 @@ type Props = {
|
|||||||
|
|
||||||
export default function InventoryConsumeForm({ id, unit }: Props) {
|
export default function InventoryConsumeForm({ id, unit }: Props) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return (
|
return (
|
||||||
@@ -44,14 +45,27 @@ export default function InventoryConsumeForm({ id, unit }: Props) {
|
|||||||
const { quantity, unit: parsedUnit } = parseQuantityInput(raw, unit);
|
const { quantity, unit: parsedUnit } = parseQuantityInput(raw, unit);
|
||||||
formData.set('amountUsed', String(quantity));
|
formData.set('amountUsed', String(quantity));
|
||||||
formData.set('unit', parsedUnit);
|
formData.set('unit', parsedUnit);
|
||||||
startTransition(async () => {
|
const comment = String(formData.get('comment') || '').trim();
|
||||||
|
const payload: Record<string, unknown> = { amountUsed: quantity };
|
||||||
|
if (comment) payload.comment = comment;
|
||||||
|
setIsPending(true);
|
||||||
try {
|
try {
|
||||||
await consumeInventoryItem(formData);
|
const res = await fetch(`/api/admin/inventory-item/${id}/consume`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Kunde inte förbruka');
|
||||||
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState } from 'react';
|
||||||
import { updateInventoryItem } from './actions';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { InventoryItem } from '../../features/inventory/types';
|
import type { InventoryItem } from '../../features/inventory/types';
|
||||||
import { UNIT_OPTIONS } from '../../lib/units';
|
import { UNIT_OPTIONS } from '../../lib/units';
|
||||||
|
|
||||||
@@ -46,8 +46,9 @@ const LOCATION_OPTIONS = [
|
|||||||
|
|
||||||
export default function InventoryEditForm({ item }: Props) {
|
export default function InventoryEditForm({ item }: Props) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
return (
|
return (
|
||||||
@@ -79,16 +80,35 @@ export default function InventoryEditForm({ item }: Props) {
|
|||||||
const raw = formData.get('quantity') as string;
|
const raw = formData.get('quantity') as string;
|
||||||
const unit = formData.get('unit') as string;
|
const unit = formData.get('unit') as string;
|
||||||
const { quantity, unit: parsedUnit } = parseQuantityInput(raw, unit);
|
const { quantity, unit: parsedUnit } = parseQuantityInput(raw, unit);
|
||||||
formData.set('quantity', String(quantity));
|
|
||||||
formData.set('unit', parsedUnit);
|
const payload: Record<string, unknown> = { opened: formData.get('opened') === 'on' };
|
||||||
startTransition(async () => {
|
if (raw) payload.quantity = quantity;
|
||||||
|
if (parsedUnit) payload.unit = parsedUnit;
|
||||||
|
payload.location = String(formData.get('location') || '').trim();
|
||||||
|
payload.brand = String(formData.get('brand') || '').trim();
|
||||||
|
payload.suitableFor = String(formData.get('suitableFor') || '').trim();
|
||||||
|
payload.comment = String(formData.get('comment') || '').trim();
|
||||||
|
const bestBeforeDate = String(formData.get('bestBeforeDate') || '').trim();
|
||||||
|
payload.bestBeforeDate = bestBeforeDate || null;
|
||||||
|
|
||||||
|
setIsPending(true);
|
||||||
try {
|
try {
|
||||||
await updateInventoryItem(formData);
|
const res = await fetch(`/api/admin/inventory-item/${item.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Kunde inte uppdatera');
|
||||||
|
}
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { createInventoryItem } from './actions';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { Product } from '../../features/inventory/types';
|
import type { Product } from '../../features/inventory/types';
|
||||||
import { UNIT_OPTIONS } from '../../lib/units';
|
import { UNIT_OPTIONS } from '../../lib/units';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export default function InventoryForm({ products }: Props) {
|
|||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const LOCATION_OPTIONS = [
|
const LOCATION_OPTIONS = [
|
||||||
{ value: '', label: 'Välj plats' },
|
{ value: '', label: 'Välj plats' },
|
||||||
@@ -82,8 +83,32 @@ export default function InventoryForm({ products }: Props) {
|
|||||||
formData.set('quantity', String(quantity));
|
formData.set('quantity', String(quantity));
|
||||||
formData.set('unit', parsedUnit);
|
formData.set('unit', parsedUnit);
|
||||||
try {
|
try {
|
||||||
await createInventoryItem(formData);
|
const payload: Record<string, unknown> = {
|
||||||
|
productId: Number(formData.get('productId')),
|
||||||
|
quantity,
|
||||||
|
unit: parsedUnit,
|
||||||
|
};
|
||||||
|
const location = String(formData.get('location') || '').trim();
|
||||||
|
if (location) payload.location = location;
|
||||||
|
payload.opened = formData.get('opened') === 'on';
|
||||||
|
const brand = String(formData.get('brand') || '').trim();
|
||||||
|
if (brand) payload.brand = brand;
|
||||||
|
const suitableFor = String(formData.get('suitableFor') || '').trim();
|
||||||
|
if (suitableFor) payload.suitableFor = suitableFor;
|
||||||
|
const bestBeforeDate = String(formData.get('bestBeforeDate') || '').trim();
|
||||||
|
if (bestBeforeDate) payload.bestBeforeDate = bestBeforeDate;
|
||||||
|
|
||||||
|
const res = await fetch('/api/admin/inventory-item', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Kunde inte spara');
|
||||||
|
}
|
||||||
form.reset();
|
form.reset();
|
||||||
|
router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,29 +1,40 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState } from 'react';
|
||||||
import { createProduct } from './actions';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function ProductForm() {
|
export default function ProductForm() {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const form = e.currentTarget;
|
const form = e.currentTarget;
|
||||||
const formData = new FormData(form);
|
const name = String((new FormData(form)).get('name') || '').trim();
|
||||||
|
|
||||||
startTransition(async () => {
|
setIsPending(true);
|
||||||
try {
|
try {
|
||||||
await createProduct(formData);
|
const res = await fetch('/api/admin/create-product', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data?.error || 'Kunde inte skapa produkt');
|
||||||
|
}
|
||||||
form.reset();
|
form.reset();
|
||||||
|
router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
Reference in New Issue
Block a user