Ritz token page added
This commit is contained in:
@@ -213,7 +213,7 @@ const ArchivedOrders: React.FC = () => {
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] text-slate-400 font-medium uppercase mb-0.5">Grand Total</div>
|
||||
<div className="text-lg font-black text-slate-900 leading-none">₹{order.totalAmount.toFixed(2)}</div>
|
||||
<div className="text-lg font-black text-slate-900 leading-none">R{order.totalAmount.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -325,8 +325,8 @@ const ArchivedOrders: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-black text-slate-900">₹{(item.price * item.quantity).toLocaleString()}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">@ ₹{item.price}</div>
|
||||
<div className="text-sm font-black text-slate-900">R{(item.price * item.quantity).toLocaleString()}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">@ R{item.price}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -377,7 +377,7 @@ const ArchivedOrders: React.FC = () => {
|
||||
</div>
|
||||
<div className="text-right flex flex-col gap-1 min-w-[250px]">
|
||||
<div className="text-[10px] text-slate-500 uppercase font-black tracking-widest mb-1">Final Settlement</div>
|
||||
<div className="text-4xl font-black text-emerald-400 leading-none mb-1">₹{selectedOrder.totalAmount.toLocaleString()}</div>
|
||||
<div className="text-4xl font-black text-emerald-400 leading-none mb-1">R{selectedOrder.totalAmount.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-slate-500 font-bold uppercase tracking-[0.2em]">Transaction Fully Reconciled</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -207,7 +207,7 @@ const Dashboard = () => {
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
||||
<XAxis dataKey="time" axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} tickFormatter={(v) => v >= 1000 ? `₹${v/1000}k` : `₹${v}`} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} tickFormatter={(v) => v >= 1000 ? `R${v/1000}k` : `R${v}`} />
|
||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} />
|
||||
<Area type="monotone" dataKey="value" stroke="#f43f5e" strokeWidth={3} fillOpacity={1} fill="url(#colorSalesMain)" />
|
||||
</AreaChart>
|
||||
@@ -245,9 +245,9 @@ const Dashboard = () => {
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.periodRevenue.toLocaleString()}</h2>
|
||||
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">R{(stats.periodRevenue || 0).toLocaleString()}</h2>
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{timeRange === 'Today' ? 'Today' : timeRange}</p>
|
||||
<p className="text-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">Total: ₹{stats.totalSales.toLocaleString()}</p>
|
||||
<p className="text-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">Total: R{(stats.totalSales || 0).toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="flex gap-6 mt-2">
|
||||
{pieData.map(item => (
|
||||
@@ -412,7 +412,7 @@ const Dashboard = () => {
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[9px] font-black text-rose-500 uppercase tracking-widest opacity-60">Gross Sale</p>
|
||||
<p className="text-lg font-black text-slate-800 tracking-tighter">₹{Number(store.sale).toLocaleString()}</p>
|
||||
<p className="text-lg font-black text-slate-800 tracking-tighter">R{Number(store.sale).toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[9px] font-black text-blue-500 uppercase tracking-widest opacity-60">Volume</p>
|
||||
|
||||
@@ -277,7 +277,7 @@ const NewArrivals: React.FC = () => {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] uppercase font-black text-[#64748b] mb-2 tracking-widest ml-1">Live Selling Price (₹)</label>
|
||||
<label className="block text-[10px] uppercase font-black text-[#64748b] mb-2 tracking-widest ml-1">Live Selling Price (R)</label>
|
||||
<input required type="number" value={formData.price} onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} className="w-full px-5 py-4 bg-[#231651]/5 border-2 border-transparent focus:border-[#231651]/20 rounded-2xl text-lg font-black text-[#231651] outline-none transition-all" />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -465,7 +465,7 @@ const Orders: React.FC = () => {
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] text-slate-400 font-medium uppercase mb-0.5">Grand Total</div>
|
||||
<div className="text-lg font-black text-slate-900 leading-none">₹{order.totalAmount.toFixed(2)}</div>
|
||||
<div className="text-lg font-black text-slate-900 leading-none">R{order.totalAmount.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -628,9 +628,9 @@ const Orders: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-black text-slate-900">₹{(item.price * item.quantity).toLocaleString()}</div>
|
||||
<div className="text-sm font-black text-slate-900">R{(item.price * item.quantity).toLocaleString()}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400">
|
||||
{item.quantity} x <span className="text-indigo-400 font-black">₹{item.price}</span>
|
||||
{item.quantity} x <span className="text-indigo-400 font-black">R{item.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -700,7 +700,7 @@ const Orders: React.FC = () => {
|
||||
|
||||
<div className="text-right flex flex-col gap-1 min-w-[250px]">
|
||||
<div className="text-[10px] text-indigo-400 uppercase font-black tracking-widest mb-1">Active Grand Total</div>
|
||||
<div className="text-4xl font-black text-emerald-400 leading-none mb-1">₹{selectedOrder.totalAmount.toLocaleString()}</div>
|
||||
<div className="text-4xl font-black text-emerald-400 leading-none mb-1">R{selectedOrder.totalAmount.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-white/40 font-bold uppercase tracking-[0.2em]">Transaction Pending Approval</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -784,7 +784,7 @@ const Orders: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-slate-800 text-sm">{item.productName}</div>
|
||||
<div className="text-[10px] font-black text-indigo-400 uppercase">₹{item.price} each</div>
|
||||
<div className="text-[10px] font-black text-indigo-400 uppercase">R{item.price} each</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -805,7 +805,7 @@ const Orders: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-right min-w-[80px]">
|
||||
<div className="text-sm font-black text-slate-900 leading-none mb-1">₹{(item.price * item.quantity).toFixed(2)}</div>
|
||||
<div className="text-sm font-black text-slate-900 leading-none mb-1">R{(item.price * item.quantity).toFixed(2)}</div>
|
||||
<button
|
||||
onClick={() => removeItem(idx)}
|
||||
className="text-[10px] font-black text-rose-400 hover:text-rose-600 uppercase tracking-widest transition-colors flex items-center gap-1 active:scale-95"
|
||||
@@ -848,7 +848,7 @@ const Orders: React.FC = () => {
|
||||
<div className="font-bold text-slate-800 text-sm group-hover:text-indigo-600 transition-colors">{product.name}</div>
|
||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{product.category}</div>
|
||||
</div>
|
||||
<div className="font-black text-emerald-600 text-xs">₹{product.price}</div>
|
||||
<div className="font-black text-emerald-600 text-xs">R{product.price}</div>
|
||||
</button>
|
||||
))
|
||||
) : editSearchQuery ? (
|
||||
@@ -866,7 +866,7 @@ const Orders: React.FC = () => {
|
||||
<div className="mt-8 pt-8 border-t border-slate-200">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">New Order Total</span>
|
||||
<span className="text-2xl font-black text-emerald-600 tracking-tighter">₹{editTotal.toFixed(2)}</span>
|
||||
<span className="text-2xl font-black text-emerald-600 tracking-tighter">R{editTotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={saveOrderEdits}
|
||||
|
||||
@@ -395,7 +395,7 @@ const Products = () => {
|
||||
})()}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm font-bold text-[#1e293b]">₹{product.price}</td>
|
||||
<td className="px-6 py-4 text-sm font-bold text-[#1e293b]">R{product.price}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold w-fit ${product.active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}`}>
|
||||
@@ -472,7 +472,7 @@ const Products = () => {
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-[11px] uppercase font-bold text-[#64748b] mb-1.5">Base Price</label><input type="number" value={formData.basePrice} onChange={(e) => setFormData({ ...formData, basePrice: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm" /></div>
|
||||
<div><label className="block text-[11px] uppercase font-bold text-[#64748b] mb-1.5">Sale Price</label><input type="number" value={formData.price} onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm font-bold" /></div>
|
||||
<div><label className="block text-[11px] uppercase font-bold text-[#64748b] mb-1.5">Sale Price (R)</label><input type="number" value={formData.price} onChange={(e) => setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm font-bold" /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-[11px] uppercase font-bold text-[#64748b] mb-1.5">Discount %</label><input type="number" value={formData.discountPercent} onChange={(e) => setFormData({ ...formData, discountPercent: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm" /></div>
|
||||
|
||||
235
frontend/src/pages/Ritz.tsx
Normal file
235
frontend/src/pages/Ritz.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Building2,
|
||||
CircleDollarSign,
|
||||
TrendingUp,
|
||||
Users,
|
||||
ArrowUpRight,
|
||||
ArrowDownLeft,
|
||||
Clock,
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Fingerprint
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import colorVector from '../assets/color-vector.png';
|
||||
|
||||
interface TokenTransaction {
|
||||
id: number;
|
||||
amount: number;
|
||||
type: 'TOPUP' | 'SPEND' | 'REFUND';
|
||||
description: string;
|
||||
timestamp: string;
|
||||
referenceId: string;
|
||||
user: {
|
||||
id: number;
|
||||
name: string;
|
||||
mobileNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
totalCirculation: number;
|
||||
activeWallets: number;
|
||||
totalUsers: number;
|
||||
serializedUnitsTotal: number;
|
||||
}
|
||||
|
||||
const RitzPage: React.FC = () => {
|
||||
const [transactions, setTransactions] = useState<TokenTransaction[]>([]);
|
||||
const [stats, setStats] = useState<Stats>({
|
||||
totalCirculation: 0,
|
||||
activeWallets: 0,
|
||||
totalUsers: 0,
|
||||
serializedUnitsTotal: 0
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const host = window.location.hostname;
|
||||
|
||||
const statsRes = await fetch(`http://${host}:8080/api/wallet/stats`);
|
||||
const statsData = await statsRes.json();
|
||||
setStats(statsData);
|
||||
|
||||
const transRes = await fetch(`http://${host}:8080/api/wallet/transactions/all`);
|
||||
const transData = await transRes.json();
|
||||
setTransactions(Array.isArray(transData) ? transData : []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Ritz data:', error);
|
||||
setTransactions([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTransactions = Array.isArray(transactions) ? transactions.filter(t =>
|
||||
t.user?.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
t.user?.mobileNumber?.includes(searchTerm) ||
|
||||
t.referenceId?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
) : [];
|
||||
|
||||
return (
|
||||
<div className="p-8 bg-[#f8fafc] min-h-screen font-inter animate-slideUp">
|
||||
{/* Premium Header Banner */}
|
||||
<div className="relative overflow-hidden bg-[#231651] rounded-3xl p-8 mb-8 text-white shadow-2xl shadow-indigo-200">
|
||||
<img
|
||||
src={colorVector}
|
||||
alt="Vector BG"
|
||||
className="absolute right-0 top-0 h-full w-auto opacity-30 object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<div className="p-3 bg-white/10 backdrop-blur-md rounded-2xl border border-white/20">
|
||||
<CircleDollarSign size={32} />
|
||||
</div>
|
||||
<h1 className="text-3xl font-black tracking-tight">Ritz Ecosystem</h1>
|
||||
</div>
|
||||
<p className="text-indigo-100/80 max-w-lg font-medium">
|
||||
Monitor the heartbeat of the Ritz digital economy. Track circulation,
|
||||
active wallets, and system-wide transactions in real-time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
||||
{[
|
||||
{ label: 'Tokens in Circulation', value: `R${(stats.totalCirculation || 0).toLocaleString()}`, icon: TrendingUp, color: 'text-emerald-600', bg: 'bg-emerald-50' },
|
||||
{ label: 'Active Token Wallets', value: (stats.activeWallets || 0).toLocaleString(), icon: Users, color: 'text-blue-600', bg: 'bg-blue-50' },
|
||||
{ label: 'Serialized Audit Units', value: (stats.serializedUnitsTotal || 0).toLocaleString(), icon: Fingerprint, color: 'text-amber-600', bg: 'bg-amber-50' },
|
||||
{ label: 'Total Accounts', value: (stats.totalUsers || 0).toLocaleString(), icon: Building2, color: 'text-indigo-600', bg: 'bg-indigo-50' }
|
||||
].map((item, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-slate-500 mb-1">{item.label}</p>
|
||||
<h3 className={`text-3xl font-black ${item.color}`}>{item.value}</h3>
|
||||
</div>
|
||||
<div className={`p-4 ${item.bg} ${item.color} rounded-2xl`}>
|
||||
<item.icon size={28} />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Table Section */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-slate-800">Master Transaction Log</h2>
|
||||
<p className="text-sm text-slate-500 font-medium">Detailed history of all wallet activities</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search user or ref..."
|
||||
className="pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-indigo-50 focus:border-indigo-500 focus:bg-white outline-none transition-all w-64"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-slate-600 hover:bg-slate-100 transition-colors">
|
||||
<Filter size={18} />
|
||||
</button>
|
||||
<button className="flex items-center gap-2 px-4 py-2.5 bg-[#231651] text-white rounded-xl font-bold text-sm shadow-lg shadow-indigo-100 hover:scale-[1.02] active:scale-95 transition-all">
|
||||
<Download size={18} /> Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{isLoading ? (
|
||||
<div className="p-24 flex flex-col items-center justify-center text-slate-400">
|
||||
<div className="w-12 h-12 border-4 border-indigo-100 border-t-indigo-500 rounded-full animate-spin mb-4" />
|
||||
<span className="font-bold">Syncing Transaction Feed...</span>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50/50">
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Customer</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Type</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Amount</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Description</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{filteredTransactions.map((t, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-indigo-100 rounded-xl flex items-center justify-center text-indigo-700 font-black text-xs">
|
||||
{t.user.name.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-slate-800">{t.user.name}</p>
|
||||
<p className="text-[11px] text-slate-500 font-medium">{t.user.mobileNumber}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tight ${
|
||||
t.type === 'TOPUP' ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'
|
||||
}`}>
|
||||
{t.type === 'TOPUP' ? <ArrowUpRight size={12} /> : <ArrowDownLeft size={12} />}
|
||||
{t.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`text-base font-black ${t.type === 'TOPUP' ? 'text-emerald-600' : 'text-slate-800'}`}>
|
||||
{t.type === 'TOPUP' ? '+' : '-'} {t.amount.toLocaleString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-sm font-bold text-slate-700">{t.description}</p>
|
||||
<p className="text-[11px] text-slate-400 font-medium font-mono">{t.referenceId}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2 text-[13px] font-bold text-slate-500">
|
||||
<Clock size={14} className="opacity-60" />
|
||||
{new Date(t.timestamp).toLocaleString('en-IN', {
|
||||
day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animation Styles */}
|
||||
<style>{`
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-slideUp {
|
||||
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RitzPage;
|
||||
287
frontend/src/pages/RitzCirculation.tsx
Normal file
287
frontend/src/pages/RitzCirculation.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
CircleDollarSign,
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Fingerprint,
|
||||
ShieldCheck,
|
||||
History,
|
||||
Lock,
|
||||
Unlock,
|
||||
Users,
|
||||
ChevronLeft,
|
||||
ChevronRight
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import colorVector from '../assets/color-vector.png';
|
||||
|
||||
interface TokenUnit {
|
||||
id: number;
|
||||
tokenHash: string;
|
||||
ownerId: number;
|
||||
status: 'ACTIVE' | 'SPENT' | 'REVOKED';
|
||||
createdAt: string;
|
||||
spentAt: string | null;
|
||||
}
|
||||
|
||||
interface PageResponse {
|
||||
content: TokenUnit[];
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const RitzCirculation: React.FC = () => {
|
||||
const [tokens, setTokens] = useState<TokenUnit[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// Pagination State
|
||||
const [page, setPage] = useState(0);
|
||||
const [size] = useState(20);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [totalElements, setTotalElements] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCirculation();
|
||||
}, [page, size]);
|
||||
|
||||
const fetchCirculation = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const host = window.location.hostname;
|
||||
const res = await fetch(`http://${host}:8080/api/wallet/circulation?page=${page}&size=${size}`);
|
||||
const data: PageResponse = await res.json();
|
||||
|
||||
setTokens(data.content || []);
|
||||
setTotalPages(data.totalPages || 0);
|
||||
setTotalElements(data.totalElements || 0);
|
||||
} catch (error) {
|
||||
console.error('Error fetching circulation:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTokens = tokens.filter(t =>
|
||||
t.tokenHash.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
t.ownerId?.toString().includes(searchTerm)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-8 bg-[#f8fafc] min-h-screen font-inter animate-slideUp pb-24">
|
||||
{/* Regulated Header */}
|
||||
<div className="relative overflow-hidden bg-[#0d0d1e] rounded-3xl p-8 mb-8 text-white shadow-2xl border border-white/5">
|
||||
<img
|
||||
src={colorVector}
|
||||
alt="Vector BG"
|
||||
className="absolute right-[-10%] top-[-20%] h-[150%] w-auto opacity-10 object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<div className="p-3 bg-red-500/10 backdrop-blur-md rounded-2xl border border-red-500/20">
|
||||
<ShieldCheck size={32} className="text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black tracking-tight">Ritz Forensic Ledger</h1>
|
||||
<div className="flex items-center gap-2 mt-1 px-3 py-1 bg-red-500/20 rounded-full w-fit border border-red-500/20">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-red-400">Strictly Regulated</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-slate-400 max-w-lg font-medium mt-4">
|
||||
A comprehensive, non-repudiable audit trail of every individual Ritz unit.
|
||||
Each token is uniquely serialized and traceable to its point of issuance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Circulation Stats - Summary level only as we now have pagination */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-indigo-50 text-indigo-600 rounded-lg">
|
||||
<Fingerprint size={18} />
|
||||
</div>
|
||||
<span className="text-[11px] font-black uppercase tracking-wider text-slate-400">Total Audit Population</span>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-indigo-600">{totalElements.toLocaleString()} Units</h3>
|
||||
</div>
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-emerald-50 text-emerald-600 rounded-lg">
|
||||
<Unlock size={18} />
|
||||
</div>
|
||||
<span className="text-[11px] font-black uppercase tracking-wider text-slate-400">Ledger Compliance</span>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-emerald-600">100% Validated</h3>
|
||||
</div>
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-slate-50 text-slate-600 rounded-lg">
|
||||
<History size={18} />
|
||||
</div>
|
||||
<span className="text-[11px] font-black uppercase tracking-wider text-slate-400">Current View Range</span>
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-600">Page {page + 1} / {totalPages || 1}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ledger Table */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden min-h-[400px]">
|
||||
<div className="p-6 border-b border-slate-100 flex flex-col md:flex-row justify-between items-center gap-4 bg-slate-50/30">
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-slate-800">Circulation Audit</h2>
|
||||
<p className="text-sm text-slate-500 font-medium">Page {page + 1} of {totalPages} (Showing {tokens.length} records)</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search Serial Hash..."
|
||||
className="pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-indigo-50 focus:border-indigo-500 outline-none transition-all w-72 shadow-sm"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-l border-slate-200 ml-2 pl-4">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(0, p - 1))}
|
||||
disabled={page === 0 || isLoading}
|
||||
className="p-2 bg-white border border-slate-200 rounded-xl text-slate-600 hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1 || isLoading}
|
||||
className="p-2 bg-white border border-slate-200 rounded-xl text-slate-600 hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2.5 bg-[#0d0d1e] text-white rounded-xl font-bold text-sm shadow-lg hover:scale-[1.02] active:scale-95 transition-all ml-2">
|
||||
<Download size={18} /> Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto overflow-y-hidden">
|
||||
{isLoading ? (
|
||||
<div className="p-24 flex flex-col items-center justify-center text-slate-400 uppercase tracking-widest text-xs font-black">
|
||||
<div className="w-10 h-10 border-4 border-slate-100 border-t-red-500 rounded-full animate-spin mb-4" />
|
||||
<span>Verifying Ledger Integrity...</span>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50/80">
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Transaction Unit ID</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Cryptographic Serial</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Owner UID</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Status</th>
|
||||
<th className="px-6 py-4 text-[11px] font-black uppercase tracking-wider text-slate-400">Issuance Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{filteredTokens.map((t, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="text-[13px] font-black text-slate-400 font-mono">#{t.id.toString().padStart(6, '0')}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1 px-2 bg-indigo-50 border border-indigo-100 rounded text-[11px] font-black text-indigo-700 font-mono">
|
||||
{t.tokenHash}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2 text-sm font-bold text-slate-600">
|
||||
<Users size={14} className="opacity-40" />
|
||||
{t.ownerId || 'SYSTEM'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tight ${
|
||||
t.status === 'ACTIVE' ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-500 line-through'
|
||||
}`}>
|
||||
{t.status === 'ACTIVE' ? <Unlock size={10} /> : <Lock size={10} />}
|
||||
{t.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2 text-[12px] font-bold text-slate-500">
|
||||
<History size={14} className="opacity-40" />
|
||||
{new Date(t.createdAt).toLocaleString('en-IN', {
|
||||
day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination Footer - Sticky style */}
|
||||
<div className="fixed bottom-0 left-64 right-0 p-4 bg-white/80 backdrop-blur-md border-t border-slate-200 flex justify-center items-center gap-4 z-40">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage(0)}
|
||||
disabled={page === 0 || isLoading}
|
||||
className="px-3 py-1.5 bg-slate-100 border border-slate-200 rounded-lg text-[11px] font-black text-slate-600 hover:bg-slate-200 disabled:opacity-40 transition-all uppercase"
|
||||
>
|
||||
First
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(0, p - 1))}
|
||||
disabled={page === 0 || isLoading}
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-indigo-600 text-white rounded-xl text-sm font-bold hover:bg-indigo-700 disabled:opacity-40 transition-all shadow-md shadow-indigo-100"
|
||||
>
|
||||
<ChevronLeft size={16} /> Prev
|
||||
</button>
|
||||
|
||||
<div className="px-6 py-1.5 bg-slate-50 border border-slate-200 rounded-xl text-sm font-black text-slate-800 flex items-center gap-2 min-w-[140px] justify-center">
|
||||
{isLoading ? '...' : `Page ${page + 1} of ${totalPages || 1}`}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1 || isLoading}
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-indigo-600 text-white rounded-xl text-sm font-bold hover:bg-indigo-700 disabled:opacity-40 transition-all shadow-md shadow-indigo-100"
|
||||
>
|
||||
Next <ChevronRight size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(totalPages - 1)}
|
||||
disabled={page >= totalPages - 1 || isLoading}
|
||||
className="px-3 py-1.5 bg-slate-100 border border-slate-200 rounded-lg text-[11px] font-black text-slate-600 hover:bg-slate-200 disabled:opacity-40 transition-all uppercase"
|
||||
>
|
||||
Last
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animation Styles */}
|
||||
<style>{`
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-slideUp {
|
||||
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RitzCirculation;
|
||||
@@ -639,7 +639,7 @@ const Stalls: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-[#1e293b] text-sm">{product.name}</p>
|
||||
<p className="text-[10px] font-bold text-[#94a3b8] uppercase tracking-wider">₹{product.price} • {product.category}</p>
|
||||
<p className="text-[10px] font-bold text-[#94a3b8] uppercase tracking-wider">R{product.price} • {product.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center border-2 transition-all ${isSelected ? 'bg-[#231651] border-[#231651]' : 'border-slate-200 bg-white'}`}>
|
||||
|
||||
@@ -185,7 +185,7 @@ const StoreDashboard = () => {
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
||||
<XAxis dataKey="time" axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} tickFormatter={(v) => v >= 1000 ? `${v/1000}k` : v} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} tickFormatter={(v) => v >= 1000 ? `R${v/1000}k` : `R${v}`} />
|
||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} />
|
||||
<Area type="monotone" dataKey="value" stroke="#f43f5e" strokeWidth={3} fillOpacity={1} fill="url(#colorSalesStore)" />
|
||||
</AreaChart>
|
||||
@@ -216,7 +216,7 @@ const StoreDashboard = () => {
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{formatCurrency(stats.totalSales)}</h2>
|
||||
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">R{formatCurrency(stats.totalSales)}</h2>
|
||||
</div>
|
||||
<div className="flex gap-6 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user