173 lines
9.9 KiB
JavaScript
173 lines
9.9 KiB
JavaScript
|
|
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 => `<div class="card stat-overview-card"><div class="d-flex justify-content-between align-items-center"><div><div class="metric-label">${item.label}</div><div class="metric-value">${item.value}</div></div><span class="metric-icon"><i class="fa-solid ${item.icon}"></i></span></div><div class="small text-body-secondary mt-2">${text.vs_prev || 'Vs previous year'}: ${Number(comparison.percent_change || 0).toFixed(1)}%</div></div>`).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 => `<div class="top-expense-item"><div><strong>${x.title}</strong><div class="small text-body-secondary">${x.date}</div></div><div class="top-expense-amount">${x.amount}</div></div>`).join('')
|
|
: `<div class="text-body-secondary">${text.no_data || 'No data'}</div>`;
|
|
}
|
|
});
|
|
|
|
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}});
|
|
}
|
|
});
|