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
298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { ChevronUp, ChevronDown, Eye, Loader2 } from 'lucide-react';
|
|
import { OrderRecord } from '@/src/types';
|
|
|
|
interface OrdersTableProps {
|
|
orders: OrderRecord[];
|
|
isLoading?: boolean;
|
|
onViewDetails: (order: OrderRecord) => void;
|
|
totalCount: number;
|
|
currentPage: number;
|
|
pageSize: number;
|
|
onPageChange: (page: number) => void;
|
|
onPageSizeChange: (size: number) => void;
|
|
onSort: (key: keyof OrderRecord) => void;
|
|
sortKey: keyof OrderRecord | null;
|
|
sortOrder: 'asc' | 'desc';
|
|
}
|
|
|
|
export function OrdersTable({
|
|
orders,
|
|
isLoading = false,
|
|
onViewDetails,
|
|
totalCount,
|
|
currentPage,
|
|
pageSize,
|
|
onPageChange,
|
|
onPageSizeChange,
|
|
onSort,
|
|
sortKey,
|
|
sortOrder,
|
|
}: OrdersTableProps) {
|
|
const totalPages = Math.ceil(totalCount / pageSize);
|
|
|
|
const getStatusBadgeClass = (status: string) => {
|
|
switch (status) {
|
|
case 'pending':
|
|
return 'bg-gradient-to-r from-muted-gold/20 to-orange-400/20 text-muted-gold border border-muted-gold/30';
|
|
case 'paid':
|
|
return 'bg-gradient-to-r from-maritime-teal/20 to-teal-400/20 text-maritime-teal border border-maritime-teal/30';
|
|
case 'fulfilled':
|
|
return 'bg-gradient-to-r from-sea-green/20 to-green-400/20 text-sea-green border border-sea-green/30';
|
|
case 'cancelled':
|
|
return 'bg-gradient-to-r from-red-500/20 to-red-400/20 text-red-600 border border-red-500/30';
|
|
default:
|
|
return 'bg-gray-100 text-gray-700 border border-gray-300';
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const formatCurrency = (amount: string, currency: string = 'USD') => {
|
|
const numAmount = parseFloat(amount) / 100;
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency || 'USD',
|
|
}).format(numAmount);
|
|
};
|
|
|
|
const getSortIcon = (column: keyof OrderRecord) => {
|
|
if (sortKey !== column) {
|
|
return <ChevronUp className="w-4 h-4 text-gray-400" />;
|
|
}
|
|
return sortOrder === 'asc' ? (
|
|
<ChevronUp className="w-4 h-4 text-deep-sea-blue" />
|
|
) : (
|
|
<ChevronDown className="w-4 h-4 text-deep-sea-blue" />
|
|
);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="bg-white border border-light-gray-border rounded-xl overflow-hidden">
|
|
<div className="flex items-center justify-center h-64">
|
|
<Loader2 className="w-8 h-8 text-deep-sea-blue animate-spin" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (orders.length === 0) {
|
|
return (
|
|
<div className="bg-white border border-light-gray-border rounded-xl overflow-hidden">
|
|
<div className="flex flex-col items-center justify-center h-64 text-center px-4">
|
|
<div className="text-6xl mb-4">📦</div>
|
|
<h3 className="text-xl font-bold text-deep-sea-blue mb-2">No Orders Found</h3>
|
|
<p className="text-deep-sea-blue/60">There are no orders matching your criteria.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white border border-light-gray-border rounded-xl overflow-hidden">
|
|
{/* Table */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-light-gray-border">
|
|
<tr>
|
|
<th
|
|
onClick={() => onSort('orderId')}
|
|
className="px-6 py-4 text-left text-xs font-semibold text-deep-sea-blue uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<span>Order ID</span>
|
|
{getSortIcon('orderId')}
|
|
</div>
|
|
</th>
|
|
<th
|
|
onClick={() => onSort('customerName')}
|
|
className="px-6 py-4 text-left text-xs font-semibold text-deep-sea-blue uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<span>Customer</span>
|
|
{getSortIcon('customerName')}
|
|
</div>
|
|
</th>
|
|
<th
|
|
onClick={() => onSort('totalAmount')}
|
|
className="px-6 py-4 text-left text-xs font-semibold text-deep-sea-blue uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<span>Amount</span>
|
|
{getSortIcon('totalAmount')}
|
|
</div>
|
|
</th>
|
|
<th
|
|
onClick={() => onSort('co2Tons')}
|
|
className="px-6 py-4 text-left text-xs font-semibold text-deep-sea-blue uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<span>CO₂ Offset</span>
|
|
{getSortIcon('co2Tons')}
|
|
</div>
|
|
</th>
|
|
<th
|
|
onClick={() => onSort('status')}
|
|
className="px-6 py-4 text-left text-xs font-semibold text-deep-sea-blue uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<span>Status</span>
|
|
{getSortIcon('status')}
|
|
</div>
|
|
</th>
|
|
<th
|
|
onClick={() => onSort('CreatedAt')}
|
|
className="px-6 py-4 text-left text-xs font-semibold text-deep-sea-blue uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<span>Created</span>
|
|
{getSortIcon('CreatedAt')}
|
|
</div>
|
|
</th>
|
|
<th className="px-6 py-4 text-right text-xs font-semibold text-deep-sea-blue uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-light-gray-border">
|
|
{orders.map((order, index) => (
|
|
<motion.tr
|
|
key={order.Id}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.05 }}
|
|
className="hover:bg-gray-50 transition-colors"
|
|
>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-mono text-deep-sea-blue">
|
|
{order.orderId ? `${order.orderId.substring(0, 8)}...` : 'N/A'}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-sm font-medium text-deep-sea-blue">{order.customerName}</div>
|
|
<div className="text-xs text-deep-sea-blue/60">{order.customerEmail}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-semibold text-deep-sea-blue">
|
|
{formatCurrency(order.totalAmount, order.currency)}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-sea-green/20 text-sea-green border border-sea-green/30">
|
|
{parseFloat(order.co2Tons).toFixed(2)} tons
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${getStatusBadgeClass(
|
|
order.status
|
|
)}`}
|
|
>
|
|
{order.status ? order.status.charAt(0).toUpperCase() + order.status.slice(1) : 'Unknown'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-deep-sea-blue/70">
|
|
{formatDate(order.CreatedAt)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right">
|
|
<button
|
|
onClick={() => onViewDetails(order)}
|
|
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-deep-sea-blue bg-deep-sea-blue/10 hover:bg-deep-sea-blue hover:text-white rounded-lg transition-colors"
|
|
>
|
|
<Eye className="w-4 h-4 mr-1" />
|
|
View
|
|
</button>
|
|
</td>
|
|
</motion.tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
<div className="px-6 py-4 border-t border-light-gray-border bg-gray-50">
|
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm text-deep-sea-blue/70">Rows per page:</span>
|
|
<select
|
|
value={pageSize}
|
|
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
|
className="px-3 py-1.5 text-sm border border-light-gray-border rounded-lg bg-white text-deep-sea-blue focus:outline-none focus:ring-2 focus:ring-deep-sea-blue/20"
|
|
>
|
|
<option value={10}>10</option>
|
|
<option value={25}>25</option>
|
|
<option value={50}>50</option>
|
|
<option value={100}>100</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="text-sm text-deep-sea-blue/70">
|
|
Showing {(currentPage - 1) * pageSize + 1} to{' '}
|
|
{Math.min(currentPage * pageSize, totalCount)} of {totalCount} orders
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={() => onPageChange(currentPage - 1)}
|
|
disabled={currentPage === 1}
|
|
className="px-4 py-2 text-sm font-medium text-deep-sea-blue bg-white border border-light-gray-border rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
Previous
|
|
</button>
|
|
|
|
<div className="hidden sm:flex items-center space-x-1">
|
|
{Array.from({ length: Math.min(totalPages, 5) }).map((_, i) => {
|
|
let pageNumber;
|
|
if (totalPages <= 5) {
|
|
pageNumber = i + 1;
|
|
} else if (currentPage <= 3) {
|
|
pageNumber = i + 1;
|
|
} else if (currentPage >= totalPages - 2) {
|
|
pageNumber = totalPages - 4 + i;
|
|
} else {
|
|
pageNumber = currentPage - 2 + i;
|
|
}
|
|
|
|
if (pageNumber < 1 || pageNumber > totalPages) return null;
|
|
|
|
return (
|
|
<button
|
|
key={pageNumber}
|
|
onClick={() => onPageChange(pageNumber)}
|
|
className={`px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
|
currentPage === pageNumber
|
|
? 'bg-deep-sea-blue text-white'
|
|
: 'text-deep-sea-blue bg-white border border-light-gray-border hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{pageNumber}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => onPageChange(currentPage + 1)}
|
|
disabled={currentPage === totalPages}
|
|
className="px-4 py-2 text-sm font-medium text-deep-sea-blue bg-white border border-light-gray-border rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|