Vendor dashboard and Intents added

This commit is contained in:
Sidharth Prabhu
2026-04-16 09:41:23 +05:30
parent 3d6a57593d
commit d4cef8d241
7 changed files with 830 additions and 36 deletions

View File

@@ -71,4 +71,9 @@ public class PurchaseController {
public Map<String, Object> getSummary() {
return purchaseService.getPurchaseSummary();
}
@GetMapping("/intent/summary")
public Map<String, Object> getIntentSummary() {
return purchaseService.getIntentSummary();
}
}

View File

@@ -22,4 +22,11 @@ public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Lo
@org.springframework.data.jpa.repository.Query("SELECT p.date, SUM(p.amount) FROM PurchaseOrder p GROUP BY p.date ORDER BY p.date ASC")
List<Object[]> getPurchaseTrend();
long countByStatus(String status);
@org.springframework.data.jpa.repository.Query("SELECT SUM(i.quantity) FROM PurchaseOrder p JOIN p.items i WHERE p.status = 'OPEN'")
Long countOpenItems();
List<PurchaseOrder> findTop5ByOrderByDateDesc();
}

View File

@@ -120,4 +120,20 @@ public class PurchaseService {
return summary;
}
public Map<String, Object> getIntentSummary() {
Map<String, Object> summary = new HashMap<>();
summary.put("openCount", purchaseOrderRepository.countByStatus("OPEN"));
summary.put("openItems", purchaseOrderRepository.countOpenItems());
summary.put("payableAmount", purchaseOrderRepository.getTotalBalanceAmount());
summary.put("unbilledCount", purchaseOrderRepository.countByStatus("RECEIVED")); // Assuming RECEIVED but not yet BILLED
List<PurchaseOrder> recent = purchaseOrderRepository.findTop5ByOrderByDateDesc();
summary.put("recentOrders", recent);
List<Object[]> trendData = purchaseOrderRepository.getPurchaseTrend();
summary.put("trend", trendData);
return summary;
}
}

View File

