Vendor API endpoints use JWT authentication, not URL path parameters. The vendorCode should only be used for page URLs (navigation), not API calls. Fixed API paths in 10 vendor JS files: - analytics.js, customers.js, inventory.js, notifications.js - order-detail.js, orders.js, products.js, profile.js - settings.js, team.js Added architecture rule JS-014 to prevent this pattern from recurring. Added validation check _check_vendor_api_paths to validate_architecture.py. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
379 lines
12 KiB
JavaScript
379 lines
12 KiB
JavaScript
// static/vendor/js/order-detail.js
|
|
/**
|
|
* Vendor order detail page logic
|
|
* View order details, manage status, handle shipments, and invoice integration
|
|
*/
|
|
|
|
const orderDetailLog = window.LogConfig.loggers.orderDetail ||
|
|
window.LogConfig.createLogger('orderDetail', false);
|
|
|
|
orderDetailLog.info('Loading...');
|
|
|
|
function vendorOrderDetail() {
|
|
orderDetailLog.info('vendorOrderDetail() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'orders',
|
|
|
|
// Order ID from URL
|
|
orderId: window.orderDetailData?.orderId || null,
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
saving: false,
|
|
creatingInvoice: false,
|
|
downloadingPdf: false,
|
|
|
|
// Order data
|
|
order: null,
|
|
shipmentStatus: null,
|
|
invoice: null,
|
|
|
|
// Modal states
|
|
showStatusModal: false,
|
|
showShipAllModal: false,
|
|
newStatus: '',
|
|
trackingNumber: '',
|
|
trackingProvider: '',
|
|
|
|
// Order statuses
|
|
statuses: [
|
|
{ value: 'pending', label: 'Pending', color: 'yellow' },
|
|
{ value: 'processing', label: 'Processing', color: 'blue' },
|
|
{ value: 'partially_shipped', label: 'Partially Shipped', color: 'orange' },
|
|
{ value: 'shipped', label: 'Shipped', color: 'indigo' },
|
|
{ value: 'delivered', label: 'Delivered', color: 'green' },
|
|
{ value: 'cancelled', label: 'Cancelled', color: 'red' },
|
|
{ value: 'refunded', label: 'Refunded', color: 'gray' }
|
|
],
|
|
|
|
async init() {
|
|
orderDetailLog.info('Order detail init() called, orderId:', this.orderId);
|
|
|
|
// Guard against multiple initialization
|
|
if (window._orderDetailInitialized) {
|
|
orderDetailLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._orderDetailInitialized = true;
|
|
|
|
// IMPORTANT: Call parent init first to set vendorCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
if (!this.orderId) {
|
|
this.error = 'Order ID not provided';
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.loadOrderDetails();
|
|
} catch (error) {
|
|
orderDetailLog.error('Init failed:', error);
|
|
this.error = 'Failed to load order details';
|
|
}
|
|
|
|
orderDetailLog.info('Order detail initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Load order details from API
|
|
*/
|
|
async loadOrderDetails() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
// Load order details
|
|
const orderResponse = await apiClient.get(
|
|
`/vendor/orders/${this.orderId}`
|
|
);
|
|
this.order = orderResponse;
|
|
this.newStatus = this.order.status;
|
|
|
|
orderDetailLog.info('Loaded order:', this.order.order_number);
|
|
|
|
// Load shipment status
|
|
await this.loadShipmentStatus();
|
|
|
|
// Load invoice if exists
|
|
await this.loadInvoice();
|
|
|
|
} catch (error) {
|
|
orderDetailLog.error('Failed to load order details:', error);
|
|
this.error = error.message || 'Failed to load order details';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load shipment status for partial shipment tracking
|
|
*/
|
|
async loadShipmentStatus() {
|
|
try {
|
|
const response = await apiClient.get(
|
|
`/vendor/orders/${this.orderId}/shipment-status`
|
|
);
|
|
this.shipmentStatus = response;
|
|
orderDetailLog.info('Loaded shipment status:', response);
|
|
} catch (error) {
|
|
orderDetailLog.warn('Failed to load shipment status:', error);
|
|
// Not critical - continue without shipment status
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load invoice for this order
|
|
*/
|
|
async loadInvoice() {
|
|
try {
|
|
// Search for invoices linked to this order
|
|
const response = await apiClient.get(
|
|
`/vendor/invoices?order_id=${this.orderId}&limit=1`
|
|
);
|
|
if (response.invoices && response.invoices.length > 0) {
|
|
this.invoice = response.invoices[0];
|
|
orderDetailLog.info('Loaded invoice:', this.invoice.invoice_number);
|
|
}
|
|
} catch (error) {
|
|
orderDetailLog.warn('Failed to load invoice:', error);
|
|
// Not critical - continue without invoice
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get status color class
|
|
*/
|
|
getStatusColor(status) {
|
|
const statusObj = this.statuses.find(s => s.value === status);
|
|
return statusObj ? statusObj.color : 'gray';
|
|
},
|
|
|
|
/**
|
|
* Get status label
|
|
*/
|
|
getStatusLabel(status) {
|
|
const statusObj = this.statuses.find(s => s.value === status);
|
|
return statusObj ? statusObj.label : status;
|
|
},
|
|
|
|
/**
|
|
* Get item shipment status
|
|
*/
|
|
getItemShipmentStatus(itemId) {
|
|
if (!this.shipmentStatus?.items) return null;
|
|
return this.shipmentStatus.items.find(i => i.item_id === itemId);
|
|
},
|
|
|
|
/**
|
|
* Check if item can be shipped
|
|
*/
|
|
canShipItem(itemId) {
|
|
const status = this.getItemShipmentStatus(itemId);
|
|
if (!status) return true; // Assume can ship if no status
|
|
return !status.is_fully_shipped;
|
|
},
|
|
|
|
/**
|
|
* Format price for display
|
|
*/
|
|
formatPrice(cents) {
|
|
if (cents === null || cents === undefined) return '-';
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(cents / 100);
|
|
},
|
|
|
|
/**
|
|
* Format date/time for display
|
|
*/
|
|
formatDateTime(dateStr) {
|
|
if (!dateStr) return '-';
|
|
return new Date(dateStr).toLocaleDateString('de-DE', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Update order status
|
|
*/
|
|
async updateOrderStatus(status) {
|
|
this.saving = true;
|
|
try {
|
|
const payload = { status };
|
|
|
|
// Add tracking info if shipping
|
|
if (status === 'shipped' && this.trackingNumber) {
|
|
payload.tracking_number = this.trackingNumber;
|
|
payload.tracking_provider = this.trackingProvider;
|
|
}
|
|
|
|
await apiClient.put(
|
|
`/vendor/orders/${this.orderId}/status`,
|
|
payload
|
|
);
|
|
|
|
Utils.showToast(`Order status updated to ${this.getStatusLabel(status)}`, 'success');
|
|
await this.loadOrderDetails();
|
|
|
|
} catch (error) {
|
|
orderDetailLog.error('Failed to update status:', error);
|
|
Utils.showToast(error.message || 'Failed to update status', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Confirm status update from modal
|
|
*/
|
|
async confirmStatusUpdate() {
|
|
await this.updateOrderStatus(this.newStatus);
|
|
this.showStatusModal = false;
|
|
this.trackingNumber = '';
|
|
this.trackingProvider = '';
|
|
},
|
|
|
|
/**
|
|
* Ship a single item
|
|
*/
|
|
async shipItem(itemId) {
|
|
this.saving = true;
|
|
try {
|
|
await apiClient.post(
|
|
`/vendor/orders/${this.orderId}/items/${itemId}/ship`,
|
|
{}
|
|
);
|
|
|
|
Utils.showToast('Item shipped successfully', 'success');
|
|
await this.loadOrderDetails();
|
|
|
|
} catch (error) {
|
|
orderDetailLog.error('Failed to ship item:', error);
|
|
Utils.showToast(error.message || 'Failed to ship item', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ship all remaining items
|
|
*/
|
|
async shipAllItems() {
|
|
this.saving = true;
|
|
try {
|
|
// Ship each unshipped item
|
|
const unshippedItems = this.shipmentStatus?.items?.filter(i => !i.is_fully_shipped) || [];
|
|
|
|
for (const item of unshippedItems) {
|
|
await apiClient.post(
|
|
`/vendor/orders/${this.orderId}/items/${item.item_id}/ship`,
|
|
{}
|
|
);
|
|
}
|
|
|
|
// Update order status to shipped with tracking
|
|
const payload = { status: 'shipped' };
|
|
if (this.trackingNumber) {
|
|
payload.tracking_number = this.trackingNumber;
|
|
payload.tracking_provider = this.trackingProvider;
|
|
}
|
|
|
|
await apiClient.put(
|
|
`/vendor/orders/${this.orderId}/status`,
|
|
payload
|
|
);
|
|
|
|
Utils.showToast('All items shipped', 'success');
|
|
this.showShipAllModal = false;
|
|
this.trackingNumber = '';
|
|
this.trackingProvider = '';
|
|
await this.loadOrderDetails();
|
|
|
|
} catch (error) {
|
|
orderDetailLog.error('Failed to ship all items:', error);
|
|
Utils.showToast(error.message || 'Failed to ship items', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create invoice for this order
|
|
*/
|
|
async createInvoice() {
|
|
this.creatingInvoice = true;
|
|
try {
|
|
const response = await apiClient.post(
|
|
`/vendor/invoices`,
|
|
{ order_id: this.orderId }
|
|
);
|
|
|
|
this.invoice = response;
|
|
Utils.showToast(`Invoice ${response.invoice_number} created`, 'success');
|
|
|
|
} catch (error) {
|
|
orderDetailLog.error('Failed to create invoice:', error);
|
|
Utils.showToast(error.message || 'Failed to create invoice', 'error');
|
|
} finally {
|
|
this.creatingInvoice = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Download invoice PDF
|
|
*/
|
|
async downloadInvoicePdf() {
|
|
if (!this.invoice) return;
|
|
|
|
this.downloadingPdf = true;
|
|
try {
|
|
const response = await fetch(
|
|
`/api/v1/vendor/${this.vendorCode}/invoices/${this.invoice.id}/pdf`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${window.Auth?.getToken()}`
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to download PDF');
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${this.invoice.invoice_number}.pdf`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
a.remove();
|
|
|
|
Utils.showToast('Invoice downloaded', 'success');
|
|
|
|
} catch (error) {
|
|
orderDetailLog.error('Failed to download invoice PDF:', error);
|
|
Utils.showToast(error.message || 'Failed to download PDF', 'error');
|
|
} finally {
|
|
this.downloadingPdf = false;
|
|
}
|
|
}
|
|
};
|
|
}
|