feat(Navigation): update navigation links and rename 'Matplan' to 'Matsedel'
feat(matsedel): add MealPlanClient and page for meal planning feat(profil): add AI and suggestion tabs for admin users
This commit is contained in:
@@ -32,17 +32,10 @@ export default async function Navigation() {
|
|||||||
<Link href="/" style={linkStyle}>🏠 Hem</Link>
|
<Link href="/" style={linkStyle}>🏠 Hem</Link>
|
||||||
<Link href="/inventory" style={linkStyle}>🛒 Varor</Link>
|
<Link href="/inventory" style={linkStyle}>🛒 Varor</Link>
|
||||||
<Link href="/recipes" style={linkStyle}>📖 Recept</Link>
|
<Link href="/recipes" style={linkStyle}>📖 Recept</Link>
|
||||||
<Link href="/matplan" style={linkStyle}>📅 Matplan</Link>
|
<Link href="/matsedel" style={linkStyle}>📅 Matsedel</Link>
|
||||||
<Link href="/import" style={linkStyle}>📥 Importera</Link>
|
<Link href="/import" style={linkStyle}>📥 Importera</Link>
|
||||||
<Link href="/baslager" style={linkStyle}>🏪 Baslager</Link>
|
<Link href="/baslager" style={linkStyle}>🏪 Baslager</Link>
|
||||||
{(session?.user as any)?.role === 'admin' && (
|
|
||||||
<>
|
|
||||||
<Link href="/admin/products" style={linkStyle}>⚙️ Admin</Link>
|
|
||||||
<Link href="/admin/products/pending" style={linkStyle}>⏳ Förslag</Link>
|
|
||||||
<Link href="/admin/ai" style={linkStyle}>🤖 AI</Link>
|
|
||||||
<Link href="/profil?tab=anvandare" style={linkStyle}>👥 Användare</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span style={{ flex: 1 }} />
|
<span style={{ flex: 1 }} />
|
||||||
{session?.user && (
|
{session?.user && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default async function MealPlanPage() {
|
|||||||
return (
|
return (
|
||||||
<main style={{ padding: '1rem', maxWidth: '900px', margin: '0 auto' }}>
|
<main style={{ padding: '1rem', maxWidth: '900px', margin: '0 auto' }}>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<h1 style={{ marginBottom: '0.25rem' }}>Matplanering</h1>
|
<h1 style={{ marginBottom: '0.25rem' }}>Matsedel</h1>
|
||||||
<p style={{ color: '#666', marginBottom: '1.5rem' }}>
|
<p style={{ color: '#666', marginBottom: '1.5rem' }}>
|
||||||
Välj ett recept per dag — se en samlad ingredienslista i slutet.
|
Välj ett recept per dag — se en samlad ingredienslista i slutet.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { fetchJson } from '../../lib/api';
|
||||||
|
import type { Recipe } from '../../features/inventory/types';
|
||||||
|
import Navigation from '../Navigation';
|
||||||
|
import MealPlanClient from './MealPlanClient';
|
||||||
|
|
||||||
|
export default async function MealPlanPage() {
|
||||||
|
const recipes = await fetchJson<Recipe[]>('/api/recipes').catch(() => [] as Recipe[]);
|
||||||
|
return (
|
||||||
|
<main style={{ padding: '1rem', maxWidth: '900px', margin: '0 auto' }}>
|
||||||
|
<Navigation />
|
||||||
|
<h1 style={{ marginBottom: '0.25rem' }}>Matsedel</h1>
|
||||||
|
<p style={{ color: '#666', marginBottom: '1.5rem' }}>
|
||||||
|
Välj ett recept per dag — se en samlad ingredienslista i slutet.
|
||||||
|
</p>
|
||||||
|
<MealPlanClient recipes={recipes} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ const ADMIN_TABS: Tab[] = [
|
|||||||
{ id: 'profil', label: 'Min profil' },
|
{ id: 'profil', label: 'Min profil' },
|
||||||
{ id: 'anvandare', label: '👥 Användare' },
|
{ id: 'anvandare', label: '👥 Användare' },
|
||||||
{ id: 'databas', label: '🗄️ Databas' },
|
{ id: 'databas', label: '🗄️ Databas' },
|
||||||
|
{ id: 'forslag', label: '⏳ Förslag' },
|
||||||
|
{ id: 'ai', label: '🤖 AI' },
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -22,16 +22,25 @@ export default async function ProfilPage({ searchParams }: Props) {
|
|||||||
} else if (tab === 'anvandare' && isAdmin) {
|
} else if (tab === 'anvandare' && isAdmin) {
|
||||||
const { default: AnvandareTab } = await import('./tabs/AnvandareTab');
|
const { default: AnvandareTab } = await import('./tabs/AnvandareTab');
|
||||||
TabContent = AnvandareTab;
|
TabContent = AnvandareTab;
|
||||||
|
} else if (tab === 'forslag' && isAdmin) {
|
||||||
|
const { default: ForslagTab } = await import('./tabs/ForslagTab');
|
||||||
|
TabContent = ForslagTab;
|
||||||
|
} else if (tab === 'ai' && isAdmin) {
|
||||||
|
const { default: AiTab } = await import('./tabs/AiTab');
|
||||||
|
TabContent = AiTab;
|
||||||
} else {
|
} else {
|
||||||
TabContent = MinProfilTab;
|
TabContent = MinProfilTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminTabs = ['databas', 'anvandare', 'forslag', 'ai'];
|
||||||
|
const activeTab = isAdmin && adminTabs.includes(tab) ? tab : 'profil';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main style={{ padding: '1rem', maxWidth: '1200px', margin: '0 auto' }}>
|
<main style={{ padding: '1rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||||
<h1 style={{ marginBottom: '1.5rem' }}>Min profil</h1>
|
<h1 style={{ marginBottom: '1.5rem' }}>Min profil</h1>
|
||||||
<ProfileTabs activeTab={tab === 'databas' || tab === 'anvandare' ? tab : 'profil'} isAdmin={isAdmin} />
|
<ProfileTabs activeTab={activeTab} isAdmin={isAdmin} />
|
||||||
<TabContent />
|
<TabContent />
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import AiAdminClient from '../../admin/ai/AiAdminClient';
|
||||||
|
import type { AiModelInfo } from '../../admin/ai/AiAdminClient';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export default async function AiTab() {
|
||||||
|
const key = process.env.MISTRAL_API_KEY ?? '';
|
||||||
|
const hasKey = key.length > 0;
|
||||||
|
const keyHint = key.length >= 4 ? key.slice(-4) : '????';
|
||||||
|
|
||||||
|
let aiFunctions: AiModelInfo[] = [];
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/ai/models`, { cache: 'no-store' });
|
||||||
|
if (res.ok) aiFunctions = await res.json();
|
||||||
|
} catch {
|
||||||
|
// backend ej nåbart
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontSize: '1.1rem', marginBottom: '0.25rem' }}>🤖 AI-konfiguration</h2>
|
||||||
|
<p style={{ color: '#666', marginBottom: '2rem', fontSize: '0.9rem' }}>
|
||||||
|
Översikt över implementerade AI-funktioner och API-nyckelstatus.
|
||||||
|
</p>
|
||||||
|
<AiAdminClient keyHint={keyHint} hasKey={hasKey} aiFunctions={aiFunctions} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { getAuthHeaders } from '../../../../lib/auth-headers';
|
||||||
|
import PendingProductsClient from '../../admin/products/pending/PendingProductsClient';
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL ?? 'http://recipe-api:8080';
|
||||||
|
|
||||||
|
export default async function ForslagTab() {
|
||||||
|
const headers = await getAuthHeaders();
|
||||||
|
let products: any[] = [];
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/products/pending`, { headers, cache: 'no-store' });
|
||||||
|
if (res.ok) products = await res.json();
|
||||||
|
} catch {
|
||||||
|
// backend ej nåbart
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontSize: '1.1rem', marginBottom: '0.5rem' }}>Väntande produktförslag</h2>
|
||||||
|
<p style={{ color: '#64748b', marginBottom: '1.5rem', fontSize: '0.9rem' }}>
|
||||||
|
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} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user