@@ -18,6 +18,8 @@ import VendorDashboard from './pages/VendorDashboard.tsx';
import PurchaseAnalytics from './pages/PurchaseAnalytics.tsx';
import Bills from './pages/Bills.tsx';
import PurchaseSummary from './pages/PurchaseSummary.tsx';
import IntentDashboard from './pages/IntentDashboard.tsx';
import IntentList from './pages/IntentList.tsx';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true';
@@ -63,6 +65,15 @@ function App() {
<Route path="purchases/vendor" element={<Vendors />} />
<Route path="purchases/analytics" element={<PurchaseAnalytics />} />
{/* Intent */}
<Route path="purchases/intent/orders-dashboard" element={<IntentDashboard title="ORDER DASHBOARD" />} />
<Route path="purchases/intent/receives-dashboard" element={<IntentDashboard title="RECEIVABLE DASHBOARD" />} />
<Route path="purchases/intent/orders" element={<IntentList title="ORDERS" />} />
<Route path="purchases/intent/receives" element={<IntentList title="RECEIVES" />} />
<Route path="purchases/intent/receives-summary" element={<PlaceholderPage title="Receives Summary" />} />
<Route path="purchases/intent/request" element={<PlaceholderPage title="Intent Request" />} />
<Route path="purchases/intent/stores" element={<PlaceholderPage title="Intent Stores" />} />
{/* Inventory */}
<Route path="inventory/base" element={<BaseMenu />} />
<Route path="inventory/products" element={<Products />} />

View File

@@ -27,11 +27,17 @@ function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
interface SubMenuItem {
title: string;
path?: string;
nestedMenu?: { title: string; path: string }[];
}
interface MenuItem {
title: string;
icon: React.ElementType;
path?: string;
subMenu?: { title: string; path: string }[];
subMenu?: SubMenuItem[];
}
const menuItems: MenuItem[] = [
@@ -53,15 +59,32 @@ const menuItems: MenuItem[] = [
]
},
{
title: 'Vendors',
title: 'Purchases',
icon: ShoppingBag,
subMenu: [
{ title: 'Dashboard', path: '/purchases/dashboard' },
{ title: 'Orders', path: '/purchases/orders' },
{ title: 'Summary', path: '/purchases/summary' },
{ title: 'Bills', path: '/purchases/bills' },
{ title: 'Vendors', path: '/purchases/vendor' },
{ title: 'Purchase Analytics', path: '/purchases/analytics' }
{
title: 'Vendor',
nestedMenu: [
{ title: 'Dashboard', path: '/purchases/dashboard' },
{ title: 'Orders', path: '/purchases/orders' },
{ title: 'Summary', path: '/purchases/summary' },
{ title: 'Bills', path: '/purchases/bills' },
{ title: 'Vendors', path: '/purchases/vendor' },
{ title: 'Purchase Analytics', path: '/purchases/analytics' }
]
},
{
title: 'Intent',
nestedMenu: [
{ title: 'Orders Dashboard', path: '/purchases/intent/orders-dashboard' },
{ title: 'Receives Dashboard', path: '/purchases/intent/receives-dashboard' },
{ title: 'Orders', path: '/purchases/intent/orders' },
{ title: 'Receives', path: '/purchases/intent/receives' },
{ title: 'Receives Summary', path: '/purchases/intent/receives-summary' },
{ title: 'Request', path: '/purchases/intent/request' },
{ title: 'Stores', path: '/purchases/intent/stores' }
]
}
]
},
{
@@ -107,6 +130,7 @@ const menuItems: MenuItem[] = [
const Sidebar = () => {
const [openMenus, setOpenMenus] = useState<string[]>([]);
const [openNestedMenus, setOpenNestedMenus] = useState<string[]>([]);
const location = useLocation();
const navigate = useNavigate();
@@ -123,7 +147,7 @@ const Sidebar = () => {
'Store Dashboard': 'dashboard',
'Sale': 'sale',
'Customers': 'customers',
'Vendors': 'purchases',
'Purchases': 'purchases',
'Inventory': 'inventory',
'Expense': 'expense',
'Reports': 'reports',
@@ -152,6 +176,14 @@ const Sidebar = () => {
);
};
const toggleNestedMenu = (title: string) => {
setOpenNestedMenus((prev) =>
prev.includes(title)
? prev.filter((item) => item !== title)
: [...prev, title]
);
};
// Check if a sub-menu should be open based on the current location
React.useEffect(() => {
const currentMenu = menuItems.find(item =>
@@ -192,7 +224,7 @@ const Sidebar = () => {
<li key={item.title}>
{item.subMenu ? (
<div>
<button
<button
onClick={() => toggleMenu(item.title)}
title={`Expand/Collapse ${item.title}`}
className={cn(
@@ -205,7 +237,7 @@ const Sidebar = () => {
<div className="flex items-center gap-3">
<item.icon size={18} strokeWidth={2} className={cn(
"transition-colors",
openMenus.includes(item.title) ? "text-[#231651]" : "text-[#64748b] group-hover:text-[#475569]"
openMenus.includes(item.title) ? "text-[#0f4475]" : "text-[#64748b] group-hover:text-[#475569]"
)} />
<span className="tracking-tight">{item.title}</span>
</div>
@@ -228,32 +260,82 @@ const Sidebar = () => {
>
{item.subMenu.map((sub) => (
<li key={sub.title}>
<NavLink
to={sub.path}
title={`Go to ${sub.title}`}
className={({ isActive }) => cn(
"flex items-center gap-3 py-2 px-4 rounded-lg text-[13px] font-medium transition-all group relative",
isActive
? "text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/10"
: "text-[#64748b] hover:text-[#1e293b] hover:translate-x-1"
)}
>
{({ isActive }) => (
<>
<div className={cn(
"w-1.5 h-1.5 rounded-full transition-all duration-300",
isActive ? "bg-[#231651] scale-125" : "bg-gray-300 group-hover:bg-gray-400"
)} />
<span>{sub.title}</span>
{isActive && (
<motion.div
layoutId="activeSubMenu"
className="absolute left-[-1.5px] top-1/4 bottom-1/4 w-[3px] bg-[#231651] rounded-full"
/>
{sub.nestedMenu ? (
<div>
<button
onClick={() => toggleNestedMenu(sub.title)}
className={cn(
"w-full flex items-center justify-between py-2 px-4 rounded-lg text-[13px] font-medium transition-all group",
openNestedMenus.includes(sub.title) ? "text-[#0f4475] bg-[#0f4475]/5" : "text-[#64748b] hover:text-[#1e293b]"
)}
</>
)}
</NavLink>
>
<div className="flex items-center gap-3">
<div className={cn(
"w-1.5 h-1.5 rounded-full",
openNestedMenus.includes(sub.title) ? "bg-[#0f4475]" : "bg-gray-300"
)} />
<span>{sub.title}</span>
</div>
<ChevronRight size={12} className={cn(
"transition-transform duration-200",
openNestedMenus.includes(sub.title) ? "rotate-90" : ""
)} />
</button>
<AnimatePresence>
{openNestedMenus.includes(sub.title) && (
<motion.ul
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="ml-4 mt-1 space-y-0.5 border-l border-[#e2e8f0]/60 overflow-hidden"
>
{sub.nestedMenu.map((nested) => (
<li key={nested.title}>
<NavLink
to={nested.path!}
className={({ isActive }) => cn(
"flex items-center gap-3 py-1.5 px-4 rounded-lg text-[12px] font-medium transition-all",
isActive ? "text-[#0f4475] bg-white shadow-sm" : "text-[#64748b] hover:text-[#1e293b]"
)}
>
<div className="w-1 h-1 rounded-full bg-gray-300" />
<span>{nested.title}</span>
</NavLink>
</li>
))}
</motion.ul>
)}
</AnimatePresence>
</div>
) : (
<NavLink
to={sub.path!}
title={`Go to ${sub.title}`}
className={({ isActive }) => cn(
"flex items-center gap-3 py-2 px-4 rounded-lg text-[13px] font-medium transition-all group relative",
isActive
? "text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/10"
: "text-[#64748b] hover:text-[#1e293b] hover:translate-x-1"
)}
>
{({ isActive }) => (
<>
<div className={cn(
"w-1.5 h-1.5 rounded-full transition-all duration-300",
isActive ? "bg-[#0f4475] scale-125" : "bg-gray-300 group-hover:bg-gray-400"
)} />
<span>{sub.title}</span>
{isActive && (
<motion.div
layoutId="activeSubMenu"
className="absolute left-[-1.5px] top-1/4 bottom-1/4 w-[3px] bg-[#0f4475] rounded-full"
/>
)}
</>
)}
</NavLink>
)}
</li>
))}
</motion.ul>

View File

@@ -0,0 +1,246 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { format } from 'date-fns';
import {
Filter,
ChevronRight,
ShoppingBag,
Package,
Wallet,
RefreshCw,
Clock,
LayoutGrid,
FileText,
Boxes,
Eye,
ArrowRight
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from 'recharts';
interface IntentDashboardProps {
title: string;
}
const StatCard = ({ icon: Icon, label, value, color }: any) => (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white p-5 rounded-2xl border border-[#e2e8f0] flex items-center gap-4 flex-1 shadow-sm hover:shadow-md transition-all"
>
<div className={`p-3 rounded-xl ${color} bg-opacity-10 text-${color.split('-')[1]}-600`}>
<Icon size={20} />
</div>
<div>
<p className="text-[11px] font-bold text-[#94a3b8] uppercase tracking-wider">{label}</p>
<h3 className="text-xl font-black text-[#1e293b]">{value}</h3>
</div>
</motion.div>
);
interface IntentStats {
openCount: number;
openItems: number;
payableAmount: number;
unbilledCount: number;
recentOrders: any[];
trend: any[];
}
const IntentDashboard: React.FC<IntentDashboardProps> = ({ title }) => {
const [stats, setStats] = useState<IntentStats | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchStats();
}, [title]);
const fetchStats = async () => {
setLoading(true);
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/intent/summary`);
const data = await response.json();
setStats(data);
} catch (error) {
console.error('Error fetching intent stats:', error);
} finally {
setLoading(false);
}
};
const chartData = stats?.trend?.map(t => ({
date: format(new Date(t[0]), 'MMM dd'),
total: t[1],
open: t[1] * 0.4 // Sample logic for visual variety if status-specific trend isn't available
})) || [];
const statusBreakdown = [
{ label: 'OPEN', value: stats?.openCount || 0 },
{ label: 'CLOSE', value: stats?.recentOrders?.filter(o => o.status === 'CLOSE').length || 0 },
{ label: 'ACKNOWLEDGE', value: 0 },
{ label: 'RECEIVED', value: stats?.unbilledCount || 0 }
];
return (
<div className="p-6 space-y-6 bg-[#f8fafc] min-h-screen font-inter">
{/* Top Header & Filter */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<h1 className="text-xl font-black text-[#0f4475] uppercase tracking-wider flex items-center gap-2">
<LayoutGrid size={20} />
{title}
</h1>
<div className="flex items-center gap-3 bg-white p-1.5 rounded-2xl border border-[#e2e8f0] shadow-sm">
<button className="flex items-center gap-2 bg-[#0f4475] text-white px-4 py-2 rounded-xl text-xs font-bold hover:bg-[#1e4e8c] transition-all shadow-md shadow-[#0f4475]/10">
<Filter size={14} /> Filter
</button>
<div className="flex items-center gap-3 px-3 border-l border-[#e2e8f0]">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-[#94a3b8] uppercase">Start:</span>
<span className="text-xs font-bold text-[#0f4475] bg-[#0f4475]/5 px-2.5 py-1 rounded-lg">2026-03-10</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-[#94a3b8] uppercase">End:</span>
<span className="text-xs font-bold text-[#0f4475] bg-[#0f4475]/5 px-2.5 py-1 rounded-lg">2026-04-10</span>
</div>
</div>
</div>
</div>
{/* Stats Grid */}
<div className="flex flex-wrap gap-4 relative">
{loading && <div className="absolute inset-0 bg-white/50 backdrop-blur-[1px] z-10 rounded-2xl flex items-center justify-center"><RefreshCw className="animate-spin text-[#0f4475]" size={24} /></div>}
<StatCard icon={Clock} label="Open" value={stats?.openCount || 0} color="bg-cyan-500" />
<StatCard icon={Boxes} label="Open Items" value={stats?.openItems || 0} color="bg-indigo-500" />
<StatCard icon={Wallet} label="Payable Amount" value={`${stats?.payableAmount?.toLocaleString() || 0}`} color="bg-amber-500" />
<StatCard icon={FileText} label="Unbilled" value={stats?.unbilledCount || 0} color="bg-rose-500" />
</div>
{/* Main Content Area */}
<div className="grid grid-cols-12 gap-6">
{/* Left Column: Chart & Breakdown */}
<div className="col-span-12 lg:col-span-8 space-y-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm relative overflow-hidden">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-[#1e293b] flex items-center gap-2">
Orders in the last 30 days
<button onClick={fetchStats} className={`text-[#94a3b8] hover:text-[#0f4475] transition-colors ${loading ? 'animate-spin' : ''}`}><RefreshCw size={14} /></button>
</h3>
<div className="flex items-center gap-4">
{['Open', 'Closed', 'Total'].map((l, i) => (
<div key={l} className="flex items-center gap-1.5">
<div className={`w-2.5 h-2.5 rounded-full ${i === 0 ? 'bg-amber-500' : i === 1 ? 'bg-rose-500' : 'bg-[#0f4475]'}`} />
<span className="text-[10px] font-bold text-[#64748b]">{l}</span>
</div>
))}
</div>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} />
<Tooltip
contentStyle={{ borderRadius: '16px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1)' }}
itemStyle={{ fontWeight: 'bold' }}
/>
<Area type="monotone" dataKey="open" stroke="#f59e0b" fill="#f59e0b" fillOpacity={0.05} strokeWidth={2} />
<Area type="monotone" dataKey="total" stroke="#0f4475" fill="#0f4475" fillOpacity={0.05} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-12">Top Items</h3>
<div className="flex flex-col items-center justify-center p-8 text-[#94a3b8]">
<Package size={48} className="opacity-10 mb-4" />
<p className="text-xs font-bold uppercase tracking-widest">No items found</p>
</div>
</div>
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-6">Status Breakdown</h3>
<div className="space-y-4">
{statusBreakdown.map(s => (
<div key={s.label} className="group border-b border-[#f1f5f9] last:border-0 pb-3 last:pb-0 flex items-center justify-between hover:bg-gray-50/50 rounded-lg p-1 transition-all cursor-pointer">
<span className="text-[11px] font-black text-[#64748b] tracking-wider">{s.label}</span>
<div className="flex items-center gap-3">
<span className="text-xs font-black text-[#1e293b]">{s.value}</span>
<button className="text-[#94a3b8] group-hover:text-[#0f4475] opacity-0 group-hover:opacity-100 transition-all"><ArrowRight size={14} /></button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Right Column: Recent & Links */}
<div className="col-span-12 lg:col-span-4 space-y-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm overflow-hidden relative">
{loading && <div className="absolute inset-0 bg-white/30 backdrop-blur-[1px] z-10" />}
<div className="flex items-center justify-between mb-6">
<h3 className="text-sm font-black text-[#1e293b]">Recent Orders</h3>
<button onClick={fetchStats} className={`text-[#38bdf8] p-1.5 hover:bg-sky-50 rounded-lg transition-all ${loading ? 'animate-spin' : ''}`}><RefreshCw size={16} /></button>
</div>
<div className="flex gap-2 mb-4 overflow-x-auto pb-2 scrollbar-hide">
{['All', 'Open', 'Billed', 'Received', 'Closed'].map((t, i) => (
<button key={t} className={`whitespace-nowrap px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all ${i === 0 ? 'bg-[#0f4475] text-white' : 'bg-[#f8fafc] text-[#64748b] hover:bg-[#f1f5f9]'}`}>
{t}
</button>
))}
</div>
<div className="space-y-3">
{(!stats?.recentOrders || stats.recentOrders.length === 0) ? (
<div className="py-12 text-center text-[#94a3b8] text-xs font-bold italic">No recent orders</div>
) : (
stats.recentOrders.map(o => (
<div key={o.id} className="p-3 bg-[#f8fafc] border border-[#f1f5f9] rounded-xl hover:shadow-md transition-all cursor-pointer group">
<div className="flex justify-between items-start mb-1">
<span className="text-[11px] font-black text-[#1e293b]">#{o.purchaseId || o.id}</span>
<span className="text-[11px] font-black text-[#1e293b]">{o.amount?.toLocaleString()}</span>
</div>
<div className="flex justify-between items-end">
<span className="text-[10px] font-semibold text-[#94a3b8]">{o.date ? format(new Date(o.date), 'MMM dd, yyyy HH:mm') : 'N/A'}</span>
<span className={`text-[9px] font-black px-1.5 py-0.5 rounded uppercase ${o.status === 'CLOSE' || o.status === 'CLOSED' ? 'text-[#ef4444] bg-[#fee2e2]' : 'text-emerald-700 bg-emerald-100'}`}>{o.status}</span>
</div>
</div>
))
)}
</div>
</div>
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-4">Quick Links</h3>
<div className="space-y-2">
{[
{ label: 'Inventory', icon: Boxes },
{ label: 'Raw Materials', icon: Package },
{ label: 'Draft Orders', icon: FileText }
].map(l => (
<button key={l.label} className="w-full flex items-center justify-between p-3.5 bg-[#f8fafc] border border-[#f1f5f9] rounded-xl hover:bg-gray-50 transition-all group">
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-lg shadow-sm text-[#94a3b8] group-hover:text-[#0f4475] transition-colors">
<l.icon size={16} />
</div>
<span className="text-xs font-bold text-[#64748b]">{l.label}</span>
</div>
<ChevronRight size={14} className="text-[#cbd5e1] group-hover:text-[#0f4475] transition-all" />
</button>
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default IntentDashboard;

View File

@@ -0,0 +1,427 @@
import React, { useState, useEffect } from 'react';
import {
Eye,
ChevronRight,
Plus,
Search,
ArrowLeft,
ChevronsRight,
ChevronLeft,
RefreshCw,
X,
Loader2,
CheckCircle,
Trash2,
ShoppingCart,
ShoppingBag
} from 'lucide-react';
import { motion } from 'framer-motion';
interface IntentListProps {
title: string;
}
const IntentList: React.FC<IntentListProps> = ({ title }) => {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [vendors, setVendors] = useState<any[]>([]);
const [isSaving, setIsSaving] = useState(false);
// New Order Form State
const initialOrderState = {
purchaseId: `PO_${Math.floor(1000 + Math.random() * 9000)}`,
date: new Date().toISOString().split('T')[0],
vendorId: '',
status: title === 'RECEIVES' ? 'RECEIVED' : 'OPEN',
discount: 0,
referenceId: '',
instruction: '',
items: [{ productName: '', quantity: 1, rate: 0, total: 0 }]
};
const [newOrder, setNewOrder] = useState(initialOrderState);
useEffect(() => {
fetchData();
fetchVendors();
}, [title]);
const fetchVendors = async () => {
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/vendors`);
if (response.ok) {
const result = await response.json();
setVendors(result);
}
} catch (error) {
console.error('Error fetching vendors:', error);
}
};
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/orders`);
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching intent list:', error);
} finally {
setLoading(false);
}
};
const handleAddItem = () => {
setNewOrder({
...newOrder,
items: [...newOrder.items, { productName: '', quantity: 1, rate: 0, total: 0 }]
});
};
const handleItemChange = (index: number, field: string, value: any) => {
const updatedItems = [...newOrder.items];
const item = { ...updatedItems[index], [field]: value };
if (field === 'quantity' || field === 'rate') {
item.total = Number(item.quantity) * Number(item.rate);
}
updatedItems[index] = item;
setNewOrder({ ...newOrder, items: updatedItems });
};
const calculateTotal = () => {
return newOrder.items.reduce((sum, item) => sum + item.total, 0) - newOrder.discount;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
try {
const payload = {
...newOrder,
vendor: { id: parseInt(newOrder.vendorId) },
amount: calculateTotal(),
date: new Date(newOrder.date).toISOString()
};
const response = await fetch(`http://${window.location.hostname}:8080/api/purchases/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.ok) {
setShowAddModal(false);
setNewOrder(initialOrderState);
fetchData();
}
} catch (error) {
console.error('Error saving order:', error);
} finally {
setIsSaving(false);
}
};
return (
<div className="p-6 bg-[#f8fafc] min-h-screen font-inter">
{/* Header Bar */}
<div className="flex items-center justify-between mb-8 bg-white p-4 rounded-3xl border border-[#e2e8f0] shadow-sm">
<div className="flex items-center gap-6">
<button className="p-2.5 bg-gray-50 rounded-xl text-[#0f4475] hover:bg-[#0f4475] hover:text-white transition-all shadow-sm">
<ArrowLeft size={18} />
</button>
<h1 className="text-xl font-black text-[#0f4475] uppercase tracking-wider">{title}</h1>
</div>
<div className="flex items-center gap-4">
{title === 'RECEIVES' && (
<button className="bg-[#0f4475] text-white px-5 py-2.5 rounded-2xl text-xs font-black shadow-lg shadow-[#0f4475]/20 hover:bg-[#1e4e8c] transition-all">
Summary
</button>
)}
<div className="relative">
<select className="bg-gray-50 border border-[#e2e8f0] rounded-2xl px-5 py-2.5 text-xs font-bold text-[#64748b] outline-none appearance-none min-w-[150px] cursor-pointer focus:bg-white focus:ring-2 focus:ring-[#0f4475]/10">
<option>Status</option>
<option>Approved</option>
<option>Pending</option>
</select>
</div>
<button
onClick={() => setShowAddModal(true)}
className="p-2.5 bg-[#0f4475] text-white rounded-xl shadow-lg shadow-[#0f4475]/20 hover:scale-105 transition-all"
>
<Plus size={18} />
</button>
</div>
</div>
{/* Table Section */}
<div className="bg-white rounded-[2rem] border border-[#e2e8f0] shadow-xl shadow-indigo-500/5 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50/50">
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Purchase ID</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Date</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Status</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Amount ()</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Store name</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Request status</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]"></th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9] relative">
{loading ? (
<tr>
<td colSpan={7} className="py-20 text-center">
<RefreshCw className="animate-spin text-[#0f4475] mx-auto mb-2" size={32} />
<p className="text-sm font-bold text-[#64748b]">Fetching orders...</p>
</td>
</tr>
) : data.length === 0 ? (
<tr>
<td colSpan={7} className="py-20 text-center text-[#94a3b8]">
<ShoppingBag className="mx-auto mb-4 opacity-10" size={64} />
<p className="text-sm font-bold uppercase tracking-widest">No orders found</p>
</td>
</tr>
) : (
data.map((row, i) => (
<motion.tr
key={i}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
className="hover:bg-gray-50/70 transition-colors group cursor-pointer"
>
<td className="px-8 py-6">
<div className="flex items-center gap-2">
<span className="text-xs font-black text-[#1e293b]">{row.purchaseId || row.id}</span>
<ChevronsRight size={12} className="text-[#cbd5e1] group-hover:text-[#0f4475] transition-colors" />
</div>
</td>
<td className="px-8 py-6 text-xs font-semibold text-[#64748b]">
{row.date ? new Date(row.date).toLocaleString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'N/A'}
</td>
<td className="px-8 py-6">
<span className={`text-[10px] font-black px-2.5 py-1 rounded-md uppercase tracking-wider ${row.status === 'CLOSE' || row.status === 'CLOSED' ? 'text-[#ef4444] bg-[#fee2e2]' : 'text-blue-600 bg-blue-50'}`}>
{row.status}
</span>
</td>
<td className="px-8 py-6 text-xs font-black text-[#1e293b]">{row.amount?.toLocaleString()}</td>
<td className="px-8 py-6 text-xs font-bold text-[#64748b]">{row.storeName || 'Main Store'}</td>
<td className="px-8 py-6">
<span className="text-[10px] font-black text-[#10b981] bg-[#dcfce7] px-2.5 py-1 rounded-md uppercase tracking-wider">APPROVED</span>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-3">
<button className="p-2 text-[#94a3b8] hover:bg-[#0f4475] hover:text-white rounded-lg transition-all">
<Eye size={16} />
</button>
<button className="p-2 text-[#94a3b8] hover:bg-gray-100 rounded-lg transition-all">
<ChevronsRight size={16} />
</button>
</div>
</td>
</motion.tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination Bar */}
<div className="bg-gray-50/50 px-8 py-4 border-t border-[#f1f5f9] flex items-center justify-end gap-8">
<div className="flex items-center gap-3 text-xs font-bold text-[#64748b]">
<span>Rows per page:</span>
<select className="bg-transparent border-0 font-black text-[#1e293b] outline-none cursor-pointer">
<option>10</option>
<option>20</option>
</select>
</div>
<div className="text-xs font-bold text-[#64748b]">
1-5 of 5
</div>
<div className="flex items-center gap-2">
<button className="p-1 text-[#cbd5e1] cursor-not-allowed"><ChevronLeft size={18} /></button>
<button className="p-1 text-[#cbd5e1] cursor-not-allowed"><ChevronRight size={18} /></button>
</div>
</div>
</div>
{/* Modern Add Order Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-[#0f4475]/30 backdrop-blur-md z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="bg-white rounded-[2.5rem] w-full max-w-5xl max-h-[90vh] shadow-2xl overflow-hidden flex flex-col border border-white/20"
>
{/* Modal Header */}
<div className="px-10 py-8 border-b border-[#f1f5f9] flex justify-between items-center bg-gray-50/30">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-gradient-to-br from-[#0f4475] to-[#1e4e8c] rounded-2xl flex items-center justify-center text-white shadow-xl shadow-[#0f4475]/20">
<ShoppingCart size={24} />
</div>
<div>
<h2 className="text-2xl font-black text-[#1e293b] tracking-tight">Create {title === 'RECEIVES' ? 'Receive' : 'Purchase'} Order</h2>
<p className="text-[11px] font-bold text-[#94a3b8] uppercase tracking-widest mt-1">New Intent Entry Draft Status</p>
</div>
</div>
<button
onClick={() => { setShowAddModal(false); setNewOrder(initialOrderState); }}
className="p-3 hover:bg-white hover:shadow-md rounded-2xl transition-all border border-transparent hover:border-[#f1f5f9] group"
>
<X size={20} className="text-[#64748b] group-hover:text-[#ef4444]" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-10 overflow-y-auto space-y-10 custom-scrollbar">
{/* Form Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Order ID</label>
<input type="text" value={newOrder.purchaseId} readOnly className="w-full px-5 py-3.5 bg-gray-50 border border-[#e2e8f0] rounded-2xl text-sm font-black text-[#0f4475] outline-none" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Date</label>
<input type="date" value={newOrder.date} onChange={e => setNewOrder({...newOrder, date: e.target.value})} className="w-full px-5 py-3.5 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold text-[#1e293b] outline-none focus:ring-4 focus:ring-[#0f4475]/5 focus:border-[#0f4475] transition-all" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Select Vendor</label>
<select
required
value={newOrder.vendorId}
onChange={e => setNewOrder({...newOrder, vendorId: e.target.value})}
className="w-full px-5 py-3.5 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold text-[#1e293b] outline-none focus:ring-4 focus:ring-[#0f4475]/5 focus:border-[#0f4475] transition-all appearance-none cursor-pointer"
>
<option value="">Choose Supplier</option>
{vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Status</label>
<div className="px-5 py-3.5 bg-[#dcfce7] border border-[#10b981]/20 rounded-2xl text-[11px] font-black text-[#10b981] uppercase tracking-widest flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-[#10b981] animate-pulse" />
{newOrder.status}
</div>
</div>
</div>
{/* Items Table */}
<div className="space-y-4">
<div className="flex items-center justify-between px-2">
<h3 className="text-xs font-black text-[#1e293b] uppercase tracking-[0.2em]">Order Line Items</h3>
<button type="button" onClick={handleAddItem} className="flex items-center gap-2 px-4 py-2 bg-[#0f4475]/5 text-[#0f4475] rounded-xl text-[11px] font-black hover:bg-[#0f4475] hover:text-white transition-all">
<Plus size={14} /> Add Product
</button>
</div>
<div className="border border-[#e2e8f0] rounded-[2rem] overflow-hidden bg-gray-50/30">
<table className="w-full text-left">
<thead className="bg-white border-b border-[#e2e8f0]">
<tr>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest">Product / Material Name</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-32 text-center">Qty</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-40">Rate ()</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-40">Subtotal</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-16"></th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{newOrder.items.map((item, idx) => (
<tr key={idx} className="group hover:bg-white transition-colors">
<td className="px-6 py-4">
<input type="text" placeholder="Start typing product name..." value={item.productName} onChange={e => handleItemChange(idx, 'productName', e.target.value)} className="w-full bg-transparent text-sm font-bold text-[#1e293b] outline-none placeholder:text-[#cbd5e1]" />
</td>
<td className="px-6 py-4">
<input type="number" min="1" value={item.quantity} onChange={e => handleItemChange(idx, 'quantity', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-black text-[#1e293b] outline-none text-center" />
</td>
<td className="px-6 py-4">
<input type="number" step="0.01" value={item.rate} onChange={e => handleItemChange(idx, 'rate', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-bold text-[#1e293b] outline-none" />
</td>
<td className="px-6 py-4">
<span className="text-sm font-black text-[#0f4475]">{item.total.toLocaleString()}</span>
</td>
<td className="px-6 py-4">
<button type="button" onClick={() => setNewOrder({...newOrder, items: newOrder.items.filter((_, i) => i !== idx)})} className="p-2 text-[#cbd5e1] hover:text-[#ef4444] transition-colors">
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Bottom Section: Notes & Totals */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-10 pt-4">
<div className="md:col-span-7 space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Reference ID / PO Number</label>
<input type="text" value={newOrder.referenceId} onChange={e => setNewOrder({...newOrder, referenceId: e.target.value})} className="w-full px-5 py-4 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold outline-none focus:border-[#0f4475] transition-all" placeholder="Enter external reference number if any..." />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Special Instructions</label>
<textarea rows={3} value={newOrder.instruction} onChange={e => setNewOrder({...newOrder, instruction: e.target.value})} className="w-full px-5 py-4 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold outline-none focus:border-[#0f4475] transition-all resize-none" placeholder="Add any specific delivery or quality requirements..." />
</div>
</div>
<div className="md:col-span-5 flex flex-col justify-end">
<div className="bg-[#1e293b] rounded-[2.5rem] p-8 text-white space-y-5 shadow-2xl shadow-[#1e293b]/20">
<div className="flex justify-between items-center text-xs opacity-60 font-bold uppercase tracking-wider">
<span>Order Subtotal</span>
<span>{newOrder.items.reduce((s, i) => s + i.total, 0).toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs opacity-60 font-bold uppercase tracking-wider">Adjustment / Discount</span>
<div className="flex items-center gap-2">
<span className="text-[#ef4444] font-black">-</span>
<input type="number" value={newOrder.discount} onChange={e => setNewOrder({...newOrder, discount: parseFloat(e.target.value) || 0})} className="w-24 text-right bg-white/10 border border-white/10 rounded-xl px-3 py-1.5 text-sm font-black text-white outline-none focus:bg-white/20 transition-all" />
</div>
</div>
<div className="h-[1px] bg-white/10 my-2"></div>
<div className="flex justify-between items-end">
<div>
<p className="text-[10px] opacity-40 font-black uppercase tracking-[0.2em] mb-1">Grand Payable</p>
<p className="text-3xl font-black tracking-tighter">{calculateTotal().toLocaleString()}</p>
</div>
<div className="text-right">
<p className="text-[10px] opacity-40 font-black uppercase tracking-[0.2em] mb-1">Total Items</p>
<p className="text-xl font-black">{newOrder.items.length}</p>
</div>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-4 pt-6">
<button
type="button"
onClick={() => { setShowAddModal(false); setNewOrder(initialOrderState); }}
className="px-10 py-5 border-2 border-[#f1f5f9] text-[#64748b] font-black rounded-2xl hover:bg-gray-50 transition-all uppercase tracking-widest text-[11px]"
>
Discard Draft
</button>
<button
type="submit"
disabled={isSaving}
className="flex-1 px-10 py-5 bg-gradient-to-r from-[#0f4475] to-[#1e4e8c] text-white font-black rounded-2xl shadow-2xl shadow-[#0f4475]/30 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-4 uppercase tracking-[0.2em] text-[11px]"
>
{isSaving ? <Loader2 className="animate-spin" size={18} /> : (
<>
<CheckCircle size={18} />
Finalize & Confirm Order
</>
)}
</button>
</div>
</form>
</motion.div>
</div>
)}
</div>
);
};
export default IntentList;