Files
boc/aamos-ledger/ui/js/views/journal.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

218 lines
8.4 KiB
JavaScript

// ═══════════════════════════════════════════════════════════════════════════
// LandveX Finance — Journal View
// ═══════════════════════════════════════════════════════════════════════════
export const Journal = {
async render(app) {
app.render(`
<div class="card">
<div class="card-header">
<h2 class="card-title">Verifikationer</h2>
<button class="btn btn-primary btn-sm" id="btn-new-entry">+ Ny</button>
</div>
<div class="card-body" id="journal-content">
<div class="skeleton" style="height:300px"></div>
</div>
</div>
`);
document.getElementById('btn-new-entry').addEventListener('click', () => {
this.showEntryForm(app);
});
try {
const data = await app.api.get('/journal?fiscal_year=2026&period=2026-06');
this.renderJournal(data, app);
} catch (e) {
document.getElementById('journal-content').innerHTML =
`<p style="color:var(--red)">Fel: ${e.message}</p>`;
}
},
renderJournal(data, app) {
if (!data.ok || !data.entries) return;
document.getElementById('journal-content').innerHTML = `
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Nr</th>
<th>Datum</th>
<th>Beskrivning</th>
<th>Belopp</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${data.entries.slice(0, 50).map(e => `
<tr data-id="${e.id}" style="cursor:pointer">
<td><strong>#${e.entry_number || '—'}</strong></td>
<td>${e.entry_date?.split('T')[0] || ''}</td>
<td>${e.description}</td>
<td class="num">${this.fmt(e.total_amount || 0)}</td>
<td><span class="badge badge-${e.status}">${e.status}</span></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
${data.entries.length > 50 ? `<p style="text-align:center;color:var(--text-3);margin-top:12px;font-size:.8rem">Visar 50 av ${data.entries.length} verifikationer</p>` : ''}
`;
// Click handlers
document.querySelectorAll('#journal-content tbody tr').forEach(tr => {
tr.addEventListener('click', () => {
const id = tr.dataset.id;
this.showEntryDetail(id, app);
});
});
},
showEntryForm(app) {
document.getElementById('journal-content').innerHTML = `
<form id="entry-form">
<div class="form-group">
<label>Datum</label>
<input type="date" name="entry_date" value="${new Date().toISOString().split('T')[0]}" required>
</div>
<div class="form-group">
<label>Beskrivning</label>
<input type="text" name="description" placeholder="Vad gäller verifikationen?" required>
</div>
<div class="form-group">
<label>Referens</label>
<input type="text" name="reference" placeholder="Fakturanr, kvittonr, etc.">
</div>
<div id="lines-container">
<div class="form-group line-row">
<label>Rad 1</label>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
<input type="text" name="account" placeholder="Konto" required>
<input type="number" name="debit" placeholder="Debet" step="0.01">
<input type="number" name="credit" placeholder="Kredit" step="0.01">
</div>
</div>
<div class="form-group line-row">
<label>Rad 2</label>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
<input type="text" name="account" placeholder="Konto" required>
<input type="number" name="debit" placeholder="Debet" step="0.01">
<input type="number" name="credit" placeholder="Kredit" step="0.01">
</div>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:16px">
<button type="submit" class="btn btn-primary">Spara verifikation</button>
<button type="button" class="btn btn-secondary" id="btn-cancel">Avbryt</button>
</div>
</form>
`;
document.getElementById('btn-cancel').addEventListener('click', () => {
this.render(app);
});
document.getElementById('entry-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const lines = [];
form.querySelectorAll('.line-row').forEach(row => {
const account = row.querySelector('[name="account"]').value;
const debit = parseFloat(row.querySelector('[name="debit"]').value) || 0;
const credit = parseFloat(row.querySelector('[name="credit"]').value) || 0;
if (account && (debit || credit)) {
lines.push({ account_number: account, debit, credit, description: '' });
}
});
const payload = {
entry_date: form.entry_date.value,
description: form.description.value,
reference: form.reference.value,
lines
};
try {
await app.api.post('/journal', payload);
alert('Verifikation sparad!');
this.render(app);
} catch (err) {
alert('Fel: ' + (err.data?.error || err.message));
}
});
},
async showEntryDetail(id, app) {
try {
const data = await app.api.get(`/journal/${id}`);
if (!data.ok) return;
const e = data.entry;
document.getElementById('journal-content').innerHTML = `
<div style="margin-bottom:16px">
<button class="btn btn-ghost btn-sm" id="btn-back">← Tillbaka</button>
</div>
<div class="card" style="margin:0">
<div class="card-header">
<div>
<h2 class="card-title">Verifikation #${e.entry_number}</h2>
<p style="font-size:.8rem;color:var(--text-2)">${e.description}</p>
</div>
<span class="badge badge-${e.status}">${e.status}</span>
</div>
<div class="card-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;font-size:.85rem">
<div><strong>Datum:</strong> ${e.entry_date?.split('T')[0]}</div>
<div><strong>Referens:</strong> ${e.reference || '—'}</div>
<div><strong>Skapad:</strong> ${e.created_at?.split('T')[0]}</div>
<div><strong>Period:</strong> ${e.period}</div>
</div>
<h3 style="font-size:.8rem;color:var(--text-2);margin-bottom:8px">Kontering</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Konto</th><th class="num">Debet</th><th class="num">Kredit</th></tr></thead>
<tbody>
${data.lines.map(l => `
<tr>
<td><strong>${l.account_number}</strong> ${l.description || ''}</td>
<td class="num">${l.debit ? this.fmt(l.debit) : ''}</td>
<td class="num">${l.credit ? this.fmt(l.credit) : ''}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
${e.status === 'draft' ? `
<div style="display:flex;gap:8px;margin-top:16px">
<button class="btn btn-primary" id="btn-post">Kontera</button>
<button class="btn btn-secondary" id="btn-delete">Ta bort</button>
</div>
` : ''}
</div>
</div>
`;
document.getElementById('btn-back').addEventListener('click', () => this.render(app));
if (e.status === 'draft') {
document.getElementById('btn-post').addEventListener('click', async () => {
try {
await app.api.post(`/journal/${id}/post`);
alert('Verifikation konterad!');
this.render(app);
} catch (err) {
alert('Fel: ' + (err.data?.error || err.message));
}
});
}
} catch (e) {
console.error(e);
}
},
fmt(n) {
return new Intl.NumberFormat('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n);
}
};