Matt 4b408986e5
All checks were successful
Build and Push Docker Images / docker (push) Successful in 2m25s
Add complete admin portal implementation with orders management
- 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
2025-11-03 22:24:17 +01:00

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>
);
}