diff --git a/public/js/views/accounting-view.js b/public/js/views/accounting-view.js
index 3bb89d6..cdb3f88 100644
--- a/public/js/views/accounting-view.js
+++ b/public/js/views/accounting-view.js
@@ -280,9 +280,17 @@ function renderRegisterTable(result) {
const el = document.getElementById('accounting-register-table');
if (!el) return;
- const rows = result.rows || [];
+ let rows = result.rows || [];
const meta = result.meta || {};
+ // ── Punkt 1: neueste Einträge oben ──
+ rows = rows.slice().sort((a, b) => {
+ const da = a.date || '';
+ const db = b.date || '';
+ if (db !== da) return db.localeCompare(da);
+ return 0;
+ });
+
if (!rows.length) {
el.innerHTML = `
@@ -291,27 +299,20 @@ function renderRegisterTable(result) {
return;
}
- const tbody = rows.map(r => `
-
- | ${escapeHtml(r.date || '')} |
- ${escapeHtml(r.type || '')} |
- ${escapeHtml(r.docNum || '')} |
- ${escapeHtml(r.payee || '')} |
- ${escapeHtml(r.splitAccount || '')} |
- ${escapeHtml((r.memo || '').slice(0, 60))} |
-
- ${r.amount != null ? fmtMoney(r.amount) : ''}
- |
-
- `).join('');
+ const tbody = rows.map(r => renderRegisterRow(r)).join('');
+ // ── Punkt 5: dynamische Anzahl ──
el.innerHTML = `
-
- ${escapeHtml(meta.reportName || 'Transaction List')}
- ${meta.startPeriod ? '— ' + escapeHtml(meta.startPeriod) : ''}
- ${meta.endPeriod ? ' to ' + escapeHtml(meta.endPeriod) : ''}
- · ${rows.length} rows
+
+
+ ${escapeHtml(meta.reportName || 'Transaction List')}
+ ${meta.startPeriod ? '— ' + escapeHtml(meta.startPeriod) : ''}
+ ${meta.endPeriod ? ' to ' + escapeHtml(meta.endPeriod) : ''}
+
+
+ ${rows.length} ${rows.length === 1 ? 'transaction' : 'transactions'}
+
@@ -323,6 +324,7 @@ function renderRegisterTable(result) {
| Payee |
Split / Category |
Memo |
+ ✓ |
Amount |
@@ -332,6 +334,47 @@ function renderRegisterTable(result) {
`;
}
+function renderRegisterRow(r) {
+ const isSplit = r.splitAccount === '-Split-';
+ const splitCellContent = isSplit ? renderSplitCell(r) : escapeHtml(r.splitAccount || '');
+
+ // Punkt 3: Cleared-Status anzeigen mit Farbe
+ let clrBadge = '';
+ if (r.clearedStatus === 'R') {
+ clrBadge = `R`;
+ } else if (r.clearedStatus === 'C') {
+ clrBadge = `C`;
+ }
+
+ return `
+
+ | ${escapeHtml(r.date || '')} |
+ ${escapeHtml(r.type || '')} |
+ ${escapeHtml(r.docNum || '')} |
+ ${escapeHtml(r.payee || '')} |
+ ${splitCellContent} |
+ ${escapeHtml(r.memo || '')} |
+ ${clrBadge} |
+
+ ${r.amount != null ? fmtMoney(r.amount) : ''}
+ |
+
`;
+}
+
+// Punkt 4: Split-Aufschlüsselung
+function renderSplitCell(r) {
+ if (!r.splits || !r.splits.length) {
+ return `-Split-`;
+ }
+ const lines = r.splits.map(s => `
+
+ ${escapeHtml(s.account || '?')}
+ ${s.amount != null ? fmtMoney(s.amount) : ''}
+
+ `).join('');
+ return `${lines}
`;
+}
+
// ────────────────────────────────────────────────────────────────────
// Reports — P&L + Balance Sheet
// ────────────────────────────────────────────────────────────────────
diff --git a/src/services/accounting-service.js b/src/services/accounting-service.js
index ab38574..81b89e4 100644
--- a/src/services/accounting-service.js
+++ b/src/services/accounting-service.js
@@ -113,7 +113,7 @@ async function listAccounts({ type = null, activeOnly = true } = {}) {
* @param {string} opts.startDate - YYYY-MM-DD
* @param {string} opts.endDate - YYYY-MM-DD
*/
-async function getRegister({ accountId, startDate, endDate }) {
+async function getRegister({ accountId, startDate, endDate, includeSplits = true }) {
if (!accountId) throw new Error('accountId is required');
const { companyId, baseUrl } = getClientInfo();
@@ -121,7 +121,6 @@ async function getRegister({ accountId, startDate, endDate }) {
const params = new URLSearchParams();
if (startDate) params.set('start_date', startDate);
if (endDate) params.set('end_date', endDate);
- // account filter: TransactionList akzeptiert eine kommaseparierte Liste
params.set('source_account', String(accountId));
params.set('minorversion', QBO_MINOR_VERSION);
@@ -131,7 +130,24 @@ async function getRegister({ accountId, startDate, endDate }) {
const data = getJson(response);
throwIfFault(data, 'TransactionList report');
- return normalizeTransactionListReport(data);
+ const result = normalizeTransactionListReport(data);
+
+ // ── NEU: Split-Details nachladen ──
+ if (includeSplits) {
+ const splitRows = result.rows.filter(r =>
+ r.splitAccount === '-Split-' && r.qboId
+ );
+ if (splitRows.length) {
+ const splits = await fetchSplitDetails(splitRows);
+ result.rows.forEach(r => {
+ if (r.qboId && splits[r.qboId]) {
+ r.splits = splits[r.qboId];
+ }
+ });
+ }
+ }
+
+ return result;
}
/**
@@ -164,7 +180,7 @@ function normalizeTransactionListReport(report) {
const idxAccount = resolve('Account', 'account_name');
const idxMemo = resolve('Memo/Description', 'Memo', 'memo');
const idxSplit = resolve('Split', 'split_acc');
- const idxAmount = resolve('Amount', 'subt_nat_amount', 'subt_nat_home_amount');
+ const idxCleared = resolve('Cleared', 'cleared_status', 'clr');
const cellAt = (colData, idx) => {
if (idx == null) return null;
@@ -192,6 +208,7 @@ function normalizeTransactionListReport(report) {
const memoCell = cellAt(r.ColData, idxMemo);
const splitCell = cellAt(r.ColData, idxSplit);
const amtCell = cellAt(r.ColData, idxAmount);
+ const clrCell = cellAt(r.ColData, idxCleared);
const qboId =
(dateCell && dateCell.id) ||
@@ -199,6 +216,14 @@ function normalizeTransactionListReport(report) {
(typeCell && typeCell.id) ||
null;
+ // ── NEU: Cleared-Status normalisieren ──
+ // QBO liefert "R" (reconciled), "C" (cleared) oder leer (uncleared)
+ let clearedStatus = null;
+ if (clrCell && clrCell.value) {
+ const v = String(clrCell.value).trim().toUpperCase();
+ if (v === 'R' || v === 'C') clearedStatus = v;
+ }
+
rows.push({
date: dateCell ? dateCell.value : null,
type: typeCell ? typeCell.value : null,
@@ -209,6 +234,7 @@ function normalizeTransactionListReport(report) {
amount: amtCell && amtCell.value !== '' && amtCell.value != null
? Number(amtCell.value) : null,
splitAccount: splitCell ? splitCell.value : null,
+ clearedStatus,
qboId
});
}
@@ -278,12 +304,87 @@ async function getBalanceSheet({ asOfDate, accountingMethod = 'Accrual' } = {})
throwIfFault(data, 'BalanceSheet report');
return data;
}
+/**
+ * Lädt für eine Liste von Split-Transaktionen die Einzel-Lines aus QBO.
+ * Wir laden nur das, was als Split markiert ist und einen qboId hat.
+ *
+ * Returns: Map>
+ */
+async function fetchSplitDetails(splitRows) {
+ if (!splitRows || splitRows.length === 0) return {};
+ const { companyId, baseUrl } = getClientInfo();
+
+ // Group by Type, weil QBO unterschiedliche Endpoints für Purchase/Deposit/JournalEntry hat
+ const result = {};
+
+ for (const row of splitRows) {
+ if (!row.qboId) continue;
+
+ // Type → QBO endpoint name
+ const endpoint = mapTypeToEndpoint(row.type);
+ if (!endpoint) continue;
+
+ try {
+ const url = withMinorVersion(
+ `${baseUrl}/v3/company/${companyId}/${endpoint}/${row.qboId}`
+ );
+ const response = await makeQboApiCall({ url, method: 'GET' });
+ const data = getJson(response);
+
+ // Response shape: { Purchase: {...} } or { Deposit: {...} } etc.
+ const txn = data[capitalize(endpoint)] || data[endpoint];
+ if (!txn || !txn.Line) continue;
+
+ const lines = txn.Line
+ .filter(l => l.DetailType !== 'SubTotalLineDetail')
+ .map(l => {
+ const detail = l.AccountBasedExpenseLineDetail
+ || l.DepositLineDetail
+ || l.JournalEntryLineDetail
+ || {};
+ const acctRef = detail.AccountRef || {};
+ return {
+ account: acctRef.name || null,
+ amount: l.Amount != null ? Number(l.Amount) : null,
+ description: l.Description || null
+ };
+ })
+ .filter(l => l.account || l.amount != null);
+
+ if (lines.length) result[row.qboId] = lines;
+ } catch (err) {
+ // Einzelne Fehler nicht den ganzen Register killen lassen
+ console.warn(`Split detail fetch failed for ${row.type} ${row.qboId}:`, err.message);
+ }
+ }
+
+ return result;
+}
+
+function mapTypeToEndpoint(type) {
+ if (!type) return null;
+ const t = type.toLowerCase();
+ if (t.includes('expense') || t.includes('check')) return 'purchase';
+ if (t.includes('deposit')) return 'deposit';
+ if (t.includes('journal')) return 'journalentry';
+ if (t.includes('bill payment')) return 'billpayment';
+ if (t.includes('bill')) return 'bill';
+ if (t.includes('credit card')) return 'purchase';
+ if (t.includes('paycheck') || t.includes('payroll')) return null; // QBO blockt Paycheck-API
+ if (t.includes('tax payment')) return null; // dito
+ return null;
+}
+
+function capitalize(s) {
+ return s.charAt(0).toUpperCase() + s.slice(1);
+}
module.exports = {
listAccounts,
getRegister,
getProfitAndLoss,
getBalanceSheet,
+ fetchSplitDetails,
// exposed for testing/debugging
normalizeTransactionListReport
};