Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
405 lines
14 KiB
JavaScript
405 lines
14 KiB
JavaScript
// app/modules/billing/static/store/js/invoices.js
|
|
/**
|
|
* Store invoice management page logic
|
|
*/
|
|
|
|
const invoicesLog = window.LogConfig?.createLogger('INVOICES') || console;
|
|
|
|
invoicesLog.info('[STORE INVOICES] Loading...');
|
|
|
|
function storeInvoices() {
|
|
invoicesLog.info('[STORE INVOICES] storeInvoices() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'invoices',
|
|
|
|
// Tab state
|
|
activeTab: 'invoices',
|
|
|
|
// Loading states
|
|
loading: false,
|
|
savingSettings: false,
|
|
creatingInvoice: false,
|
|
downloadingPdf: false,
|
|
|
|
// Messages
|
|
error: '',
|
|
successMessage: '',
|
|
|
|
// Settings
|
|
hasSettings: false,
|
|
settings: null,
|
|
settingsForm: {
|
|
merchant_name: '',
|
|
merchant_address: '',
|
|
merchant_city: '',
|
|
merchant_postal_code: '',
|
|
merchant_country: 'LU',
|
|
vat_number: '',
|
|
invoice_prefix: 'INV',
|
|
default_vat_rate: '17.00',
|
|
bank_name: '',
|
|
bank_iban: '',
|
|
bank_bic: '',
|
|
payment_terms: 'Net 30 days',
|
|
footer_text: ''
|
|
},
|
|
|
|
// Stats
|
|
stats: {
|
|
total_invoices: 0,
|
|
total_revenue_cents: 0,
|
|
draft_count: 0,
|
|
issued_count: 0,
|
|
paid_count: 0,
|
|
cancelled_count: 0
|
|
},
|
|
|
|
// Invoices list
|
|
invoices: [],
|
|
totalInvoices: 0,
|
|
page: 1,
|
|
perPage: 20,
|
|
filters: {
|
|
status: ''
|
|
},
|
|
|
|
// Create invoice modal
|
|
showCreateModal: false,
|
|
createForm: {
|
|
order_id: '',
|
|
notes: ''
|
|
},
|
|
|
|
async init() {
|
|
// Guard against multiple initialization
|
|
if (window._storeInvoicesInitialized) {
|
|
return;
|
|
}
|
|
window._storeInvoicesInitialized = true;
|
|
|
|
// Call parent init first to set storeCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
await this.loadSettings();
|
|
await this.loadStats();
|
|
await this.loadInvoices();
|
|
},
|
|
|
|
/**
|
|
* Load invoice settings
|
|
*/
|
|
async loadSettings() {
|
|
try {
|
|
const response = await apiClient.get('/store/invoices/settings');
|
|
if (response) {
|
|
this.settings = response;
|
|
this.hasSettings = true;
|
|
// Populate form with existing settings
|
|
this.settingsForm = {
|
|
merchant_name: response.merchant_name || '',
|
|
merchant_address: response.merchant_address || '',
|
|
merchant_city: response.merchant_city || '',
|
|
merchant_postal_code: response.merchant_postal_code || '',
|
|
merchant_country: response.merchant_country || 'LU',
|
|
vat_number: response.vat_number || '',
|
|
invoice_prefix: response.invoice_prefix || 'INV',
|
|
default_vat_rate: response.default_vat_rate?.toString() || '17.00',
|
|
bank_name: response.bank_name || '',
|
|
bank_iban: response.bank_iban || '',
|
|
bank_bic: response.bank_bic || '',
|
|
payment_terms: response.payment_terms || 'Net 30 days',
|
|
footer_text: response.footer_text || ''
|
|
};
|
|
} else {
|
|
this.hasSettings = false;
|
|
}
|
|
} catch (error) {
|
|
// 404 means not configured yet, which is fine
|
|
if (error.status !== 404) {
|
|
invoicesLog.error('[STORE INVOICES] Failed to load settings:', error);
|
|
}
|
|
this.hasSettings = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load invoice statistics
|
|
*/
|
|
async loadStats() {
|
|
try {
|
|
const response = await apiClient.get('/store/invoices/stats');
|
|
this.stats = {
|
|
total_invoices: response.total_invoices || 0,
|
|
total_revenue_cents: response.total_revenue_cents || 0,
|
|
draft_count: response.draft_count || 0,
|
|
issued_count: response.issued_count || 0,
|
|
paid_count: response.paid_count || 0,
|
|
cancelled_count: response.cancelled_count || 0
|
|
};
|
|
} catch (error) {
|
|
invoicesLog.error('[STORE INVOICES] Failed to load stats:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load invoices list
|
|
*/
|
|
async loadInvoices() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: this.page.toString(),
|
|
per_page: this.perPage.toString()
|
|
});
|
|
|
|
if (this.filters.status) {
|
|
params.append('status', this.filters.status);
|
|
}
|
|
|
|
const response = await apiClient.get(`/store/invoices?${params}`);
|
|
this.invoices = response.items || [];
|
|
this.totalInvoices = response.total || 0;
|
|
} catch (error) {
|
|
invoicesLog.error('[STORE INVOICES] Failed to load invoices:', error);
|
|
this.error = error.message || 'Failed to load invoices';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Refresh all data
|
|
*/
|
|
async refreshData() {
|
|
await this.loadSettings();
|
|
await this.loadStats();
|
|
await this.loadInvoices();
|
|
this.successMessage = 'Data refreshed';
|
|
setTimeout(() => this.successMessage = '', 3000);
|
|
},
|
|
|
|
/**
|
|
* Save invoice settings
|
|
*/
|
|
async saveSettings() {
|
|
if (!this.settingsForm.merchant_name) {
|
|
this.error = 'Merchant name is required';
|
|
return;
|
|
}
|
|
|
|
this.savingSettings = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const payload = {
|
|
merchant_name: this.settingsForm.merchant_name,
|
|
merchant_address: this.settingsForm.merchant_address || null,
|
|
merchant_city: this.settingsForm.merchant_city || null,
|
|
merchant_postal_code: this.settingsForm.merchant_postal_code || null,
|
|
merchant_country: this.settingsForm.merchant_country || 'LU',
|
|
vat_number: this.settingsForm.vat_number || null,
|
|
invoice_prefix: this.settingsForm.invoice_prefix || 'INV',
|
|
default_vat_rate: parseFloat(this.settingsForm.default_vat_rate) || 17.0,
|
|
bank_name: this.settingsForm.bank_name || null,
|
|
bank_iban: this.settingsForm.bank_iban || null,
|
|
bank_bic: this.settingsForm.bank_bic || null,
|
|
payment_terms: this.settingsForm.payment_terms || null,
|
|
footer_text: this.settingsForm.footer_text || null
|
|
};
|
|
|
|
let response;
|
|
if (this.hasSettings) {
|
|
// Update existing settings
|
|
response = await apiClient.put('/store/invoices/settings', payload);
|
|
} else {
|
|
// Create new settings
|
|
response = await apiClient.post('/store/invoices/settings', payload);
|
|
}
|
|
|
|
this.settings = response;
|
|
this.hasSettings = true;
|
|
this.successMessage = 'Settings saved successfully';
|
|
} catch (error) {
|
|
invoicesLog.error('[STORE INVOICES] Failed to save settings:', error);
|
|
this.error = error.message || 'Failed to save settings';
|
|
} finally {
|
|
this.savingSettings = false;
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open create invoice modal
|
|
*/
|
|
openCreateModal() {
|
|
if (!this.hasSettings) {
|
|
this.error = 'Please configure invoice settings first';
|
|
this.activeTab = 'settings';
|
|
return;
|
|
}
|
|
this.createForm = {
|
|
order_id: '',
|
|
notes: ''
|
|
};
|
|
this.showCreateModal = true;
|
|
},
|
|
|
|
/**
|
|
* Create invoice from order
|
|
*/
|
|
async createInvoice() {
|
|
if (!this.createForm.order_id) {
|
|
this.error = 'Please enter an order ID';
|
|
return;
|
|
}
|
|
|
|
this.creatingInvoice = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const payload = {
|
|
order_id: parseInt(this.createForm.order_id),
|
|
notes: this.createForm.notes || null
|
|
};
|
|
|
|
const response = await apiClient.post('/store/invoices', payload);
|
|
|
|
this.showCreateModal = false;
|
|
this.successMessage = `Invoice ${response.invoice_number} created successfully`;
|
|
await this.loadStats();
|
|
await this.loadInvoices();
|
|
} catch (error) {
|
|
invoicesLog.error('[STORE INVOICES] Failed to create invoice:', error);
|
|
this.error = error.message || 'Failed to create invoice';
|
|
} finally {
|
|
this.creatingInvoice = false;
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update invoice status
|
|
*/
|
|
async updateStatus(invoice, newStatus) {
|
|
const statusLabels = {
|
|
'issued': 'mark as issued',
|
|
'paid': 'mark as paid',
|
|
'cancelled': 'cancel'
|
|
};
|
|
|
|
if (!confirm(`Are you sure you want to ${statusLabels[newStatus] || newStatus} this invoice?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await apiClient.put(`/store/invoices/${invoice.id}/status`, {
|
|
status: newStatus
|
|
});
|
|
|
|
this.successMessage = `Invoice ${invoice.invoice_number} status updated to ${newStatus}`;
|
|
await this.loadStats();
|
|
await this.loadInvoices();
|
|
} catch (error) {
|
|
invoicesLog.error('[STORE INVOICES] Failed to update status:', error);
|
|
this.error = error.message || 'Failed to update invoice status';
|
|
}
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
},
|
|
|
|
/**
|
|
* Download invoice PDF
|
|
*/
|
|
async downloadPDF(invoice) {
|
|
this.downloadingPdf = true;
|
|
|
|
try {
|
|
// Get the token for authentication
|
|
const token = localStorage.getItem('orion_token') || localStorage.getItem('store_token');
|
|
if (!token) {
|
|
throw new Error('Not authenticated');
|
|
}
|
|
|
|
// noqa: js-008 - File download needs response headers for filename
|
|
const response = await fetch(`/api/v1/store/invoices/${invoice.id}/pdf`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.detail || 'Failed to download PDF');
|
|
}
|
|
|
|
// Get filename from Content-Disposition header
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = `invoice-${invoice.invoice_number}.pdf`;
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename="(.+)"/);
|
|
if (match) {
|
|
filename = match[1];
|
|
}
|
|
}
|
|
|
|
// Download the file
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
this.successMessage = `Downloaded: ${filename}`;
|
|
} catch (error) {
|
|
invoicesLog.error('[STORE INVOICES] Failed to download PDF:', error);
|
|
this.error = error.message || 'Failed to download PDF';
|
|
} finally {
|
|
this.downloadingPdf = false;
|
|
setTimeout(() => this.successMessage = '', 5000);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return 'N/A';
|
|
const date = new Date(dateStr);
|
|
const locale = window.STORE_CONFIG?.locale || 'en-GB';
|
|
return date.toLocaleDateString(locale, {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric'
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Format currency for display
|
|
*/
|
|
formatCurrency(cents, currency = 'EUR') {
|
|
if (cents === null || cents === undefined) return 'N/A';
|
|
const amount = cents / 100;
|
|
const locale = window.STORE_CONFIG?.locale || 'en-GB';
|
|
const currencyCode = window.STORE_CONFIG?.currency || currency;
|
|
return new Intl.NumberFormat(locale, {
|
|
style: 'currency',
|
|
currency: currencyCode
|
|
}).format(amount);
|
|
}
|
|
};
|
|
}
|