Matt a6484de35e
Some checks failed
Build and Push Docker Images / docker (push) Failing after 1m57s
Integrate NocoDB backend for admin portal with real data
Phase 2 Backend Integration Complete:

Backend Infrastructure:
- Created NocoDB client abstraction layer (src/api/nocodbClient.ts)
- Clean TypeScript API hiding NocoDB query syntax complexity
- Helper methods for orders, stats, search, timeline, and filtering
- Automatic date range handling and pagination support

API Routes:
- POST /api/admin/stats - Dashboard statistics with time range filtering
- GET /api/admin/orders - List orders with search, filter, sort, pagination
- GET /api/admin/orders/[id] - Single order details
- PATCH /api/admin/orders/[id] - Update order fields
- DELETE /api/admin/orders/[id] - Cancel order (soft delete)
- GET /api/admin/orders/export - CSV/Excel export with filters

Dashboard Updates:
- Real-time data fetching from NocoDB
- Time range selector (7d, 30d, 90d, all time)
- Recharts line chart for orders timeline
- Recharts pie chart for status distribution
- Loading states and error handling
- Dynamic stat cards with real numbers

Dependencies Added:
- papaparse - CSV export
- xlsx - Excel export with styling
- @types/papaparse - TypeScript support

Data Types:
- OrderRecord interface for NocoDB data structure
- DashboardStats, TimelineData, OrderFilters interfaces
- Full type safety across API and UI

Environment Configuration:
- NOCODB_BASE_URL, NOCODB_BASE_ID configured
- NOCODB_API_KEY, NOCODB_ORDERS_TABLE_ID configured
- All credentials stored securely in .env.local

Ready for testing with sample data in NocoDB!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:40:25 +01:00

132 lines
4.6 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { nocodbClient } from '@/api/nocodbClient';
import type { OrderFilters } from '@/api/nocodbClient';
import * as XLSX from 'xlsx';
import Papa from 'papaparse';
/**
* GET /api/admin/orders/export
* Export orders to CSV or Excel
* Query params:
* - format: 'csv' | 'xlsx' (default: csv)
* - status: Filter by status
* - dateFrom, dateTo: Date range filter
*/
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const format = searchParams.get('format') || 'csv';
// Build filters (same as orders list)
const filters: OrderFilters = {};
if (searchParams.get('status')) filters.status = searchParams.get('status')!;
if (searchParams.get('dateFrom')) filters.dateFrom = searchParams.get('dateFrom')!;
if (searchParams.get('dateTo')) filters.dateTo = searchParams.get('dateTo')!;
// Get all matching orders (no pagination for export)
const response = await nocodbClient.getOrders(filters, { limit: 10000 });
const orders = response.list;
// Transform data for export
const exportData = orders.map((order) => ({
'Order ID': order.orderId,
'Status': order.status,
'Created Date': order.CreatedAt ? new Date(order.CreatedAt).toLocaleDateString() : '',
'Vessel Name': order.vesselName,
'IMO Number': order.imoNumber || '',
'Distance (NM)': order.distance || '',
'Avg Speed (kn)': order.avgSpeed || '',
'CO2 Tons': order.co2Tons || '',
'Total Amount': order.totalAmount || '',
'Currency': order.currency || '',
'Customer Name': order.customerName || '',
'Customer Email': order.customerEmail || '',
'Customer Company': order.customerCompany || '',
'Departure Port': order.departurePort || '',
'Arrival Port': order.arrivalPort || '',
'Payment Method': order.paymentMethod || '',
'Payment Reference': order.paymentReference || '',
'Wren Order ID': order.wrenOrderId || '',
'Certificate URL': order.certificateUrl || '',
'Fulfilled At': order.fulfilledAt ? new Date(order.fulfilledAt).toLocaleDateString() : '',
'Notes': order.notes || '',
}));
if (format === 'xlsx') {
// Generate Excel file
const worksheet = XLSX.utils.json_to_sheet(exportData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Orders');
// Style headers (make them bold)
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
for (let col = range.s.c; col <= range.e.c; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: 0, c: col });
if (!worksheet[cellAddress]) continue;
worksheet[cellAddress].s = {
font: { bold: true },
fill: { fgColor: { rgb: 'D3D3D3' } },
};
}
// Set column widths
worksheet['!cols'] = [
{ wch: 20 }, // Order ID
{ wch: 12 }, // Status
{ wch: 15 }, // Created Date
{ wch: 25 }, // Vessel Name
{ wch: 12 }, // IMO Number
{ wch: 12 }, // Distance
{ wch: 12 }, // Avg Speed
{ wch: 10 }, // CO2 Tons
{ wch: 12 }, // Total Amount
{ wch: 10 }, // Currency
{ wch: 20 }, // Customer Name
{ wch: 25 }, // Customer Email
{ wch: 25 }, // Customer Company
{ wch: 20 }, // Departure Port
{ wch: 20 }, // Arrival Port
{ wch: 15 }, // Payment Method
{ wch: 20 }, // Payment Reference
{ wch: 20 }, // Wren Order ID
{ wch: 30 }, // Certificate URL
{ wch: 15 }, // Fulfilled At
{ wch: 40 }, // Notes
];
// Generate buffer
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
// Return Excel file
return new NextResponse(buffer, {
status: 200,
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': `attachment; filename="puffin-orders-${Date.now()}.xlsx"`,
},
});
} else {
// Generate CSV
const csv = Papa.unparse(exportData);
// Return CSV file
return new NextResponse(csv, {
status: 200,
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="puffin-orders-${Date.now()}.csv"`,
},
});
}
} catch (error) {
console.error('Error exporting orders:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to export orders',
},
{ status: 500 }
);
}
}