'use client' import { useState, useEffect, useCallback, useRef } from 'react'; import { saleService, customerService, filamentService, colorService } from '@/src/services/api'; import { Customer, Sale, SaleItem, CreateSaleRequest } from '@/src/types/sales'; import { Filament } from '@/src/types/filament'; interface LineItem { filament_id: string; item_type: 'refill' | 'spulna'; quantity: number; } export default function SalesPage() { const [mounted, setMounted] = useState(false); const [darkMode, setDarkMode] = useState(false); // Sales list state const [sales, setSales] = useState([]); const [totalSales, setTotalSales] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); // Modal state const [showNewSaleModal, setShowNewSaleModal] = useState(false); const [showDetailModal, setShowDetailModal] = useState(false); const [selectedSale, setSelectedSale] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const LIMIT = 50; // Dark mode init useEffect(() => { setMounted(true); const saved = localStorage.getItem('darkMode'); setDarkMode(saved !== null ? JSON.parse(saved) : true); }, []); useEffect(() => { if (!mounted) return; localStorage.setItem('darkMode', JSON.stringify(darkMode)); if (darkMode) document.documentElement.classList.add('dark'); else document.documentElement.classList.remove('dark'); }, [darkMode, mounted]); // Auth check useEffect(() => { if (!mounted) return; const token = localStorage.getItem('authToken'); const expiry = localStorage.getItem('tokenExpiry'); if (!token || !expiry || Date.now() > parseInt(expiry)) { window.location.href = '/upadaj'; } }, [mounted]); // Fetch sales const fetchSales = useCallback(async () => { try { setLoading(true); setError(''); const data = await saleService.getAll(page, LIMIT); setSales(data.sales); setTotalSales(data.total); } catch { setError('Greska pri ucitavanju prodaja'); } finally { setLoading(false); } }, [page]); useEffect(() => { if (!mounted) return; fetchSales(); }, [mounted, fetchSales]); const totalPages = Math.ceil(totalSales / LIMIT); const handleViewDetail = async (sale: Sale) => { try { setDetailLoading(true); setShowDetailModal(true); const detail = await saleService.getById(sale.id); setSelectedSale(detail); } catch { setError('Greska pri ucitavanju detalja prodaje'); setShowDetailModal(false); } finally { setDetailLoading(false); } }; const handleDeleteSale = async (id: string) => { if (!window.confirm('Da li ste sigurni da zelite da obrisete ovu prodaju?')) return; try { await saleService.delete(id); setShowDetailModal(false); setSelectedSale(null); fetchSales(); } catch { setError('Greska pri brisanju prodaje'); } }; const formatDate = (dateStr?: string) => { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleDateString('sr-RS', { day: '2-digit', month: '2-digit', year: 'numeric', }); }; const formatPrice = (amount: number) => { return amount.toLocaleString('sr-RS') + ' RSD'; }; if (!mounted) return null; return (
{/* Sidebar */}

Admin Panel

{/* Main Content */}

Prodaja

{error && (
{error}
)} {/* Sales Table */}
{loading ? (
Ucitavanje...
) : sales.length === 0 ? (
Nema prodaja
) : ( {sales.map((sale) => ( ))}
Datum Ime kupca Stavki Cena Akcije
{formatDate(sale.created_at)} {sale.customer_name || '-'} {sale.item_count ?? 0} {formatPrice(sale.total_amount)}
)}
{/* Pagination */} {totalPages > 1 && (
Strana {page} od {totalPages}
)}
{/* New Sale Modal */} {showNewSaleModal && ( setShowNewSaleModal(false)} onCreated={() => { setShowNewSaleModal(false); fetchSales(); }} formatPrice={formatPrice} /> )} {/* Sale Detail Modal */} {showDetailModal && ( { setShowDetailModal(false); setSelectedSale(null); }} onDelete={(id) => handleDeleteSale(id)} formatPrice={formatPrice} formatDate={formatDate} /> )}
); } // --- NewSaleModal --- function NewSaleModal({ onClose, onCreated, formatPrice, }: { onClose: () => void; onCreated: () => void; formatPrice: (n: number) => string; }) { const [customerName, setCustomerName] = useState(''); const [customerPhone, setCustomerPhone] = useState(''); const [customerCity, setCustomerCity] = useState(''); const [customerNotes, setCustomerNotes] = useState(''); const [notes, setNotes] = useState(''); const [items, setItems] = useState([{ filament_id: '', item_type: 'refill', quantity: 1 }]); const [filaments, setFilaments] = useState([]); const [colorPrices, setColorPrices] = useState>({}); const [customerSuggestions, setCustomerSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(''); const searchTimeout = useRef | null>(null); const suggestionsRef = useRef(null); useEffect(() => { const loadData = async () => { try { const [filamentData, colorData] = await Promise.all([ filamentService.getAll(), colorService.getAll(), ]); setFilaments(filamentData); const priceMap: Record = {}; for (const c of colorData) { priceMap[c.name] = { cena_refill: c.cena_refill || 3499, cena_spulna: c.cena_spulna || 3999 }; } setColorPrices(priceMap); } catch { // Data failed to load } }; loadData(); }, []); // Close suggestions on outside click useEffect(() => { const handler = (e: MouseEvent) => { if (suggestionsRef.current && !suggestionsRef.current.contains(e.target as Node)) { setShowSuggestions(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); const handleCustomerSearch = (value: string) => { setCustomerName(value); if (searchTimeout.current) clearTimeout(searchTimeout.current); if (value.length < 2) { setCustomerSuggestions([]); setShowSuggestions(false); return; } searchTimeout.current = setTimeout(async () => { try { const results = await customerService.search(value); setCustomerSuggestions(results); setShowSuggestions(results.length > 0); } catch { setCustomerSuggestions([]); setShowSuggestions(false); } }, 300); }; const selectCustomer = (customer: Customer) => { setCustomerName(customer.name); setCustomerPhone(customer.phone || ''); setCustomerCity(customer.city || ''); setCustomerNotes(customer.notes || ''); setShowSuggestions(false); }; const getFilamentById = (id: string): Filament | undefined => { return filaments.find((f) => String(f.id) === String(id)); }; const getFilamentPrice = (filament: Filament, type: 'refill' | 'spulna'): number => { const prices = colorPrices[filament.boja]; const base = type === 'refill' ? (prices?.cena_refill || 3499) : (prices?.cena_spulna || 3999); if (filament.sale_active && filament.sale_percentage) { return Math.round(base * (1 - filament.sale_percentage / 100)); } return base; }; const getItemPrice = (item: LineItem): number => { const filament = getFilamentById(item.filament_id); if (!filament) return 0; return getFilamentPrice(filament, item.item_type) * item.quantity; }; const totalAmount = items.reduce((sum, item) => sum + getItemPrice(item), 0); const getAvailableStock = (filament: Filament, type: 'refill' | 'spulna'): number => { return type === 'refill' ? filament.refill : filament.spulna; }; const updateItem = (index: number, updates: Partial) => { setItems((prev) => { const next = [...prev]; next[index] = { ...next[index], ...updates }; return next; }); }; const removeItem = (index: number) => { if (items.length <= 1) return; setItems((prev) => prev.filter((_, i) => i !== index)); }; const addItem = () => { setItems((prev) => [...prev, { filament_id: '', item_type: 'refill', quantity: 1 }]); }; const handleSubmit = async () => { if (!customerName.trim()) { setSubmitError('Ime kupca je obavezno'); return; } const validItems = items.filter((item) => item.filament_id !== '' && item.quantity > 0); if (validItems.length === 0) { setSubmitError('Dodajte bar jednu stavku'); return; } // Validate stock for (const item of validItems) { const filament = getFilamentById(item.filament_id); if (!filament) { setSubmitError('Nevalidan filament'); return; } const available = getAvailableStock(filament, item.item_type); if (item.quantity > available) { setSubmitError( `Nedovoljno zaliha za ${filament.boja} ${filament.tip} (${item.item_type}): dostupno ${available}, trazeno ${item.quantity}` ); return; } } const payload: CreateSaleRequest = { customer: { name: customerName.trim(), phone: customerPhone.trim() || undefined, city: customerCity.trim() || undefined, notes: customerNotes.trim() || undefined, }, items: validItems.map((item) => ({ filament_id: item.filament_id, item_type: item.item_type, quantity: item.quantity, })), notes: notes.trim() || undefined, }; try { setSubmitting(true); setSubmitError(''); await saleService.create(payload); onCreated(); } catch { setSubmitError('Greska pri kreiranju prodaje'); } finally { setSubmitting(false); } }; return (

Nova prodaja

{submitError && (
{submitError}
)} {/* Customer Section */}

Kupac

handleCustomerSearch(e.target.value)} onFocus={() => customerSuggestions.length > 0 && setShowSuggestions(true)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Ime i prezime" /> {showSuggestions && customerSuggestions.length > 0 && (
{customerSuggestions.map((c) => ( ))}
)}
setCustomerPhone(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Broj telefona" />
setCustomerCity(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Grad" />
Beleske o kupcu