document.addEventListener('DOMContentLoaded', async () => { const previewButtons = document.querySelectorAll('.preview-trigger'); const previewModalImage = document.getElementById('previewModalImage'); previewButtons.forEach(button => button.addEventListener('click', () => { if (previewModalImage) previewModalImage.src = button.dataset.preview; })); setupDocumentEditor(); if (!window.expenseStatsYear || typeof Chart === 'undefined') return; const query = new URLSearchParams({ year: window.expenseStatsYear, month: window.expenseStatsMonth || 0, start_year: window.expenseStatsStartYear || window.expenseStatsYear, end_year: window.expenseStatsEndYear || window.expenseStatsYear }); const response = await fetch(`/analytics/data?${query.toString()}`); if (!response.ok) return; const payload = await response.json(); const text = window.expenseStatsText || {}; const overview = document.getElementById('stats-overview'); if (overview) { const comparison = payload.comparison || {}; overview.innerHTML = [ { icon: 'fa-wallet', label: text.total || 'Total', value: payload.overview.total.toFixed(2) }, { icon: 'fa-list-check', label: text.count || 'Count', value: payload.overview.count }, { icon: 'fa-calculator', label: text.average || 'Average', value: payload.overview.average.toFixed(2) }, { icon: 'fa-rotate-left', label: text.refunds || 'Refunds', value: payload.overview.refunds.toFixed(2) }, ].map(item => `
${item.label}
${item.value}
${text.vs_prev || 'Vs previous year'}: ${Number(comparison.percent_change || 0).toFixed(1)}%
`).join(''); } const chartDefaults = { responsive: true, maintainAspectRatio: false, resizeDelay: 150 }; const buildChart = (id, config) => { const canvas = document.getElementById(id); if (!canvas) return; canvas.style.height = '100%'; new Chart(canvas, config); }; buildChart('chart-monthly', {type: 'line', data: { labels: payload.yearly_totals.map(x => x.month), datasets: [{ label: text.total || 'Amount', data: payload.yearly_totals.map(x => x.amount), tension: 0.35, fill: false }] }, options: chartDefaults}); buildChart('chart-categories', {type: 'doughnut', data: { labels: payload.category_totals.map(x => x.category), datasets: [{ data: payload.category_totals.map(x => x.amount) }] }, options: chartDefaults}); buildChart('chart-payments', {type: 'bar', data: { labels: payload.payment_methods.map(x => x.method), datasets: [{ label: text.total || 'Amount', data: payload.payment_methods.map(x => x.amount) }] }, options: chartDefaults}); buildChart('chart-range', {type: 'bar', data: { labels: payload.range_totals.map(x => x.year), datasets: [{ label: text.total || 'Amount', data: payload.range_totals.map(x => x.amount) }] }, options: chartDefaults}); buildChart('chart-quarterly', {type: 'bar', data: { labels: payload.quarterly_totals.map(x => x.quarter), datasets: [{ label: text.total || 'Amount', data: payload.quarterly_totals.map(x => x.amount) }] }, options: chartDefaults}); buildChart('chart-weekdays', {type: 'line', data: { labels: payload.weekday_totals.map(x => x.day), datasets: [{ label: text.total || 'Amount', data: payload.weekday_totals.map(x => x.amount), tension: 0.35, fill: false }] }, options: chartDefaults}); const top = document.getElementById('top-expenses'); if (top) { top.innerHTML = payload.top_expenses.length ? payload.top_expenses.map(x => `
${x.title}
${x.date}
${x.amount}
`).join('') : `
${text.no_data || 'No data'}
`; } }); function setupDocumentEditor() { const fileInput = document.getElementById('documentInput'); const cameraButton = document.getElementById('cameraCaptureButton'); const pickerButton = document.getElementById('filePickerButton'); const uploadHint = document.getElementById('documentInputHint'); const dropZone = document.getElementById('dropUploadZone'); const img = document.getElementById('documentPreviewImage'); const empty = document.getElementById('documentPreviewEmpty'); const stage = document.getElementById('documentPreviewStage'); const selection = document.getElementById('cropSelection'); const rotateField = document.getElementById('rotateField'); const scaleField = document.getElementById('scaleField'); const cropX = document.querySelector('input[name="crop_x"]'); const cropY = document.querySelector('input[name="crop_y"]'); const cropW = document.querySelector('input[name="crop_w"]'); const cropH = document.querySelector('input[name="crop_h"]'); if (!fileInput || !img || !stage) return; const isMobile = window.matchMedia('(max-width: 991px)').matches && (navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches); const desktopHint = uploadHint?.dataset.desktopHint || uploadHint?.textContent || ''; const mobileHint = uploadHint?.dataset.mobileHint || desktopHint; if (cameraButton) cameraButton.classList.toggle('d-none', !isMobile); if (uploadHint) uploadHint.textContent = isMobile ? mobileHint : desktopHint; pickerButton?.addEventListener('click', () => { fileInput.removeAttribute('capture'); fileInput.click(); }); cameraButton?.addEventListener('click', () => { fileInput.setAttribute('capture', 'environment'); fileInput.click(); }); let editorState = { rotate: Number(rotateField?.value || 0), scale: Number(scaleField?.value || 100) }; let drag = null; const renderTransform = () => { img.style.transform = `rotate(${editorState.rotate}deg) scale(${editorState.scale / 100})`; if (rotateField) rotateField.value = editorState.rotate; if (scaleField) scaleField.value = editorState.scale; }; const handleFiles = () => { const file = fileInput.files?.[0]; if (!file || !file.type.startsWith('image/')) { if (empty) empty.classList.remove('d-none'); img.classList.add('d-none'); return; } const reader = new FileReader(); reader.onload = e => { img.src = String(e.target?.result || ''); img.classList.remove('d-none'); if (empty) empty.classList.add('d-none'); renderTransform(); }; reader.readAsDataURL(file); }; fileInput.addEventListener('change', handleFiles); if (dropZone) {['dragenter','dragover'].forEach(eventName => dropZone.addEventListener(eventName, event => { event.preventDefault(); dropZone.classList.add('is-dragover'); })); ['dragleave','drop'].forEach(eventName => dropZone.addEventListener(eventName, event => { event.preventDefault(); dropZone.classList.remove('is-dragover'); })); dropZone.addEventListener('drop', event => { const dt = event.dataTransfer; if (!dt?.files?.length) return; fileInput.files = dt.files; handleFiles(); }); dropZone.addEventListener('click', ()=>fileInput.click()); } document.querySelectorAll('.js-rotate').forEach(btn => btn.addEventListener('click', () => { editorState.rotate = (editorState.rotate + Number(btn.dataset.step || 0) + 360) % 360; renderTransform(); })); document.querySelectorAll('.js-scale').forEach(btn => btn.addEventListener('click', () => { editorState.scale = Math.max(20, Math.min(200, editorState.scale + Number(btn.dataset.step || 0))); renderTransform(); })); document.getElementById('editorReset')?.addEventListener('click', () => { editorState = { rotate: 0, scale: 100 }; renderTransform(); [cropX, cropY, cropW, cropH].forEach(field => { if (field) field.value = ''; }); selection?.classList.add('d-none'); selection?.setAttribute('style', ''); }); stage.addEventListener('pointerdown', e => { const rect = stage.getBoundingClientRect(); drag = { startX: e.clientX - rect.left, startY: e.clientY - rect.top }; if (selection) { selection.classList.remove('d-none'); selection.style.left = `${drag.startX}px`; selection.style.top = `${drag.startY}px`; selection.style.width = '0px'; selection.style.height = '0px'; } }); stage.addEventListener('pointermove', e => { if (!drag || !selection) return; const rect = stage.getBoundingClientRect(); const currentX = Math.max(0, Math.min(rect.width, e.clientX - rect.left)); const currentY = Math.max(0, Math.min(rect.height, e.clientY - rect.top)); const left = Math.min(drag.startX, currentX); const top = Math.min(drag.startY, currentY); const width = Math.abs(currentX - drag.startX); const height = Math.abs(currentY - drag.startY); Object.assign(selection.style, { left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px` }); if (cropX) cropX.value = Math.round(left); if (cropY) cropY.value = Math.round(top); if (cropW) cropW.value = Math.round(width); if (cropH) cropH.value = Math.round(height); }); const stopDrag = () => { drag = null; }; stage.addEventListener('pointerup', stopDrag); stage.addEventListener('pointerleave', stopDrag); renderTransform(); } document.addEventListener('DOMContentLoaded', () => { if (typeof Chart !== 'undefined' && window.dashboardCategoryData) { const c1 = document.getElementById('dashboard-category-chart'); if (c1) new Chart(c1, {type:'doughnut', data:{labels: window.dashboardCategoryData.map(x=>x.label), datasets:[{data:window.dashboardCategoryData.map(x=>x.amount)}]}, options:{responsive:true, maintainAspectRatio:false}}); const c2 = document.getElementById('dashboard-payment-chart'); if (c2) new Chart(c2, {type:'bar', data:{labels: window.dashboardPaymentData.map(x=>x.method), datasets:[{data:window.dashboardPaymentData.map(x=>x.amount)}]}, options:{responsive:true, maintainAspectRatio:false}}); } });