feat: add Letzshop frontend for admin and vendor portals
Add complete frontend UI for Letzshop marketplace integration:
Admin portal (/admin/letzshop):
- Vendor overview with Letzshop status cards
- Vendor table with configuration state and sync info
- Configuration modal for API credentials
- Connection testing and manual sync triggers
- Orders modal for viewing vendor orders
Vendor portal (/vendor/{code}/letzshop):
- Orders tab with import, confirm, reject actions
- Settings tab for API credentials management
- Tracking modal for shipment updates
- Order details modal with line items
- Stats display for order status counts
Also includes:
- Routes for both admin and vendor Letzshop pages
- Sidebar navigation updates for both portals
- Alpine.js data functions for reactive UI state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
276
static/admin/js/letzshop.js
Normal file
276
static/admin/js/letzshop.js
Normal file
@@ -0,0 +1,276 @@
|
||||
// static/admin/js/letzshop.js
|
||||
/**
|
||||
* Admin Letzshop management page logic
|
||||
*/
|
||||
|
||||
console.log('[ADMIN LETZSHOP] Loading...');
|
||||
|
||||
function adminLetzshop() {
|
||||
console.log('[ADMIN LETZSHOP] adminLetzshop() called');
|
||||
|
||||
return {
|
||||
// Inherit base layout state
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'letzshop',
|
||||
|
||||
// Loading states
|
||||
loading: false,
|
||||
savingConfig: false,
|
||||
loadingOrders: false,
|
||||
|
||||
// Messages
|
||||
error: '',
|
||||
successMessage: '',
|
||||
|
||||
// Vendors data
|
||||
vendors: [],
|
||||
totalVendors: 0,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
|
||||
// Filters
|
||||
filters: {
|
||||
configuredOnly: false
|
||||
},
|
||||
|
||||
// Stats
|
||||
stats: {
|
||||
total: 0,
|
||||
configured: 0,
|
||||
autoSync: 0,
|
||||
pendingOrders: 0
|
||||
},
|
||||
|
||||
// Configuration modal
|
||||
showConfigModal: false,
|
||||
selectedVendor: null,
|
||||
vendorCredentials: null,
|
||||
configForm: {
|
||||
api_key: '',
|
||||
auto_sync_enabled: false,
|
||||
sync_interval_minutes: 15
|
||||
},
|
||||
showApiKey: false,
|
||||
|
||||
// Orders modal
|
||||
showOrdersModal: false,
|
||||
vendorOrders: [],
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._adminLetzshopInitialized) {
|
||||
return;
|
||||
}
|
||||
window._adminLetzshopInitialized = true;
|
||||
|
||||
console.log('[ADMIN LETZSHOP] Initializing...');
|
||||
await this.loadVendors();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load vendors with Letzshop status
|
||||
*/
|
||||
async loadVendors() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
skip: ((this.page - 1) * this.limit).toString(),
|
||||
limit: this.limit.toString(),
|
||||
configured_only: this.filters.configuredOnly.toString()
|
||||
});
|
||||
|
||||
const response = await apiClient.get(`/admin/letzshop/vendors?${params}`);
|
||||
this.vendors = response.vendors || [];
|
||||
this.totalVendors = response.total || 0;
|
||||
|
||||
// Calculate stats
|
||||
this.stats.total = this.totalVendors;
|
||||
this.stats.configured = this.vendors.filter(v => v.is_configured).length;
|
||||
this.stats.autoSync = this.vendors.filter(v => v.auto_sync_enabled).length;
|
||||
this.stats.pendingOrders = this.vendors.reduce((sum, v) => sum + (v.pending_orders || 0), 0);
|
||||
|
||||
console.log('[ADMIN LETZSHOP] Loaded vendors:', this.vendors.length);
|
||||
} catch (error) {
|
||||
console.error('[ADMIN LETZSHOP] Failed to load vendors:', error);
|
||||
this.error = error.message || 'Failed to load vendors';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh all data
|
||||
*/
|
||||
async refreshData() {
|
||||
await this.loadVendors();
|
||||
this.successMessage = 'Data refreshed';
|
||||
setTimeout(() => this.successMessage = '', 3000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Open configuration modal for a vendor
|
||||
*/
|
||||
async openConfigModal(vendor) {
|
||||
this.selectedVendor = vendor;
|
||||
this.vendorCredentials = null;
|
||||
this.configForm = {
|
||||
api_key: '',
|
||||
auto_sync_enabled: vendor.auto_sync_enabled || false,
|
||||
sync_interval_minutes: 15
|
||||
};
|
||||
this.showApiKey = false;
|
||||
this.showConfigModal = true;
|
||||
|
||||
// Load existing credentials if configured
|
||||
if (vendor.is_configured) {
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/letzshop/vendors/${vendor.vendor_id}/credentials`);
|
||||
this.vendorCredentials = response;
|
||||
this.configForm.auto_sync_enabled = response.auto_sync_enabled;
|
||||
this.configForm.sync_interval_minutes = response.sync_interval_minutes || 15;
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
console.error('[ADMIN LETZSHOP] Failed to load credentials:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save vendor configuration
|
||||
*/
|
||||
async saveVendorConfig() {
|
||||
if (!this.configForm.api_key && !this.vendorCredentials) {
|
||||
this.error = 'Please enter an API key';
|
||||
return;
|
||||
}
|
||||
|
||||
this.savingConfig = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
auto_sync_enabled: this.configForm.auto_sync_enabled,
|
||||
sync_interval_minutes: parseInt(this.configForm.sync_interval_minutes)
|
||||
};
|
||||
|
||||
if (this.configForm.api_key) {
|
||||
payload.api_key = this.configForm.api_key;
|
||||
}
|
||||
|
||||
await apiClient.post(
|
||||
`/admin/letzshop/vendors/${this.selectedVendor.vendor_id}/credentials`,
|
||||
payload
|
||||
);
|
||||
|
||||
this.showConfigModal = false;
|
||||
this.successMessage = 'Configuration saved successfully';
|
||||
await this.loadVendors();
|
||||
} catch (error) {
|
||||
console.error('[ADMIN LETZSHOP] Failed to save config:', error);
|
||||
this.error = error.message || 'Failed to save configuration';
|
||||
} finally {
|
||||
this.savingConfig = false;
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete vendor configuration
|
||||
*/
|
||||
async deleteVendorConfig() {
|
||||
if (!confirm('Are you sure you want to remove Letzshop configuration for this vendor?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/admin/letzshop/vendors/${this.selectedVendor.vendor_id}/credentials`);
|
||||
this.showConfigModal = false;
|
||||
this.successMessage = 'Configuration removed';
|
||||
await this.loadVendors();
|
||||
} catch (error) {
|
||||
console.error('[ADMIN LETZSHOP] Failed to delete config:', error);
|
||||
this.error = error.message || 'Failed to remove configuration';
|
||||
}
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Test connection for a vendor
|
||||
*/
|
||||
async testConnection(vendor) {
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(`/admin/letzshop/vendors/${vendor.vendor_id}/test`);
|
||||
|
||||
if (response.success) {
|
||||
this.successMessage = `Connection successful for ${vendor.vendor_name} (${response.response_time_ms?.toFixed(0)}ms)`;
|
||||
} else {
|
||||
this.error = response.error_details || 'Connection failed';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ADMIN LETZSHOP] Connection test failed:', error);
|
||||
this.error = error.message || 'Connection test failed';
|
||||
}
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Trigger sync for a vendor
|
||||
*/
|
||||
async triggerSync(vendor) {
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(`/admin/letzshop/vendors/${vendor.vendor_id}/sync`);
|
||||
|
||||
if (response.success) {
|
||||
this.successMessage = response.message || 'Sync completed';
|
||||
await this.loadVendors();
|
||||
} else {
|
||||
this.error = response.message || 'Sync failed';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ADMIN LETZSHOP] Sync failed:', error);
|
||||
this.error = error.message || 'Sync failed';
|
||||
}
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* View orders for a vendor
|
||||
*/
|
||||
async viewOrders(vendor) {
|
||||
this.selectedVendor = vendor;
|
||||
this.vendorOrders = [];
|
||||
this.loadingOrders = true;
|
||||
this.showOrdersModal = true;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/letzshop/vendors/${vendor.vendor_id}/orders?limit=100`);
|
||||
this.vendorOrders = response.orders || [];
|
||||
} catch (error) {
|
||||
console.error('[ADMIN LETZSHOP] Failed to load orders:', error);
|
||||
this.error = error.message || 'Failed to load orders';
|
||||
} finally {
|
||||
this.loadingOrders = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[ADMIN LETZSHOP] Module loaded');
|
||||
415
static/vendor/js/letzshop.js
vendored
Normal file
415
static/vendor/js/letzshop.js
vendored
Normal file
@@ -0,0 +1,415 @@
|
||||
// static/vendor/js/letzshop.js
|
||||
/**
|
||||
* Vendor Letzshop orders management page logic
|
||||
*/
|
||||
|
||||
console.log('[VENDOR LETZSHOP] Loading...');
|
||||
|
||||
function vendorLetzshop() {
|
||||
console.log('[VENDOR LETZSHOP] vendorLetzshop() called');
|
||||
|
||||
return {
|
||||
// Inherit base layout state
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'letzshop',
|
||||
|
||||
// Tab state
|
||||
activeTab: 'orders',
|
||||
|
||||
// Loading states
|
||||
loading: false,
|
||||
importing: false,
|
||||
saving: false,
|
||||
testing: false,
|
||||
submittingTracking: false,
|
||||
|
||||
// Messages
|
||||
error: '',
|
||||
successMessage: '',
|
||||
|
||||
// Integration status
|
||||
status: {
|
||||
is_configured: false,
|
||||
is_connected: false,
|
||||
auto_sync_enabled: false,
|
||||
last_sync_at: null,
|
||||
last_sync_status: null
|
||||
},
|
||||
|
||||
// Credentials
|
||||
credentials: null,
|
||||
credentialsForm: {
|
||||
api_key: '',
|
||||
auto_sync_enabled: false,
|
||||
sync_interval_minutes: 15
|
||||
},
|
||||
showApiKey: false,
|
||||
|
||||
// Orders
|
||||
orders: [],
|
||||
totalOrders: 0,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
filters: {
|
||||
sync_status: ''
|
||||
},
|
||||
|
||||
// Order stats
|
||||
orderStats: {
|
||||
pending: 0,
|
||||
confirmed: 0,
|
||||
rejected: 0,
|
||||
shipped: 0
|
||||
},
|
||||
|
||||
// Modals
|
||||
showTrackingModal: false,
|
||||
showOrderModal: false,
|
||||
selectedOrder: null,
|
||||
trackingForm: {
|
||||
tracking_number: '',
|
||||
tracking_carrier: ''
|
||||
},
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._vendorLetzshopInitialized) {
|
||||
return;
|
||||
}
|
||||
window._vendorLetzshopInitialized = true;
|
||||
|
||||
// Call parent init first to set vendorCode from URL
|
||||
const parentInit = data().init;
|
||||
if (parentInit) {
|
||||
await parentInit.call(this);
|
||||
}
|
||||
|
||||
await this.loadStatus();
|
||||
await this.loadOrders();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load integration status
|
||||
*/
|
||||
async loadStatus() {
|
||||
try {
|
||||
const response = await apiClient.get('/vendor/letzshop/status');
|
||||
this.status = response;
|
||||
|
||||
if (this.status.is_configured) {
|
||||
await this.loadCredentials();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to load status:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load credentials (masked)
|
||||
*/
|
||||
async loadCredentials() {
|
||||
try {
|
||||
const response = await apiClient.get('/vendor/letzshop/credentials');
|
||||
this.credentials = response;
|
||||
this.credentialsForm.auto_sync_enabled = response.auto_sync_enabled;
|
||||
this.credentialsForm.sync_interval_minutes = response.sync_interval_minutes;
|
||||
} catch (error) {
|
||||
// 404 means not configured, which is fine
|
||||
if (error.status !== 404) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to load credentials:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load orders
|
||||
*/
|
||||
async loadOrders() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
skip: ((this.page - 1) * this.limit).toString(),
|
||||
limit: this.limit.toString()
|
||||
});
|
||||
|
||||
if (this.filters.sync_status) {
|
||||
params.append('sync_status', this.filters.sync_status);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(`/vendor/letzshop/orders?${params}`);
|
||||
this.orders = response.orders;
|
||||
this.totalOrders = response.total;
|
||||
|
||||
// Calculate stats
|
||||
await this.loadOrderStats();
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to load orders:', error);
|
||||
this.error = error.message || 'Failed to load orders';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load order stats by fetching counts for each status
|
||||
*/
|
||||
async loadOrderStats() {
|
||||
try {
|
||||
// Get all orders without filter to calculate stats
|
||||
const allResponse = await apiClient.get('/vendor/letzshop/orders?limit=1000');
|
||||
const allOrders = allResponse.orders || [];
|
||||
|
||||
this.orderStats = {
|
||||
pending: allOrders.filter(o => o.sync_status === 'pending').length,
|
||||
confirmed: allOrders.filter(o => o.sync_status === 'confirmed').length,
|
||||
rejected: allOrders.filter(o => o.sync_status === 'rejected').length,
|
||||
shipped: allOrders.filter(o => o.sync_status === 'shipped').length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to load order stats:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh all data
|
||||
*/
|
||||
async refreshData() {
|
||||
await this.loadStatus();
|
||||
await this.loadOrders();
|
||||
this.successMessage = 'Data refreshed';
|
||||
setTimeout(() => this.successMessage = '', 3000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Import orders from Letzshop
|
||||
*/
|
||||
async importOrders() {
|
||||
if (!this.status.is_configured) {
|
||||
this.error = 'Please configure your API key first';
|
||||
this.activeTab = 'settings';
|
||||
return;
|
||||
}
|
||||
|
||||
this.importing = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/vendor/letzshop/orders/import', {
|
||||
operation: 'order_import'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.successMessage = response.message;
|
||||
await this.loadOrders();
|
||||
} else {
|
||||
this.error = response.message || 'Import failed';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Import failed:', error);
|
||||
this.error = error.message || 'Failed to import orders';
|
||||
} finally {
|
||||
this.importing = false;
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save credentials
|
||||
*/
|
||||
async saveCredentials() {
|
||||
if (!this.credentialsForm.api_key && !this.credentials) {
|
||||
this.error = 'Please enter an API key';
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
auto_sync_enabled: this.credentialsForm.auto_sync_enabled,
|
||||
sync_interval_minutes: parseInt(this.credentialsForm.sync_interval_minutes)
|
||||
};
|
||||
|
||||
if (this.credentialsForm.api_key) {
|
||||
payload.api_key = this.credentialsForm.api_key;
|
||||
}
|
||||
|
||||
const response = await apiClient.post('/vendor/letzshop/credentials', payload);
|
||||
this.credentials = response;
|
||||
this.credentialsForm.api_key = '';
|
||||
this.status.is_configured = true;
|
||||
this.successMessage = 'Credentials saved successfully';
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to save credentials:', error);
|
||||
this.error = error.message || 'Failed to save credentials';
|
||||
} finally {
|
||||
this.saving = false;
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Test connection
|
||||
*/
|
||||
async testConnection() {
|
||||
this.testing = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/vendor/letzshop/test');
|
||||
|
||||
if (response.success) {
|
||||
this.successMessage = `Connection successful (${response.response_time_ms?.toFixed(0)}ms)`;
|
||||
} else {
|
||||
this.error = response.error_details || 'Connection failed';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Connection test failed:', error);
|
||||
this.error = error.message || 'Connection test failed';
|
||||
} finally {
|
||||
this.testing = false;
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete credentials
|
||||
*/
|
||||
async deleteCredentials() {
|
||||
if (!confirm('Are you sure you want to remove your Letzshop credentials?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete('/vendor/letzshop/credentials');
|
||||
this.credentials = null;
|
||||
this.status.is_configured = false;
|
||||
this.credentialsForm = {
|
||||
api_key: '',
|
||||
auto_sync_enabled: false,
|
||||
sync_interval_minutes: 15
|
||||
};
|
||||
this.successMessage = 'Credentials removed';
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to delete credentials:', error);
|
||||
this.error = error.message || 'Failed to remove credentials';
|
||||
}
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm order
|
||||
*/
|
||||
async confirmOrder(order) {
|
||||
if (!confirm('Confirm this order?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(`/vendor/letzshop/orders/${order.id}/confirm`);
|
||||
|
||||
if (response.success) {
|
||||
this.successMessage = 'Order confirmed';
|
||||
await this.loadOrders();
|
||||
} else {
|
||||
this.error = response.message || 'Failed to confirm order';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to confirm order:', error);
|
||||
this.error = error.message || 'Failed to confirm order';
|
||||
}
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reject order
|
||||
*/
|
||||
async rejectOrder(order) {
|
||||
if (!confirm('Reject this order? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(`/vendor/letzshop/orders/${order.id}/reject`);
|
||||
|
||||
if (response.success) {
|
||||
this.successMessage = 'Order rejected';
|
||||
await this.loadOrders();
|
||||
} else {
|
||||
this.error = response.message || 'Failed to reject order';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to reject order:', error);
|
||||
this.error = error.message || 'Failed to reject order';
|
||||
}
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Open tracking modal
|
||||
*/
|
||||
openTrackingModal(order) {
|
||||
this.selectedOrder = order;
|
||||
this.trackingForm = {
|
||||
tracking_number: order.tracking_number || '',
|
||||
tracking_carrier: order.tracking_carrier || ''
|
||||
};
|
||||
this.showTrackingModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit tracking
|
||||
*/
|
||||
async submitTracking() {
|
||||
if (!this.trackingForm.tracking_number || !this.trackingForm.tracking_carrier) {
|
||||
this.error = 'Please fill in all fields';
|
||||
return;
|
||||
}
|
||||
|
||||
this.submittingTracking = true;
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/vendor/letzshop/orders/${this.selectedOrder.id}/tracking`,
|
||||
this.trackingForm
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
this.showTrackingModal = false;
|
||||
this.successMessage = 'Tracking information saved';
|
||||
await this.loadOrders();
|
||||
} else {
|
||||
this.error = response.message || 'Failed to save tracking';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VENDOR LETZSHOP] Failed to set tracking:', error);
|
||||
this.error = error.message || 'Failed to save tracking';
|
||||
} finally {
|
||||
this.submittingTracking = false;
|
||||
}
|
||||
setTimeout(() => this.successMessage = '', 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* View order details
|
||||
*/
|
||||
viewOrderDetails(order) {
|
||||
this.selectedOrder = order;
|
||||
this.showOrderModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user