From 36ea126993ce7beeacb70bd0083367306cf6d1ab Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 6 Jun 2026 19:49:20 +0700 Subject: [PATCH] feat(admin): enhance operations with search and sales dashboard --- apps/admin/src/pages/CustomersPage.tsx | 27 +++++++++++-- apps/admin/src/pages/DashboardPage.tsx | 32 +++++++++++---- apps/admin/src/pages/OrdersPage.tsx | 54 +++++++++++++++++++------- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/apps/admin/src/pages/CustomersPage.tsx b/apps/admin/src/pages/CustomersPage.tsx index c5843c5..488f166 100644 --- a/apps/admin/src/pages/CustomersPage.tsx +++ b/apps/admin/src/pages/CustomersPage.tsx @@ -17,6 +17,7 @@ interface Customer { export default function CustomersPage() { const [customers, setCustomers] = useState([]) const [loading, setLoading] = useState(true) + const [searchTerm, setSearchTerm] = useState('') useEffect(() => { fetchCustomers() @@ -34,12 +35,30 @@ export default function CustomersPage() { } } + const filteredCustomers = customers.filter(c => { + const term = searchTerm.toLowerCase() + return ( + (c.displayName || '').toLowerCase().includes(term) || + (c.phone || '').includes(term) || + (c.email || '').toLowerCase().includes(term) + ) + }) + if (loading) return
Loading Customers...
return (
-
+

LINE Customers

+
+ setSearchTerm(e.target.value)} + className="w-full bg-white border border-slate-200 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors" + /> +
@@ -54,14 +73,14 @@ export default function CustomersPage() { - {customers.length === 0 ? ( + {filteredCustomers.length === 0 ? ( - No customers found yet. + No customers found matching your search. ) : ( - customers.map(customer => ( + filteredCustomers.map(customer => (
diff --git a/apps/admin/src/pages/DashboardPage.tsx b/apps/admin/src/pages/DashboardPage.tsx index 4d61b69..594c026 100644 --- a/apps/admin/src/pages/DashboardPage.tsx +++ b/apps/admin/src/pages/DashboardPage.tsx @@ -7,29 +7,45 @@ export default function DashboardPage() { products: 0, categories: 0, scents: 0, - avgPrice: 0 + avgPrice: 0, + orders: 0, + revenue: 0, + customers: 0 }) const [loading, setLoading] = useState(true) useEffect(() => { const fetchData = async () => { try { - const [catRes, prodRes] = await Promise.all([ + const [catRes, prodRes, orderRes, customerRes] = await Promise.all([ fetch(`${API_BASE}/categories`), - fetch(`${API_BASE}/products`) + fetch(`${API_BASE}/products`), + fetch(`${API_BASE}/orders`), + fetch(`${API_BASE}/customers`) ]) const cats: Category[] = await catRes.json() const prods: Product[] = await prodRes.json() + const orders = await orderRes.json() + const customers = await customerRes.json() const scents = new Set(prods.map(p => p.scent.id)).size const totalProducts = prods.length const avgPrice = prods.reduce((acc, curr) => acc + (curr.price || 0), 0) / (totalProducts || 1) + + const validOrders = Array.isArray(orders) ? orders : [] + const totalOrders = validOrders.length + const revenue = validOrders.reduce((acc: number, curr: any) => acc + (curr.status === 'PAID' || curr.status === 'SHIPPED' ? curr.totalAmount : 0), 0) + + const totalCustomers = Array.isArray(customers) ? customers.length : 0 setStats({ products: totalProducts, categories: cats.length, scents, - avgPrice + avgPrice, + orders: totalOrders, + revenue, + customers: totalCustomers }) } catch (err) { console.error('Dashboard data error:', err) @@ -41,10 +57,10 @@ export default function DashboardPage() { }, []) const cards = [ - { label: 'Total Products', value: stats.products, icon: Package, color: 'text-blue-400' }, - { label: 'Active Categories', value: stats.categories, icon: Layers, color: 'text-cyan-glow' }, - { label: 'Unique Scents', value: stats.scents, icon: Tag, color: 'text-purple-400' }, - { label: 'Avg Unit Price', value: `฿${Math.round(stats.avgPrice).toLocaleString()}`, icon: TrendingUp, color: 'text-emerald-400' }, + { label: 'Total Revenue', value: `฿${stats.revenue.toLocaleString()}`, icon: TrendingUp, color: 'text-emerald-400' }, + { label: 'Total Orders', value: stats.orders, icon: Package, color: 'text-blue-400' }, + { label: 'Total Customers', value: stats.customers, icon: Tag, color: 'text-purple-400' }, + { label: 'Total Products', value: stats.products, icon: Layers, color: 'text-cyan-glow' }, ] return ( diff --git a/apps/admin/src/pages/OrdersPage.tsx b/apps/admin/src/pages/OrdersPage.tsx index 64ef7a2..ae831d1 100644 --- a/apps/admin/src/pages/OrdersPage.tsx +++ b/apps/admin/src/pages/OrdersPage.tsx @@ -1,12 +1,13 @@ import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { API_BASE, formatPrice, type Order } from '@homaroma/types' -import { ChevronRight, Clock, CheckCircle2, Truck, XCircle } from 'lucide-react' +import { Search, ChevronRight, Clock, CheckCircle2, Truck, XCircle } from 'lucide-react' export default function OrdersPage() { const [orders, setOrders] = useState([]) const [loading, setLoading] = useState(true) const [filter, setFilter] = useState('ALL') + const [searchTerm, setSearchTerm] = useState('') const fetchData = async () => { setLoading(true) @@ -25,7 +26,17 @@ export default function OrdersPage() { fetchData() }, []) - const filteredOrders = orders.filter(o => filter === 'ALL' || o.status === filter) + const filteredOrders = orders.filter(o => { + const matchesFilter = filter === 'ALL' || o.status === filter + const searchLower = searchTerm.toLowerCase() + const matchesSearch = + o.id.toString().includes(searchLower) || + o.customerName.toLowerCase().includes(searchLower) || + o.customerPhone.includes(searchLower) || + (o.customer?.displayName?.toLowerCase() || '').includes(searchLower) + + return matchesFilter && matchesSearch + }) const StatusIcon = ({ status }: { status: string }) => { switch (status) { @@ -47,19 +58,32 @@ export default function OrdersPage() {
- {/* Filters */} -
- {['ALL', 'PENDING_PAYMENT', 'PAID', 'SHIPPED', 'CANCELLED'].map((f) => ( - - ))} + {/* Filters & Search */} +
+
+ {['ALL', 'PENDING_PAYMENT', 'PAID', 'SHIPPED', 'CANCELLED'].map((f) => ( + + ))} +
+ +
+ + setSearchTerm(e.target.value)} + className="w-full bg-white/5 border border-white/10 rounded-xl pl-10 pr-4 py-2 text-sm text-white focus:outline-none focus:border-cyan-glow/50 transition-colors" + /> +
{/* Orders Table */}