import React from 'react'; import superagent from 'superagent'; import { Header, Segment, Form, Button, Icon, Table, Label, Divider, Grid, Message, Input, Dropdown, Modal, TextArea, Statistic, Loader } from 'semantic-ui-react'; import ExperimentalWrapper from './ExperimentalWrapper.jsx'; // Status options for charge flags const STATUS_OPTIONS = [ { key: 'new', text: 'New', value: 'new' }, { key: 'reviewed', text: 'Reviewed', value: 'reviewed' }, { key: 'disputed', text: 'Disputed', value: 'disputed' }, { key: 'resolved', text: 'Resolved', value: 'resolved' }, ]; export default class DHLChargesPage extends React.Component { constructor(props) { super(props); this.state = { // Data invoices: [], flaggedCharges: [], shipmentLookupResults: {}, // UI State loading: false, loadingCharges: false, selectedInvoice: null, statusFilter: 'all', searchTrackingNumber: '', // Modal state editModalOpen: false, editingCharge: null, editStatus: '', editNotes: '', // CSV Upload state uploading: false, uploadResult: null, // Reconcile state reconcilingInvoiceId: null, // Clear all state clearing: false, clearConfirmOpen: false, clearResult: null, // Lookup state lookupModalOpen: false, lookupTrackingNumber: '', lookupResult: null, lookupLoading: false, // Fee breakdown expanded rows expandedChargeIds: {}, // Raw data state rawDataExpanded: {}, // Summary stats from API summary: null, // Selected invoice detail (with hydrated charges + shipment data) selectedInvoiceDetail: null, // Tab state for invoice detail view activeTab: 'charges' }; } componentDidMount() { this.loadInvoices(); this.loadFlaggedCharges(); this.loadSummary(); } async loadInvoices() { this.setState({ loading: true }); try { const res = await superagent.get('/api/admin/dhl/invoices').accept('json'); this.setState({ invoices: res.body.items || [] }); } catch (error) { console.error('Failed to load invoices:', error); } finally { this.setState({ loading: false }); } } async loadSummary() { try { const res = await superagent.get('/api/admin/dhl/invoices/summary').accept('json'); this.setState({ summary: res.body }); } catch (error) { console.error('Failed to load summary:', error); } } async loadFlaggedCharges(invoiceId) { this.setState({ loadingCharges: true }); try { const query = {}; if (invoiceId) { query.invoiceId = invoiceId; } const res = await superagent.get('/api/admin/dhl/charges/flagged') .query(query) .accept('json'); this.setState({ flaggedCharges: res.body.items || [] }); } catch (error) { console.error('Failed to load flagged charges:', error); } finally { this.setState({ loadingCharges: false }); } } async uploadCSV(file) { this.setState({ uploading: true, uploadResult: null }); try { const res = await superagent.post('/api/admin/dhl/invoices/upload-csv') .attach('file', file) .accept('json'); this.setState({ uploadResult: res.body }); await this.loadInvoices(); await this.loadSummary(); } catch (error) { console.error('CSV upload failed:', error); this.setState({ uploadResult: { error: error.response?.body?.error || error.message } }); } finally { this.setState({ uploading: false }); } } async clearAllData() { this.setState({ clearing: true, clearResult: null }); try { const res = await superagent.delete('/api/admin/dhl/all').accept('json'); this.setState({ clearResult: res.body, clearConfirmOpen: false }); await this.loadInvoices(); await this.loadFlaggedCharges(); await this.loadSummary(); } catch (error) { console.error('Clear all failed:', error); this.setState({ clearResult: { error: error.response?.body?.error || error.message }, clearConfirmOpen: false }); } finally { this.setState({ clearing: false }); } } async reconcileInvoice(invoiceId) { this.setState({ reconcilingInvoiceId: invoiceId }); try { await superagent.post('/api/admin/dhl/reconcile') .send({ invoiceId }) .accept('json'); // Reload both invoices and charges await this.loadInvoices(); await this.loadFlaggedCharges(this.state.selectedInvoice); } catch (error) { console.error('Reconciliation failed:', error); } finally { this.setState({ reconcilingInvoiceId: null }); } } async lookupShipment(trackingNumber) { this.setState({ lookupLoading: true }); try { const res = await superagent.get('/api/admin/shipper/shipments') .query({ trackingNumber, limit: 1 }); const shipments = res.body; if (shipments && shipments.items && shipments.items.length > 0) { const shipment = shipments.items[0]; this.setState({ lookupResult: shipment, shipmentLookupResults: { ...this.state.shipmentLookupResults, [trackingNumber]: shipment } }); return shipment; } else if (shipments && shipments.length > 0) { const shipment = shipments[0]; this.setState({ lookupResult: shipment, shipmentLookupResults: { ...this.state.shipmentLookupResults, [trackingNumber]: shipment } }); return shipment; } else { this.setState({ lookupResult: { notFound: true } }); return null; } } catch (error) { console.error('Failed to lookup shipment:', error); this.setState({ lookupResult: { error: error.message } }); return null; } finally { this.setState({ lookupLoading: false }); } } openEditModal(charge) { this.setState({ editModalOpen: true, editingCharge: charge, editStatus: charge.status, editNotes: charge.notes || '' }); } async saveChargeEdits() { const { editingCharge, editStatus, editNotes } = this.state; try { await superagent.put(`/api/admin/dhl/charges/${editingCharge.uniqueId || editingCharge.id}`) .send({ status: editStatus, notes: editNotes }) .accept('json'); this.setState({ editModalOpen: false, editingCharge: null }); // Reload charges await this.loadFlaggedCharges(this.state.selectedInvoice); } catch (error) { console.error('Failed to save charge edits:', error); } } openLookupModal(trackingNumber) { this.setState({ lookupModalOpen: true, lookupTrackingNumber: trackingNumber, lookupResult: this.state.shipmentLookupResults[trackingNumber] || null }); if (!this.state.shipmentLookupResults[trackingNumber]) { this.lookupShipment(trackingNumber); } } async selectInvoice(invoiceId) { this.setState({ selectedInvoice: invoiceId, activeTab: 'charges', selectedInvoiceDetail: null }); this.loadFlaggedCharges(invoiceId); if (invoiceId) { // Find the invoice's numeric id for the API call const invoice = this.state.invoices.find(inv => (inv.uniqueId || inv.id) === invoiceId); if (invoice) { this.setState({ loadingCharges: true }); try { const res = await superagent.get(`/api/admin/dhl/invoices/${invoice.id}`).accept('json'); this.setState({ selectedInvoiceDetail: res.body }); } catch (error) { console.error('Failed to load invoice detail:', error); } finally { this.setState({ loadingCharges: false }); } } } } toggleRawData(invoiceId) { this.setState(prev => ({ rawDataExpanded: { ...prev.rawDataExpanded, [invoiceId]: !prev.rawDataExpanded[invoiceId] } })); } formatDate(timestamp) { if (!timestamp) return '-'; return new Date(timestamp).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } formatCurrency(amount, currency = 'USD') { if (amount === null || amount === undefined) return '-'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount); } toggleFeeBreakdown(chargeId) { this.setState(prev => ({ expandedChargeIds: { ...prev.expandedChargeIds, [chargeId]: !prev.expandedChargeIds[chargeId] } })); } renderFeeBreakdown(fb, currency) { if (!fb || typeof fb !== 'object') return null; const items = []; if (fb.weightCharge != null) items.push({ label: 'Weight Charge', amount: fb.weightCharge }); if (fb.fuelSurcharge != null) items.push({ label: 'Fuel Surcharge', amount: fb.fuelSurcharge }); if (fb.taxAdjustment != null) items.push({ label: 'Tax Adjustment', amount: fb.taxAdjustment }); if (fb.invoiceFee != null) items.push({ label: 'Invoice Fee', amount: fb.invoiceFee }); if (fb.otherCharges && fb.otherCharges.length > 0) { fb.otherCharges.forEach(oc => items.push({ label: `Other: ${oc.code}`, amount: oc.amount })); } if (fb.extraCharges && fb.extraCharges.length > 0) { fb.extraCharges.forEach(xc => items.push({ label: xc.name || xc.code, amount: xc.amount })); } if (fb.discounts && fb.discounts.length > 0) { fb.discounts.forEach(d => items.push({ label: `Discount: ${d.code}`, amount: d.amount })); } if (fb.totalExtraCharges != null) items.push({ label: 'Total Extra Charges', amount: fb.totalExtraCharges, bold: true }); if (items.length === 0) return No breakdown; return (
{uploadResult.error}
) : (
{uploadResult.chargesCreated} charge(s) added{uploadResult.chargesUpdated > 0 ? `, ${uploadResult.chargesUpdated} replaced` : ''}.
{(uploadResult.invoicesCreated > 0 || uploadResult.invoicesUpdated > 0) && (
{uploadResult.invoicesCreated > 0 && `${uploadResult.invoicesCreated} invoice(s) created. `}
{uploadResult.invoicesUpdated > 0 && `${uploadResult.invoicesUpdated} invoice(s) updated.`}
)}
{uploadResult.errors && uploadResult.errors.length > 0 && (
<>
{uploadResult.errors.length} warning(s).>
)}
This will permanently delete all DHL invoices, charges, and flags. This action cannot be undone.
Are you sure you want to continue?
{clearResult.error}
) : (Deleted {clearResult.invoicesDeleted} invoice(s), {clearResult.chargesDeleted} charge(s), {clearResult.flagsDeleted} flag(s).
)}
{JSON.stringify(invoice, null, 4)}
No shipment found with tracking number: {lookupTrackingNumber}
{lookupResult.error}