Files
orion/static/vendor/js/invoices.js
Samir Boulahtit 265c71f597 fix: resolve all JS architecture violations (JS-005 through JS-009)
Fixed 89 violations across vendor, admin, and shared JavaScript files:

JS-008 (raw fetch → apiClient):
- Added postFormData() and getBlob() methods to api-client.js
- Updated inventory.js, messages.js to use apiClient.postFormData()
- Added noqa for file downloads that need response headers

JS-009 (window.showToast → Utils.showToast):
- Updated admin/messages.js, notifications.js, vendor/messages.js
- Replaced alert() in customers.js

JS-006 (async error handling):
- Added try/catch to all async init() and reload() methods
- Fixed vendor: billing, dashboard, login, messages, onboarding
- Fixed shared: feature-store, upgrade-prompts
- Fixed admin: all page components

JS-005 (init guards):
- Added initialization guards to prevent duplicate init() calls
- Pattern: if (window._componentInitialized) return;

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 21:32:19 +01:00

402 lines
14 KiB
JavaScript

// static/vendor/js/invoices.js
/**
* Vendor invoice management page logic
*/
const invoicesLog = window.LogConfig?.createLogger('INVOICES') || console;
invoicesLog.info('[VENDOR INVOICES] Loading...');
function vendorInvoices() {
invoicesLog.info('[VENDOR INVOICES] vendorInvoices() 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: {
company_name: '',
company_address: '',
company_city: '',
company_postal_code: '',
company_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._vendorInvoicesInitialized) {
return;
}
window._vendorInvoicesInitialized = true;
// Call parent init first to set vendorCode 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('/vendor/invoices/settings');
if (response) {
this.settings = response;
this.hasSettings = true;
// Populate form with existing settings
this.settingsForm = {
company_name: response.company_name || '',
company_address: response.company_address || '',
company_city: response.company_city || '',
company_postal_code: response.company_postal_code || '',
company_country: response.company_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('[VENDOR INVOICES] Failed to load settings:', error);
}
this.hasSettings = false;
}
},
/**
* Load invoice statistics
*/
async loadStats() {
try {
const response = await apiClient.get('/vendor/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('[VENDOR 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(`/vendor/invoices?${params}`);
this.invoices = response.items || [];
this.totalInvoices = response.total || 0;
} catch (error) {
invoicesLog.error('[VENDOR 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.company_name) {
this.error = 'Company name is required';
return;
}
this.savingSettings = true;
this.error = '';
try {
const payload = {
company_name: this.settingsForm.company_name,
company_address: this.settingsForm.company_address || null,
company_city: this.settingsForm.company_city || null,
company_postal_code: this.settingsForm.company_postal_code || null,
company_country: this.settingsForm.company_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('/vendor/invoices/settings', payload);
} else {
// Create new settings
response = await apiClient.post('/vendor/invoices/settings', payload);
}
this.settings = response;
this.hasSettings = true;
this.successMessage = 'Settings saved successfully';
} catch (error) {
invoicesLog.error('[VENDOR 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('/vendor/invoices', payload);
this.showCreateModal = false;
this.successMessage = `Invoice ${response.invoice_number} created successfully`;
await this.loadStats();
await this.loadInvoices();
} catch (error) {
invoicesLog.error('[VENDOR 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(`/vendor/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('[VENDOR 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('wizamart_token') || localStorage.getItem('vendor_token');
if (!token) {
throw new Error('Not authenticated');
}
// noqa: js-008 - File download needs response headers for filename
const response = await fetch(`/api/v1/vendor/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('[VENDOR 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);
return date.toLocaleDateString('en-GB', {
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;
return new Intl.NumberFormat('de-LU', {
style: 'currency',
currency: currency
}).format(amount);
}
};
}