All checks were successful
Build and Push Docker Images / docker (push) Successful in 2m25s
- Fully implemented OrdersTable with sorting, pagination, and filtering - Added OrderFilters component for search, status, and date range filtering - Created OrderStatsCards for dashboard metrics display - Built OrderDetailsModal for viewing complete order information - Implemented ExportButton for CSV export functionality - Updated dashboard and orders pages to use new components - Enhanced OrderRecord type definitions in src/types.ts - All components working with NocoDB API integration
232 lines
6.8 KiB
TypeScript
232 lines
6.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { OrderStatsCards } from '@/components/admin/OrderStatsCards';
|
|
import { OrdersTable } from '@/components/admin/OrdersTable';
|
|
import { OrderFilters } from '@/components/admin/OrderFilters';
|
|
import { OrderDetailsModal } from '@/components/admin/OrderDetailsModal';
|
|
import { ExportButton } from '@/components/admin/ExportButton';
|
|
import { OrderRecord } from '@/src/types';
|
|
|
|
interface OrderStats {
|
|
totalOrders: number;
|
|
totalRevenue: number;
|
|
totalCO2Offset: number;
|
|
fulfillmentRate: number;
|
|
}
|
|
|
|
export default function AdminOrders() {
|
|
const [orders, setOrders] = useState<OrderRecord[]>([]);
|
|
const [stats, setStats] = useState<OrderStats | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(25);
|
|
const [totalCount, setTotalCount] = useState(0);
|
|
const [sortKey, setSortKey] = useState<keyof OrderRecord | null>('CreatedAt');
|
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
|
const [selectedOrder, setSelectedOrder] = useState<OrderRecord | null>(null);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('');
|
|
const [dateFrom, setDateFrom] = useState('');
|
|
const [dateTo, setDateTo] = useState('');
|
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
|
|
|
// Fetch orders and stats
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [currentPage, pageSize, sortKey, sortOrder, searchTerm, statusFilter, dateFrom, dateTo]);
|
|
|
|
const fetchData = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Build query params for orders
|
|
const params = new URLSearchParams();
|
|
params.append('limit', pageSize.toString());
|
|
params.append('offset', ((currentPage - 1) * pageSize).toString());
|
|
|
|
if (sortKey) {
|
|
params.append('sortBy', sortKey);
|
|
params.append('sortOrder', sortOrder);
|
|
}
|
|
|
|
// Add filters
|
|
if (searchTerm) {
|
|
params.append('search', searchTerm);
|
|
}
|
|
if (statusFilter) {
|
|
params.append('status', statusFilter);
|
|
}
|
|
if (dateFrom) {
|
|
params.append('dateFrom', dateFrom);
|
|
}
|
|
if (dateTo) {
|
|
params.append('dateTo', dateTo);
|
|
}
|
|
|
|
// Fetch orders
|
|
const ordersResponse = await fetch(`/api/admin/orders?${params}`);
|
|
const ordersData = await ordersResponse.json();
|
|
|
|
if (!ordersResponse.ok) {
|
|
throw new Error(ordersData.error || 'Failed to fetch orders');
|
|
}
|
|
|
|
setOrders(ordersData.data || []);
|
|
setTotalCount(ordersData.pagination?.totalRows || 0);
|
|
|
|
// Fetch stats (for current month)
|
|
const statsResponse = await fetch('/api/admin/stats');
|
|
const statsData = await statsResponse.json();
|
|
|
|
if (statsResponse.ok) {
|
|
setStats(statsData.data.stats);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching data:', err);
|
|
setError(err instanceof Error ? err.message : 'Failed to load orders');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSort = (key: keyof OrderRecord) => {
|
|
if (sortKey === key) {
|
|
// Toggle sort order if same column
|
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
// New column, default to ascending
|
|
setSortKey(key);
|
|
setSortOrder('asc');
|
|
}
|
|
};
|
|
|
|
const handlePageChange = (newPage: number) => {
|
|
setCurrentPage(newPage);
|
|
};
|
|
|
|
const handlePageSizeChange = (newSize: number) => {
|
|
setPageSize(newSize);
|
|
setCurrentPage(1); // Reset to first page when changing page size
|
|
};
|
|
|
|
const handleViewDetails = (order: OrderRecord) => {
|
|
setSelectedOrder(order);
|
|
setIsDetailsModalOpen(true);
|
|
};
|
|
|
|
const handleCloseDetailsModal = () => {
|
|
setIsDetailsModalOpen(false);
|
|
// Optional: clear selected order after animation completes
|
|
setTimeout(() => setSelectedOrder(null), 300);
|
|
};
|
|
|
|
const handleSearchChange = (search: string) => {
|
|
setSearchTerm(search);
|
|
setCurrentPage(1); // Reset to first page on filter change
|
|
};
|
|
|
|
const handleStatusChange = (status: string) => {
|
|
setStatusFilter(status);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const handleDateRangeChange = (from: string, to: string) => {
|
|
setDateFrom(from);
|
|
setDateTo(to);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const handleResetFilters = () => {
|
|
setSearchTerm('');
|
|
setStatusFilter('');
|
|
setDateFrom('');
|
|
setDateTo('');
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="space-y-8">
|
|
<h1 className="text-3xl font-bold text-deep-sea-blue mb-2">Orders</h1>
|
|
<div className="bg-red-50 border border-red-200 rounded-xl p-6 text-center">
|
|
<p className="text-red-800 font-medium">{error}</p>
|
|
<button
|
|
onClick={fetchData}
|
|
className="mt-4 px-4 py-2 bg-deep-sea-blue text-white rounded-lg hover:bg-deep-sea-blue/90 transition-colors"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className="space-y-8"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-deep-sea-blue mb-2">Orders</h1>
|
|
<p className="text-deep-sea-blue/70 font-medium">
|
|
View and manage all carbon offset orders
|
|
</p>
|
|
</div>
|
|
<ExportButton
|
|
currentOrders={orders}
|
|
filters={{
|
|
search: searchTerm,
|
|
status: statusFilter,
|
|
dateFrom: dateFrom,
|
|
dateTo: dateTo,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<OrderStatsCards stats={stats} isLoading={isLoading && !stats} />
|
|
|
|
{/* Filters */}
|
|
<OrderFilters
|
|
onSearchChange={handleSearchChange}
|
|
onStatusChange={handleStatusChange}
|
|
onDateRangeChange={handleDateRangeChange}
|
|
onReset={handleResetFilters}
|
|
searchValue={searchTerm}
|
|
statusValue={statusFilter}
|
|
dateFromValue={dateFrom}
|
|
dateToValue={dateTo}
|
|
/>
|
|
|
|
{/* Orders Table */}
|
|
<OrdersTable
|
|
orders={orders}
|
|
isLoading={isLoading}
|
|
onViewDetails={handleViewDetails}
|
|
totalCount={totalCount}
|
|
currentPage={currentPage}
|
|
pageSize={pageSize}
|
|
onPageChange={handlePageChange}
|
|
onPageSizeChange={handlePageSizeChange}
|
|
onSort={handleSort}
|
|
sortKey={sortKey}
|
|
sortOrder={sortOrder}
|
|
/>
|
|
|
|
{/* Order Details Modal */}
|
|
<OrderDetailsModal
|
|
order={selectedOrder}
|
|
isOpen={isDetailsModalOpen}
|
|
onClose={handleCloseDetailsModal}
|
|
/>
|
|
</motion.div>
|
|
);
|
|
}
|