194 lines
4.8 KiB
JavaScript
194 lines
4.8 KiB
JavaScript
|
|
/**
|
|||
|
|
* LandveX Finance — Entry Point
|
|||
|
|
* Sprint 3: UI-refaktorering
|
|||
|
|
*
|
|||
|
|
* Initierar appen, laddar state, sätter upp event listeners.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import state from './state.mjs';
|
|||
|
|
import { getHealth } from './api.mjs';
|
|||
|
|
import { $, on } from './utils.mjs';
|
|||
|
|
|
|||
|
|
// ── Tab-modules (lazy-loaded) ──
|
|||
|
|
const tabModules = {
|
|||
|
|
overview: () => import('./tabs/overview.mjs'),
|
|||
|
|
transactions: () => import('./tabs/transactions.mjs'),
|
|||
|
|
invoices: () => import('./tabs/invoices.mjs'),
|
|||
|
|
vat: () => import('./tabs/vat.mjs'),
|
|||
|
|
payroll: () => import('./tabs/payroll.mjs'),
|
|||
|
|
period: () => import('./tabs/period.mjs'),
|
|||
|
|
tasks: () => import('./tabs/tasks.mjs'),
|
|||
|
|
forecast: () => import('./tabs/forecast.mjs'),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let currentTabModule = null;
|
|||
|
|
|
|||
|
|
// ── Init ──
|
|||
|
|
export async function init() {
|
|||
|
|
console.log('[finance] init start');
|
|||
|
|
|
|||
|
|
// 1. Bygg period-väljare
|
|||
|
|
buildPeriodSelects();
|
|||
|
|
|
|||
|
|
// 2. Sätt upp globala event listeners
|
|||
|
|
setupGlobalEvents();
|
|||
|
|
|
|||
|
|
// 3. Ladda initial tab
|
|||
|
|
await switchTab(state.get().tab);
|
|||
|
|
|
|||
|
|
// 4. Kolla health
|
|||
|
|
checkHealth();
|
|||
|
|
|
|||
|
|
// 5. Sätt upp state-subscribers
|
|||
|
|
state.subscribe(onStateChange);
|
|||
|
|
|
|||
|
|
console.log('[finance] init done');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Tab-switching ──
|
|||
|
|
export async function switchTab(tab) {
|
|||
|
|
if (!tabModules[tab]) {
|
|||
|
|
console.error('[finance] unknown tab:', tab);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Deaktivera nuvarande tab
|
|||
|
|
if (currentTabModule?.deactivate) {
|
|||
|
|
currentTabModule.deactivate();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Uppdatera UI
|
|||
|
|
document.querySelectorAll('.tab-sec').forEach(s => s.classList.remove('active'));
|
|||
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|||
|
|
|
|||
|
|
const tabEl = $(`tab-${tab}`);
|
|||
|
|
if (tabEl) tabEl.classList.add('active');
|
|||
|
|
|
|||
|
|
const tabIndex = {
|
|||
|
|
overview: 0, transactions: 1, invoices: 2, vat: 3,
|
|||
|
|
payroll: 4, period: 5, tasks: 6, forecast: 7
|
|||
|
|
};
|
|||
|
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
|||
|
|
if (tabBtns[tabIndex[tab] ?? 0]) {
|
|||
|
|
tabBtns[tabIndex[tab] ?? 0].classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Uppdatera state
|
|||
|
|
state.setTab(tab);
|
|||
|
|
|
|||
|
|
// Lazy-load modul
|
|||
|
|
try {
|
|||
|
|
const module = await tabModules[tab]();
|
|||
|
|
currentTabModule = module;
|
|||
|
|
|
|||
|
|
if (module.activate) {
|
|||
|
|
await module.activate(state.get().period);
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('[finance] failed to load tab:', tab, e);
|
|||
|
|
showError(`Kunde inte ladda ${tab}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Period ──
|
|||
|
|
function buildPeriodSelects() {
|
|||
|
|
const FISCAL = state.get().fiscalYear;
|
|||
|
|
const ms = $('sel-month');
|
|||
|
|
if (!ms) return;
|
|||
|
|
|
|||
|
|
ms.innerHTML = '';
|
|||
|
|
for (let m = 1; m <= 12; m++) {
|
|||
|
|
const v = `${FISCAL}-${String(m).padStart(2, '0')}`;
|
|||
|
|
const o = document.createElement('option');
|
|||
|
|
o.value = v;
|
|||
|
|
o.textContent = `${MN[m - 1]} ${FISCAL}`;
|
|||
|
|
if (v === state.get().period) o.selected = true;
|
|||
|
|
ms.appendChild(o);
|
|||
|
|
}
|
|||
|
|
ms.onchange = () => state.setPeriod(ms.value);
|
|||
|
|
|
|||
|
|
const qs = $('sel-quarter');
|
|||
|
|
if (qs) {
|
|||
|
|
qs.innerHTML = '';
|
|||
|
|
const curM = parseInt((state.get().period || '').split('-')[1] || 6);
|
|||
|
|
for (let q = 1; q <= 4; q++) {
|
|||
|
|
const o = document.createElement('option');
|
|||
|
|
o.value = `Q${q}-${FISCAL}`;
|
|||
|
|
o.textContent = `Q${q} ${['Jan–Mar', 'Apr–Jun', 'Jul–Sep', 'Okt–Dec'][q - 1]} ${FISCAL}`;
|
|||
|
|
if (Math.ceil(curM / 3) === q) o.selected = true;
|
|||
|
|
qs.appendChild(o);
|
|||
|
|
}
|
|||
|
|
qs.onchange = () => state.setPeriod(qs.value);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Globala events ──
|
|||
|
|
function setupGlobalEvents() {
|
|||
|
|
// Escape stänger modaler
|
|||
|
|
on(document, 'keydown', (e) => {
|
|||
|
|
if (e.key === 'Escape') {
|
|||
|
|
closeAllModals();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Window resize (debounced)
|
|||
|
|
let resizeTimer;
|
|||
|
|
on(window, 'resize', () => {
|
|||
|
|
clearTimeout(resizeTimer);
|
|||
|
|
resizeTimer = setTimeout(() => {
|
|||
|
|
state.set({ windowWidth: window.innerWidth });
|
|||
|
|
}, 250);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── State change handler ──
|
|||
|
|
function onStateChange(s) {
|
|||
|
|
// Uppdatera loading-indikatorer
|
|||
|
|
const isLoading = s.loading.size > 0;
|
|||
|
|
const statusEl = $('data-status');
|
|||
|
|
if (statusEl) {
|
|||
|
|
statusEl.innerHTML = isLoading
|
|||
|
|
? '<span class="sdot"></span><span>Laddar…</span>'
|
|||
|
|
: '<span class="sdot live"></span><span>Live</span>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Health check ──
|
|||
|
|
async function checkHealth() {
|
|||
|
|
try {
|
|||
|
|
await getHealth();
|
|||
|
|
console.log('[finance] health: ok');
|
|||
|
|
} catch (e) {
|
|||
|
|
console.warn('[finance] health check failed:', e.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Modaler ──
|
|||
|
|
export function openModal(id) {
|
|||
|
|
const el = $(id);
|
|||
|
|
if (el) el.classList.remove('hidden');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function closeModal(id) {
|
|||
|
|
const el = $(id);
|
|||
|
|
if (el) el.classList.add('hidden');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function closeAllModals() {
|
|||
|
|
document.querySelectorAll('.modal-bg').forEach(m => m.classList.add('hidden'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Felhantering ──
|
|||
|
|
export function showError(msg) {
|
|||
|
|
console.error('[finance]', msg);
|
|||
|
|
// TODO: Toast/notifikation
|
|||
|
|
alert(msg); // Fallback
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Start ──
|
|||
|
|
if (document.readyState === 'loading') {
|
|||
|
|
document.addEventListener('DOMContentLoaded', init);
|
|||
|
|
} else {
|
|||
|
|
init();
|
|||
|
|
}
|