MAJOR UPPDATE: "First Ai"
feat: add AI categorization for products and enhance user management - Integrated AI service for category suggestions in receipt import and product management. - Added premium subscription feature for users with corresponding API endpoints. - Implemented admin interface for managing pending product suggestions. - Enhanced user management to include premium status and corresponding UI updates. - Updated database schema to support new fields for premium status and product status.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { setProductStatus } from '../actions';
|
||||
|
||||
type PendingProduct = {
|
||||
id: number;
|
||||
name: string;
|
||||
canonicalName: string | null;
|
||||
createdAt: string;
|
||||
categoryRef?: { name: string; parent?: { name: string } } | null;
|
||||
owner?: { id: number; username: string } | null;
|
||||
};
|
||||
|
||||
export default function PendingProductsClient({ products: initial }: { products: PendingProduct[] }) {
|
||||
const [products, setProducts] = useState<PendingProduct[]>(initial);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [processing, setProcessing] = useState<number | null>(null);
|
||||
|
||||
function handleAction(id: number, status: 'active' | 'rejected') {
|
||||
setError(null);
|
||||
setProcessing(id);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await setProductStatus(id, status);
|
||||
setProducts((prev) => prev.filter((p) => p.id !== id));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Fel vid uppdatering');
|
||||
} finally {
|
||||
setProcessing(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (products.length === 0) {
|
||||
return (
|
||||
<div style={{ color: '#64748b', background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 8, padding: '2rem', textAlign: 'center' }}>
|
||||
Inga väntande produktförslag 🎉
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && (
|
||||
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 6, padding: '0.6rem 1rem', color: '#dc2626', marginBottom: '1rem', fontSize: 14 }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#f1f5f9', textAlign: 'left' }}>
|
||||
{['Produkt', 'Kategori (AI)', 'Föreslagen av', 'Datum', 'Åtgärd'].map((h) => (
|
||||
<th key={h} style={{ padding: '0.6rem 0.8rem', borderBottom: '2px solid #e2e8f0' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.map((p) => {
|
||||
const isProcessing = processing === p.id && isPending;
|
||||
const categoryPath = [p.categoryRef?.parent?.name, p.categoryRef?.name].filter(Boolean).join(' › ');
|
||||
return (
|
||||
<tr key={p.id} style={{ borderBottom: '1px solid #e2e8f0', opacity: isProcessing ? 0.5 : 1 }}>
|
||||
<td style={{ padding: '0.6rem 0.8rem' }}>
|
||||
<div style={{ fontWeight: 500 }}>{p.canonicalName ?? p.name}</div>
|
||||
{p.canonicalName && p.canonicalName !== p.name && (
|
||||
<div style={{ fontSize: 12, color: '#94a3b8' }}>{p.name}</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '0.6rem 0.8rem' }}>
|
||||
{categoryPath ? (
|
||||
<span style={{ fontSize: 12, background: '#e0f2fe', borderRadius: 999, padding: '0.15rem 0.5rem', color: '#0369a1' }}>{categoryPath}</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, color: '#94a3b8' }}>—</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '0.6rem 0.8rem', color: '#475569' }}>{p.owner?.username ?? '—'}</td>
|
||||
<td style={{ padding: '0.6rem 0.8rem', color: '#94a3b8', fontSize: 12 }}>
|
||||
{new Date(p.createdAt).toLocaleDateString('sv-SE')}
|
||||
</td>
|
||||
<td style={{ padding: '0.6rem 0.8rem' }}>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
onClick={() => handleAction(p.id, 'active')}
|
||||
disabled={isProcessing}
|
||||
style={{ padding: '0.3rem 0.7rem', background: '#dcfce7', border: '1px solid #86efac', borderRadius: 4, cursor: 'pointer', fontSize: 12, color: '#15803d', fontWeight: 600 }}
|
||||
>
|
||||
✓ Godkänn
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAction(p.id, 'rejected')}
|
||||
disabled={isProcessing}
|
||||
style={{ padding: '0.3rem 0.7rem', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 4, cursor: 'pointer', fontSize: 12, color: '#dc2626', fontWeight: 600 }}
|
||||
>
|
||||
✕ Avvisa
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { auth } from '../../../../auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import Navigation from '../../../Navigation';
|
||||
import { getAuthHeaders } from '../../../../lib/auth-headers';
|
||||
import PendingProductsClient from './PendingProductsClient';
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL ?? 'http://recipe-api:8080';
|
||||
|
||||
export default async function PendingProductsPage() {
|
||||
const session = await auth();
|
||||
if (!session || (session.user as any)?.role !== 'admin') redirect('/');
|
||||
|
||||
const headers = await getAuthHeaders();
|
||||
const res = await fetch(`${API_BASE}/api/products/pending`, { headers, cache: 'no-store' });
|
||||
const products = res.ok ? await res.json() : [];
|
||||
|
||||
return (
|
||||
<main style={{ padding: '1rem', maxWidth: '1100px', margin: '0 auto' }}>
|
||||
<Navigation />
|
||||
<h1 style={{ marginBottom: '0.5rem' }}>Väntande produktförslag</h1>
|
||||
<p style={{ color: '#64748b', marginBottom: '1.5rem' }}>
|
||||
Produkter som användare har föreslagit. Godkänn för att göra dem tillgängliga i katalogen, eller avvisa för att ta bort dem.
|
||||
</p>
|
||||
<PendingProductsClient products={products} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user