- Restructure from single filament table to multi-category product catalog (filamenti, stampaci, ploce, mlaznice, delovi, oprema) - Add shared layout components (SiteHeader, SiteFooter, CategoryNav, Breadcrumb) - Add reusable UI primitives (Badge, Button, Card, Modal, PriceDisplay, EmptyState) - Add catalog components (CatalogPage, ProductTable, ProductGrid, FilamentCard, ProductCard) - Add admin dashboard with sidebar navigation and category management - Add product API endpoints and database migrations - Add SEO pages (politika-privatnosti, uslovi-koriscenja, robots.txt, sitemap.xml) - Fix light mode: gradient text contrast, category nav accessibility, surface tokens, card shadows, CTA section theming
250 lines
9.7 KiB
TypeScript
250 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { filamentService, productService } from '@/src/services/api';
|
|
import { Filament } from '@/src/types/filament';
|
|
import { Product } from '@/src/types/product';
|
|
|
|
interface StatsCard {
|
|
label: string;
|
|
value: number | string;
|
|
colorHex: string;
|
|
href?: string;
|
|
}
|
|
|
|
export default function AdminOverview() {
|
|
const [filaments, setFilaments] = useState<(Filament & { id: string })[]>([]);
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const [filamentData, productData] = await Promise.all([
|
|
filamentService.getAll(),
|
|
productService.getAll().catch(() => []),
|
|
]);
|
|
setFilaments(filamentData);
|
|
setProducts(productData);
|
|
} catch (error) {
|
|
console.error('Error loading overview data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, []);
|
|
|
|
const lowStockFilaments = filaments.filter(f => f.kolicina <= 2 && f.kolicina > 0);
|
|
const outOfStockFilaments = filaments.filter(f => f.kolicina === 0);
|
|
const lowStockProducts = products.filter(p => p.stock <= 2 && p.stock > 0);
|
|
const outOfStockProducts = products.filter(p => p.stock === 0);
|
|
const activeSales = filaments.filter(f => f.sale_active).length + products.filter(p => p.sale_active).length;
|
|
|
|
const statsCards: StatsCard[] = [
|
|
{ label: 'Ukupno filamenata', value: filaments.length, colorHex: '#3b82f6', href: '/upadaj/dashboard/filamenti' },
|
|
{ label: 'Ukupno proizvoda', value: products.length, colorHex: '#22c55e', href: '/upadaj/dashboard/stampaci' },
|
|
{ label: 'Nisko stanje', value: lowStockFilaments.length + lowStockProducts.length, colorHex: '#f59e0b' },
|
|
{ label: 'Aktivni popusti', value: activeSales, colorHex: '#a855f7', href: '/upadaj/dashboard/prodaja' },
|
|
];
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
|
<p className="text-white/40 text-sm">Ucitavanje...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Page title */}
|
|
<div>
|
|
<h1
|
|
className="text-2xl font-black text-white tracking-tight"
|
|
style={{ fontFamily: 'var(--font-display)' }}
|
|
>
|
|
Pregled
|
|
</h1>
|
|
<p className="text-white/40 mt-1 text-sm">Brzi pregled stanja inventara</p>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{statsCards.map((card) => {
|
|
const content = (
|
|
<div
|
|
key={card.label}
|
|
className="rounded-2xl p-5 text-white"
|
|
style={{
|
|
background: `linear-gradient(135deg, ${card.colorHex}, ${card.colorHex}cc)`,
|
|
boxShadow: `0 4px 20px ${card.colorHex}30`,
|
|
}}
|
|
>
|
|
<p className="text-sm font-semibold opacity-80">{card.label}</p>
|
|
<p
|
|
className="text-3xl font-black mt-1"
|
|
style={{ fontFamily: 'var(--font-display)' }}
|
|
>
|
|
{card.value}
|
|
</p>
|
|
</div>
|
|
);
|
|
return card.href ? (
|
|
<Link key={card.label} href={card.href} className="hover:scale-[1.02] transition-transform">
|
|
{content}
|
|
</Link>
|
|
) : (
|
|
<div key={card.label}>{content}</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Low Stock Alerts */}
|
|
{(lowStockFilaments.length > 0 || outOfStockFilaments.length > 0 || lowStockProducts.length > 0 || outOfStockProducts.length > 0) && (
|
|
<div className="bg-white/[0.04] border border-white/[0.06] rounded-2xl p-6">
|
|
<h2
|
|
className="text-lg font-bold text-white mb-4"
|
|
style={{ fontFamily: 'var(--font-display)' }}
|
|
>
|
|
Upozorenja o stanju
|
|
</h2>
|
|
|
|
{/* Out of stock */}
|
|
{(outOfStockFilaments.length > 0 || outOfStockProducts.length > 0) && (
|
|
<div className="mb-4">
|
|
<h3 className="text-sm font-semibold text-red-400 mb-2">
|
|
Nema na stanju ({outOfStockFilaments.length + outOfStockProducts.length})
|
|
</h3>
|
|
<div className="space-y-1.5">
|
|
{outOfStockFilaments.slice(0, 5).map(f => (
|
|
<div key={f.id} className="text-sm text-white/60 flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-red-500 shrink-0" />
|
|
{f.tip} {f.finish} - {f.boja}
|
|
</div>
|
|
))}
|
|
{outOfStockProducts.slice(0, 5).map(p => (
|
|
<div key={p.id} className="text-sm text-white/60 flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-red-500 shrink-0" />
|
|
{p.name}
|
|
</div>
|
|
))}
|
|
{(outOfStockFilaments.length + outOfStockProducts.length) > 10 && (
|
|
<p className="text-xs text-white/30">
|
|
...i jos {outOfStockFilaments.length + outOfStockProducts.length - 10}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Low stock */}
|
|
{(lowStockFilaments.length > 0 || lowStockProducts.length > 0) && (
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-amber-400 mb-2">
|
|
Nisko stanje ({lowStockFilaments.length + lowStockProducts.length})
|
|
</h3>
|
|
<div className="space-y-1.5">
|
|
{lowStockFilaments.slice(0, 5).map(f => (
|
|
<div key={f.id} className="text-sm text-white/60 flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-amber-500 shrink-0" />
|
|
{f.tip} {f.finish} - {f.boja} (kolicina: {f.kolicina})
|
|
</div>
|
|
))}
|
|
{lowStockProducts.slice(0, 5).map(p => (
|
|
<div key={p.id} className="text-sm text-white/60 flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-amber-500 shrink-0" />
|
|
{p.name} (stanje: {p.stock})
|
|
</div>
|
|
))}
|
|
{(lowStockFilaments.length + lowStockProducts.length) > 10 && (
|
|
<p className="text-xs text-white/30">
|
|
...i jos {lowStockFilaments.length + lowStockProducts.length - 10}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent Activity */}
|
|
<div className="bg-white/[0.04] border border-white/[0.06] rounded-2xl p-6">
|
|
<h2
|
|
className="text-lg font-bold text-white mb-4"
|
|
style={{ fontFamily: 'var(--font-display)' }}
|
|
>
|
|
Poslednje dodano
|
|
</h2>
|
|
<div className="space-y-2.5">
|
|
{[...filaments]
|
|
.sort((a, b) => {
|
|
const dateA = a.updated_at || a.created_at || '';
|
|
const dateB = b.updated_at || b.created_at || '';
|
|
return new Date(dateB).getTime() - new Date(dateA).getTime();
|
|
})
|
|
.slice(0, 5)
|
|
.map(f => (
|
|
<div key={f.id} className="flex items-center justify-between text-sm">
|
|
<div className="flex items-center gap-3">
|
|
{f.boja_hex && (
|
|
<div
|
|
className="w-4 h-4 rounded-md border border-white/10"
|
|
style={{ backgroundColor: f.boja_hex }}
|
|
/>
|
|
)}
|
|
<span className="text-white/70">{f.tip} {f.finish} - {f.boja}</span>
|
|
</div>
|
|
<span className="text-white/30 text-xs">
|
|
{f.updated_at
|
|
? new Date(f.updated_at).toLocaleDateString('sr-RS')
|
|
: f.created_at
|
|
? new Date(f.created_at).toLocaleDateString('sr-RS')
|
|
: '-'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
{filaments.length === 0 && (
|
|
<p className="text-white/30 text-sm">Nema filamenata</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="bg-white/[0.04] border border-white/[0.06] rounded-2xl p-6">
|
|
<h2
|
|
className="text-lg font-bold text-white mb-4"
|
|
style={{ fontFamily: 'var(--font-display)' }}
|
|
>
|
|
Brze akcije
|
|
</h2>
|
|
<div className="flex flex-wrap gap-3">
|
|
{[
|
|
{ href: '/upadaj/dashboard/filamenti', label: 'Dodaj filament', color: '#3b82f6' },
|
|
{ href: '/upadaj/dashboard/stampaci', label: 'Dodaj proizvod', color: '#22c55e' },
|
|
{ href: '/upadaj/dashboard/prodaja', label: 'Upravljaj popustima', color: '#a855f7' },
|
|
{ href: '/upadaj/dashboard/boje', label: 'Upravljaj bojama', color: '#ec4899' },
|
|
{ href: '/upadaj/dashboard/analitika', label: 'Analitika', color: '#14b8a6' },
|
|
].map(action => (
|
|
<Link
|
|
key={action.href}
|
|
href={action.href}
|
|
className="px-4 py-2.5 text-white rounded-xl text-sm font-semibold hover:scale-[1.03] transition-transform"
|
|
style={{
|
|
background: action.color,
|
|
boxShadow: `0 2px 10px ${action.color}30`,
|
|
}}
|
|
>
|
|
{action.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|