Vendor dashboard and Intents added
This commit is contained in:
@@ -71,4 +71,9 @@ public class PurchaseController {
|
|||||||
public Map<String, Object> getSummary() {
|
public Map<String, Object> getSummary() {
|
||||||
return purchaseService.getPurchaseSummary();
|
return purchaseService.getPurchaseSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/intent/summary")
|
||||||
|
public Map<String, Object> getIntentSummary() {
|
||||||
|
return purchaseService.getIntentSummary();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
@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();
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,4 +120,20 @@ public class PurchaseService {
|
|||||||
|
|
||||||
return summary;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import VendorDashboard from './pages/VendorDashboard.tsx';
|
|||||||
import PurchaseAnalytics from './pages/PurchaseAnalytics.tsx';
|
import PurchaseAnalytics from './pages/PurchaseAnalytics.tsx';
|
||||||
import Bills from './pages/Bills.tsx';
|
import Bills from './pages/Bills.tsx';
|
||||||
import PurchaseSummary from './pages/PurchaseSummary.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 ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true';
|
const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true';
|
||||||
@@ -63,6 +65,15 @@ function App() {
|
|||||||
<Route path="purchases/vendor" element={<Vendors />} />
|
<Route path="purchases/vendor" element={<Vendors />} />
|
||||||
<Route path="purchases/analytics" element={<PurchaseAnalytics />} />
|
<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 */}
|
{/* Inventory */}
|
||||||
<Route path="inventory/base" element={<BaseMenu />} />
|
<Route path="inventory/base" element={<BaseMenu />} />
|
||||||
<Route path="inventory/products" element={<Products />} />
|
<Route path="inventory/products" element={<Products />} />
|
||||||
|
|||||||
@@ -27,11 +27,17 @@ function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SubMenuItem {
|
||||||
|
title: string;
|
||||||
|
path?: string;
|
||||||
|
nestedMenu?: { title: string; path: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
title: string;
|
title: string;
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
path?: string;
|
path?: string;
|
||||||
subMenu?: { title: string; path: string }[];
|
subMenu?: SubMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
@@ -53,15 +59,32 @@ const menuItems: MenuItem[] = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Vendors',
|
title: 'Purchases',
|
||||||
icon: ShoppingBag,
|
icon: ShoppingBag,
|
||||||
subMenu: [
|
subMenu: [
|
||||||
{ title: 'Dashboard', path: '/purchases/dashboard' },
|
{
|
||||||
{ title: 'Orders', path: '/purchases/orders' },
|
title: 'Vendor',
|
||||||
{ title: 'Summary', path: '/purchases/summary' },
|
nestedMenu: [
|
||||||
{ title: 'Bills', path: '/purchases/bills' },
|
{ title: 'Dashboard', path: '/purchases/dashboard' },
|
||||||
{ title: 'Vendors', path: '/purchases/vendor' },
|
{ title: 'Orders', path: '/purchases/orders' },
|
||||||
{ title: 'Purchase Analytics', path: '/purchases/analytics' }
|
{ 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 Sidebar = () => {
|
||||||
const [openMenus, setOpenMenus] = useState<string[]>([]);
|
const [openMenus, setOpenMenus] = useState<string[]>([]);
|
||||||
|
const [openNestedMenus, setOpenNestedMenus] = useState<string[]>([]);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -123,7 +147,7 @@ const Sidebar = () => {
|
|||||||
'Store Dashboard': 'dashboard',
|
'Store Dashboard': 'dashboard',
|
||||||
'Sale': 'sale',
|
'Sale': 'sale',
|
||||||
'Customers': 'customers',
|
'Customers': 'customers',
|
||||||
'Vendors': 'purchases',
|
'Purchases': 'purchases',
|
||||||
'Inventory': 'inventory',
|
'Inventory': 'inventory',
|
||||||
'Expense': 'expense',
|
'Expense': 'expense',
|
||||||
'Reports': 'reports',
|
'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
|
// Check if a sub-menu should be open based on the current location
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const currentMenu = menuItems.find(item =>
|
const currentMenu = menuItems.find(item =>
|
||||||
@@ -192,7 +224,7 @@ const Sidebar = () => {
|
|||||||
<li key={item.title}>
|
<li key={item.title}>
|
||||||
{item.subMenu ? (
|
{item.subMenu ? (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleMenu(item.title)}
|
onClick={() => toggleMenu(item.title)}
|
||||||
title={`Expand/Collapse ${item.title}`}
|
title={`Expand/Collapse ${item.title}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -205,7 +237,7 @@ const Sidebar = () => {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<item.icon size={18} strokeWidth={2} className={cn(
|
<item.icon size={18} strokeWidth={2} className={cn(
|
||||||
"transition-colors",
|
"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>
|
<span className="tracking-tight">{item.title}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,32 +260,82 @@ const Sidebar = () => {
|
|||||||
>
|
>
|
||||||
{item.subMenu.map((sub) => (
|
{item.subMenu.map((sub) => (
|
||||||
<li key={sub.title}>
|
<li key={sub.title}>
|
||||||
<NavLink
|
{sub.nestedMenu ? (
|
||||||
to={sub.path}
|
<div>
|
||||||
title={`Go to ${sub.title}`}
|
<button
|
||||||
className={({ isActive }) => cn(
|
onClick={() => toggleNestedMenu(sub.title)}
|
||||||
"flex items-center gap-3 py-2 px-4 rounded-lg text-[13px] font-medium transition-all group relative",
|
className={cn(
|
||||||
isActive
|
"w-full flex items-center justify-between py-2 px-4 rounded-lg text-[13px] font-medium transition-all group",
|
||||||
? "text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/10"
|
openNestedMenus.includes(sub.title) ? "text-[#0f4475] bg-[#0f4475]/5" : "text-[#64748b] hover:text-[#1e293b]"
|
||||||
: "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"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
>
|
||||||
)}
|
<div className="flex items-center gap-3">
|
||||||
</NavLink>
|
<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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</motion.ul>
|
</motion.ul>
|
||||||
|
|||||||
246
frontend/src/pages/IntentDashboard.tsx
Normal file
246
frontend/src/pages/IntentDashboard.tsx
Normal 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;
|
||||||
427
frontend/src/pages/IntentList.tsx
Normal file
427
frontend/src/pages/IntentList.tsx
Normal 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;
|
||||||
Reference in New Issue
Block a user