Completes Phase 2: Adds reconciliation features

Implements balance assertions, reconciliation API endpoints, a reconciliation UI dashboard, and automated daily balance checks.

This provides comprehensive reconciliation tools to ensure accounting accuracy and catch discrepancies early.

Updates roadmap to mark Phase 2 as complete.
This commit is contained in:
padreug 2025-10-23 02:31:15 +02:00
parent c0277dfc98
commit 6d84479f7d
7 changed files with 963 additions and 6 deletions

View file

@ -271,7 +271,7 @@
<div v-if="failedAssertions.length > 0" class="q-mb-md">
<q-banner class="bg-negative text-white" rounded>
<template v-slot:avatar>
<q-icon name="error" color="white" />
<q-icon name="error" color="white"></q-icon>
</template>
<div class="text-weight-bold">{% raw %}{{ failedAssertions.length }}{% endraw %} Failed Assertion{% raw %}{{ failedAssertions.length > 1 ? 's' : '' }}{% endraw %}</div>
</q-banner>
@ -377,6 +377,127 @@
</q-card-section>
</q-card>
<!-- Reconciliation Dashboard (Super User Only) -->
<q-card v-if="isSuperUser">
<q-card-section>
<div class="row items-center justify-between q-mb-md">
<h6 class="q-my-none">Reconciliation Dashboard</h6>
<q-btn
size="sm"
color="primary"
@click="runFullReconciliation"
:loading="reconciliation.checking"
icon="sync"
label="Check All"
>
<q-tooltip>Re-check all balance assertions</q-tooltip>
</q-btn>
</div>
<!-- Summary Stats -->
<div v-if="reconciliation.summary" class="row q-gutter-md q-mb-md">
<!-- Assertions Stats -->
<q-card flat bordered class="col">
<q-card-section>
<div class="text-caption text-grey">Balance Assertions</div>
<div class="text-h6">{% raw %}{{ reconciliation.summary.assertions.total }}{% endraw %}</div>
<div class="text-caption">
<span class="text-positive">{% raw %}{{ reconciliation.summary.assertions.passed }}{% endraw %} passed</span> |
<span class="text-negative">{% raw %}{{ reconciliation.summary.assertions.failed }}{% endraw %} failed</span> |
<span class="text-grey">{% raw %}{{ reconciliation.summary.assertions.pending }}{% endraw %} pending</span>
</div>
</q-card-section>
</q-card>
<!-- Journal Entries Stats -->
<q-card flat bordered class="col">
<q-card-section>
<div class="text-caption text-grey">Journal Entries</div>
<div class="text-h6">{% raw %}{{ reconciliation.summary.entries.total }}{% endraw %}</div>
<div class="text-caption">
<span class="text-positive">{% raw %}{{ reconciliation.summary.entries.cleared }}{% endraw %} cleared</span> |
<span class="text-orange">{% raw %}{{ reconciliation.summary.entries.pending }}{% endraw %} pending</span> |
<span class="text-warning">{% raw %}{{ reconciliation.summary.entries.flagged }}{% endraw %} flagged</span>
</div>
</q-card-section>
</q-card>
<!-- Accounts Stats -->
<q-card flat bordered class="col">
<q-card-section>
<div class="text-caption text-grey">Total Accounts</div>
<div class="text-h6">{% raw %}{{ reconciliation.summary.accounts.total }}{% endraw %}</div>
<div class="text-caption text-grey">
Last checked: {% raw %}{{ reconciliation.summary.last_checked ? formatDate(reconciliation.summary.last_checked) : 'Never' }}{% endraw %}
</div>
</q-card-section>
</q-card>
</div>
<!-- Discrepancies Alert -->
<q-banner v-if="reconciliation.discrepancies && reconciliation.discrepancies.total_discrepancies > 0" class="bg-warning text-dark q-mb-md" rounded>
<template v-slot:avatar>
<q-icon name="warning" color="orange"></q-icon>
</template>
<div class="text-weight-bold">
{% raw %}{{ reconciliation.discrepancies.total_discrepancies }}{% endraw %} Discrepancy(ies) Found
</div>
<div class="text-caption">
{% raw %}{{ reconciliation.discrepancies.failed_assertions.length }}{% endraw %} failed assertions,
{% raw %}{{ reconciliation.discrepancies.flagged_entries.length }}{% endraw %} flagged entries
</div>
<template v-slot:action>
<q-btn flat label="View Details" @click="reconciliation.showDiscrepancies = !reconciliation.showDiscrepancies"></q-btn>
</template>
</q-banner>
<!-- Discrepancies Details -->
<div v-if="reconciliation.showDiscrepancies && reconciliation.discrepancies">
<!-- Failed Assertions -->
<div v-if="reconciliation.discrepancies.failed_assertions.length > 0" class="q-mb-md">
<div class="text-subtitle2 q-mb-sm">Failed Assertions</div>
<q-list bordered separator dense>
<q-item v-for="assertion in reconciliation.discrepancies.failed_assertions" :key="assertion.id">
<q-item-section avatar>
<q-icon name="error" color="negative" size="sm"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>{% raw %}{{ getAccountName(assertion.account_id) }}{% endraw %}</q-item-label>
<q-item-label caption>
Expected: {% raw %}{{ formatSats(assertion.expected_balance_sats) }}{% endraw %} |
Actual: {% raw %}{{ formatSats(assertion.checked_balance_sats) }}{% endraw %} |
Diff: {% raw %}{{ formatSats(assertion.difference_sats) }}{% endraw %}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<!-- Flagged Entries -->
<div v-if="reconciliation.discrepancies.flagged_entries.length > 0">
<div class="text-subtitle2 q-mb-sm">Flagged Entries</div>
<q-list bordered separator dense>
<q-item v-for="entry in reconciliation.discrepancies.flagged_entries" :key="entry.id">
<q-item-section avatar>
<q-icon name="flag" color="warning" size="sm"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>{% raw %}{{ entry.description }}{% endraw %}</q-item-label>
<q-item-label caption>{% raw %}{{ formatDate(entry.entry_date) }}{% endraw %}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
<!-- No issues message -->
<div v-if="reconciliation.summary && (!reconciliation.discrepancies || reconciliation.discrepancies.total_discrepancies === 0)" class="text-center text-positive q-pa-md">
<q-icon name="check_circle" size="lg" color="positive"></q-icon>
<div class="q-mt-sm">All accounts reconciled successfully!</div>
</div>
</q-card-section>
</q-card>
<!-- Quick Actions -->
<q-card>
<q-card-section>