bae705aa97
- 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
218 lines
8.4 KiB
JavaScript
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);
|
|
}
|
|
};
|