castle/templates/castle/index.html
padreug 0257b7807c PHASE 2: Implements balance assertions for reconciliation
Adds balance assertion functionality to enable admins to verify accounting accuracy.

This includes:
- A new `balance_assertions` table in the database
- CRUD operations for balance assertions (create, get, list, check, delete)
- API endpoints for managing balance assertions (admin only)
- UI elements for creating, viewing, and re-checking assertions

Also, reorders the implementation roadmap in the documentation to reflect better the dependencies between phases.
2025-10-23 02:06:22 +02:00

1000 lines
36 KiB
HTML

{% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %}
{% block scripts %}
{{ window_vars(user) }}
<script src="{{ static_url_for('castle/static', path='js/index.js') }}"></script>
{% endblock %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<h5 class="q-my-none">🏰 Castle Accounting</h5>
<p class="q-mb-none">Track expenses, receivables, and balances for the collective</p>
</div>
<div class="col-auto">
<q-btn v-if="!isSuperUser" flat round icon="account_balance_wallet" @click="showUserWalletDialog">
<q-tooltip>Configure Your Wallet</q-tooltip>
</q-btn>
<q-btn v-if="isSuperUser" flat round icon="settings" @click="showSettingsDialog">
<q-tooltip>Castle Settings (Super User Only)</q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
</q-card>
<!-- Setup Warning -->
<q-banner v-if="!castleWalletConfigured && isSuperUser" class="bg-warning text-white" rounded>
<template v-slot:avatar>
<q-icon name="warning" color="white"></q-icon>
</template>
<div>
<strong>Setup Required:</strong> Castle Wallet ID must be configured before the extension can function.
</div>
<template v-slot:action>
<q-btn flat color="white" label="Configure Now" @click="showSettingsDialog"></q-btn>
</template>
</q-banner>
<q-banner v-if="!castleWalletConfigured && !isSuperUser" class="bg-info text-white" rounded>
<template v-slot:avatar>
<q-icon name="info" color="white"></q-icon>
</template>
<div>
<strong>Setup Required:</strong> This extension requires configuration by the super user before it can be used.
</div>
</q-banner>
<q-banner v-if="castleWalletConfigured && !userWalletConfigured && !isSuperUser" class="bg-orange text-white" rounded>
<template v-slot:avatar>
<q-icon name="account_balance_wallet" color="white"></q-icon>
</template>
<div>
<strong>Wallet Setup Required:</strong> You must configure your wallet before using this extension.
</div>
<template v-slot:action>
<q-btn flat color="white" label="Configure Wallet" @click="showUserWalletDialog"></q-btn>
</template>
</q-banner>
<!-- User Balance Card -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col">
<h6 class="q-my-none">Your Balance</h6>
</div>
<div class="col-auto">
<q-btn flat round icon="refresh" @click="loadBalance">
<q-tooltip>Refresh balance</q-tooltip>
</q-btn>
</div>
</div>
<div v-if="balance !== null">
<div class="text-h4" :class="isSuperUser ? (balance.balance >= 0 ? 'text-negative' : 'text-positive') : (balance.balance >= 0 ? 'text-positive' : 'text-negative')">
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
</div>
<div v-if="balance.fiat_balances && Object.keys(balance.fiat_balances).length > 0" class="text-h6 q-mt-sm">
<span v-for="(amount, currency) in balance.fiat_balances" :key="currency" class="q-mr-md">
{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}
</span>
</div>
<div class="text-subtitle2" v-if="isSuperUser">
{% raw %}{{ balance.balance > 0 ? 'Total you owe' : balance.balance < 0 ? 'Total owed to you' : 'No outstanding balances' }}{% endraw %}
</div>
<div class="text-subtitle2" v-else>
{% raw %}{{ balance.balance >= 0 ? 'Castle owes you' : 'You owe Castle' }}{% endraw %}
</div>
<div class="q-mt-md q-gutter-sm">
<q-btn
v-if="balance.balance < 0 && !isSuperUser"
color="primary"
@click="showPayBalanceDialog"
>
Pay Balance
</q-btn>
<q-btn
v-if="balance.balance > 0 && !isSuperUser"
color="secondary"
@click="showManualPaymentDialog"
>
Request Manual Payment
</q-btn>
</div>
</div>
<div v-else>
<q-spinner color="primary" size="md"></q-spinner>
Loading balance...
</div>
</q-card-section>
</q-card>
<!-- User Balances Breakdown (Super User Only) -->
<q-card v-if="isSuperUser && allUserBalances.length > 0">
<q-card-section>
<h6 class="q-my-none q-mb-md">Outstanding Balances by User</h6>
<q-table
flat
:rows="allUserBalances"
:columns="[
{name: 'user', label: 'User', field: 'username', align: 'left'},
{name: 'balance', label: 'Amount Owed', field: 'balance', align: 'right'}
]"
row-key="user_id"
hide-pagination
:rows-per-page-options="[0]"
>
<template v-slot:body-cell-user="props">
<q-td :props="props">
<div>{% raw %}{{ props.row.username }}{% endraw %}</div>
<div class="text-caption text-grey">{% raw %}{{ props.row.user_id.substring(0, 16) }}...{% endraw %}</div>
</q-td>
</template>
<template v-slot:body-cell-balance="props">
<q-td :props="props">
<div :class="props.row.balance > 0 ? 'text-negative' : 'text-positive'">
{% raw %}{{ formatSats(Math.abs(props.row.balance)) }} sats{% endraw %}
</div>
<div v-if="props.row.fiat_balances && Object.keys(props.row.fiat_balances).length > 0" class="text-caption">
<span v-for="(amount, currency) in props.row.fiat_balances" :key="currency" class="q-mr-sm">
{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}
</span>
</div>
<div class="text-caption text-grey">
{% raw %}{{ props.row.balance > 0 ? 'You owe' : 'Owes you' }}{% endraw %}
</div>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
<!-- Pending Expense Entries (Super User Only) -->
<q-card v-if="isSuperUser && pendingExpenses.length > 0">
<q-card-section>
<h6 class="q-my-none q-mb-md">Pending Expense Approvals</h6>
<q-list separator>
<q-item v-for="entry in pendingExpenses" :key="entry.id">
<q-item-section avatar>
<q-icon name="pending" color="orange" size="sm">
<q-tooltip>Pending approval</q-tooltip>
</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-label caption v-if="entry.meta && entry.meta.user_id">
User: {% raw %}{{ entry.meta.user_id.substring(0, 16) }}...{% endraw %}
</q-item-label>
<q-item-label caption v-if="entry.reference" class="text-grey">
Ref: {% raw %}{{ entry.reference }}{% endraw %}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>{% raw %}{{ formatSats(getTotalAmount(entry)) }} sats{% endraw %}</q-item-label>
<q-item-label caption v-if="getEntryFiatAmount(entry)">
{% raw %}{{ getEntryFiatAmount(entry) }}{% endraw %}
</q-item-label>
</q-item-section>
<q-item-section side>
<div class="q-gutter-xs">
<q-btn
size="sm"
color="positive"
@click="approveExpense(entry.id)"
:loading="entry.approving"
>
Approve
</q-btn>
<q-btn
size="sm"
color="negative"
@click="rejectExpense(entry.id)"
:loading="entry.rejecting"
>
Reject
</q-btn>
</div>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</q-card>
<!-- Pending Manual Payment Requests (Super User Only) -->
<q-card v-if="isSuperUser && pendingManualPaymentRequests.length > 0">
<q-card-section>
<h6 class="q-my-none q-mb-md">Pending Manual Payment Requests</h6>
<q-list separator>
<q-item v-for="request in pendingManualPaymentRequests" :key="request.id">
<q-item-section>
<q-item-label>{% raw %}{{ request.description }}{% endraw %}</q-item-label>
<q-item-label caption>
User: {% raw %}{{ request.user_id.substring(0, 16) }}...{% endraw %}
</q-item-label>
<q-item-label caption>
Requested: {% raw %}{{ formatDate(request.created_at) }}{% endraw %}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>{% raw %}{{ formatSats(request.amount) }} sats{% endraw %}</q-item-label>
</q-item-section>
<q-item-section side>
<div class="q-gutter-xs">
<q-btn
size="sm"
color="positive"
@click="approveManualPaymentRequest(request.id)"
:loading="request.approving"
>
Approve
</q-btn>
<q-btn
size="sm"
color="negative"
@click="rejectManualPaymentRequest(request.id)"
:loading="request.rejecting"
>
Reject
</q-btn>
</div>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</q-card>
<!-- Balance Assertions (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">Balance Assertions</h6>
<q-btn
size="sm"
color="primary"
@click="assertionDialog.show = true"
icon="add"
label="Create Assertion"
>
<q-tooltip>Create a new balance assertion for reconciliation</q-tooltip>
</q-btn>
</div>
<!-- Failed Assertions -->
<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" />
</template>
<div class="text-weight-bold">{% raw %}{{ failedAssertions.length }}{% endraw %} Failed Assertion{% raw %}{{ failedAssertions.length > 1 ? 's' : '' }}{% endraw %}</div>
</q-banner>
<q-list bordered separator class="q-mt-sm">
<q-item v-for="assertion in failedAssertions" :key="assertion.id">
<q-item-section avatar>
<q-icon name="error" color="negative" size="sm">
<q-tooltip>Assertion failed</q-tooltip>
</q-icon>
</q-item-section>
<q-item-section>
<q-item-label>Account: {% raw %}{{ getAccountName(assertion.account_id) }}{% endraw %}</q-item-label>
<q-item-label caption>Expected: {% raw %}{{ formatSats(assertion.expected_balance_sats) }}{% endraw %} sats</q-item-label>
<q-item-label caption>Actual: {% raw %}{{ formatSats(assertion.checked_balance_sats) }}{% endraw %} sats</q-item-label>
<q-item-label caption class="text-negative">Difference: {% raw %}{{ formatSats(assertion.difference_sats) }}{% endraw %} sats</q-item-label>
<q-item-label caption v-if="assertion.fiat_currency">
Expected: {% raw %}{{ assertion.expected_balance_fiat }} {{ assertion.fiat_currency }}{% endraw %} |
Actual: {% raw %}{{ assertion.checked_balance_fiat }} {{ assertion.fiat_currency }}{% endraw %} |
Difference: {% raw %}{{ assertion.difference_fiat }} {{ assertion.fiat_currency }}{% endraw %}
</q-item-label>
<q-item-label caption>Checked: {% raw %}{{ formatDate(assertion.checked_at) }}{% endraw %}</q-item-label>
</q-item-section>
<q-item-section side>
<div class="q-gutter-xs">
<q-btn
size="sm"
flat
round
icon="refresh"
@click="recheckAssertion(assertion.id)"
:loading="assertion.rechecking"
>
<q-tooltip>Re-check assertion</q-tooltip>
</q-btn>
<q-btn
size="sm"
flat
round
icon="delete"
@click="deleteAssertion(assertion.id)"
:loading="assertion.deleting"
>
<q-tooltip>Delete assertion</q-tooltip>
</q-btn>
</div>
</q-item-section>
</q-item>
</q-list>
</div>
<!-- Passed Assertions -->
<div v-if="passedAssertions.length > 0">
<q-expansion-item
label="Passed Assertions"
:caption="`${passedAssertions.length} assertion${passedAssertions.length > 1 ? 's' : ''} passed`"
icon="check_circle"
header-class="text-positive"
>
<q-list bordered separator>
<q-item v-for="assertion in passedAssertions" :key="assertion.id">
<q-item-section avatar>
<q-icon name="check_circle" color="positive" size="sm">
<q-tooltip>Assertion passed</q-tooltip>
</q-icon>
</q-item-section>
<q-item-section>
<q-item-label>{% raw %}{{ getAccountName(assertion.account_id) }}{% endraw %}</q-item-label>
<q-item-label caption>Balance: {% raw %}{{ formatSats(assertion.checked_balance_sats) }}{% endraw %} sats</q-item-label>
<q-item-label caption v-if="assertion.fiat_currency">
Fiat: {% raw %}{{ assertion.checked_balance_fiat }} {{ assertion.fiat_currency }}{% endraw %}
</q-item-label>
<q-item-label caption>Checked: {% raw %}{{ formatDate(assertion.checked_at) }}{% endraw %}</q-item-label>
</q-item-section>
<q-item-section side>
<div class="q-gutter-xs">
<q-btn
size="sm"
flat
round
icon="refresh"
@click="recheckAssertion(assertion.id)"
:loading="assertion.rechecking"
>
<q-tooltip>Re-check assertion</q-tooltip>
</q-btn>
<q-btn
size="sm"
flat
round
icon="delete"
@click="deleteAssertion(assertion.id)"
:loading="assertion.deleting"
>
<q-tooltip>Delete assertion</q-tooltip>
</q-btn>
</div>
</q-item-section>
</q-item>
</q-list>
</q-expansion-item>
</div>
<!-- No assertions message -->
<div v-if="balanceAssertions.length === 0" class="text-center text-grey q-pa-md">
No balance assertions yet. Create one to verify your accounting accuracy.
</div>
</q-card-section>
</q-card>
<!-- Quick Actions -->
<q-card>
<q-card-section>
<h6 class="q-my-none q-mb-md">Quick Actions</h6>
<div class="row q-gutter-sm">
<q-btn
color="primary"
@click="expenseDialog.show = true"
:disable="!castleWalletConfigured || (!userWalletConfigured && !isSuperUser)"
>
Add Expense
<q-tooltip v-if="!castleWalletConfigured">
Castle wallet must be configured first
</q-tooltip>
<q-tooltip v-if="castleWalletConfigured && !userWalletConfigured && !isSuperUser">
You must configure your wallet first
</q-tooltip>
</q-btn>
<q-btn
v-if="isSuperUser"
color="orange"
@click="showReceivableDialog"
:disable="!castleWalletConfigured"
>
Add Receivable
<q-tooltip v-if="!castleWalletConfigured">
Castle wallet must be configured first
</q-tooltip>
<q-tooltip v-else>
Record when a user owes the Castle
</q-tooltip>
</q-btn>
<q-btn color="secondary" @click="loadTransactions">
View Transactions
</q-btn>
</div>
</q-card-section>
</q-card>
<!-- Recent Transactions -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col">
<h6 class="q-my-none">Recent Transactions</h6>
</div>
<div class="col-auto">
<q-btn flat round icon="refresh" @click="loadTransactions">
<q-tooltip>Refresh transactions</q-tooltip>
</q-btn>
</div>
</div>
<q-list v-if="transactions.length > 0" separator>
<q-item v-for="entry in transactions" :key="entry.id">
<q-item-section avatar>
<!-- Transaction status flag -->
<q-icon v-if="entry.flag === '*'" name="check_circle" color="positive" size="sm">
<q-tooltip>Cleared</q-tooltip>
</q-icon>
<q-icon v-else-if="entry.flag === '!'" name="pending" color="orange" size="sm">
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-icon v-else-if="entry.flag === '#'" name="flag" color="red" size="sm">
<q-tooltip>Flagged - needs review</q-tooltip>
</q-icon>
<q-icon v-else-if="entry.flag === 'x'" name="cancel" color="grey" size="sm">
<q-tooltip>Voided</q-tooltip>
</q-icon>
</q-item-section>
<q-item-section>
<q-item-label>
{% raw %}{{ entry.description }}{% endraw %}
<!-- Castle's perspective: Receivables are incoming (green), Payables are outgoing (red) -->
<q-badge v-if="isSuperUser && isReceivable(entry)" color="positive" class="q-ml-sm">
Receivable
</q-badge>
<q-badge v-else-if="isSuperUser && isPayable(entry)" color="negative" class="q-ml-sm">
Payable
</q-badge>
<!-- User's perspective: Receivables are outgoing (red), Payables are incoming (green) -->
<q-badge v-else-if="!isSuperUser && isReceivable(entry)" color="negative" class="q-ml-sm">
Payable
</q-badge>
<q-badge v-else-if="!isSuperUser && isPayable(entry)" color="positive" class="q-ml-sm">
Receivable
</q-badge>
</q-item-label>
<q-item-label caption>
{% raw %}{{ formatDate(entry.entry_date) }}{% endraw %}
</q-item-label>
<q-item-label caption v-if="entry.reference" class="text-grey">
Ref: {% raw %}{{ entry.reference }}{% endraw %}
</q-item-label>
<q-item-label caption v-if="entry.meta && Object.keys(entry.meta).length > 0" class="text-blue-grey-6">
<q-icon name="info" size="xs" class="q-mr-xs"></q-icon>
<span v-if="entry.meta.source">Source: {% raw %}{{ entry.meta.source }}{% endraw %}</span>
<span v-if="entry.meta.created_via" class="q-ml-sm">Via: {% raw %}{{ entry.meta.created_via }}{% endraw %}</span>
</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>{% raw %}{{ formatSats(getTotalAmount(entry)) }} sats{% endraw %}</q-item-label>
<q-item-label caption v-if="getEntryFiatAmount(entry)">
{% raw %}{{ getEntryFiatAmount(entry) }}{% endraw %}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
<div v-else class="text-center q-pa-md text-grey">
No transactions yet
</div>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<!-- Chart of Accounts -->
<q-card>
<q-card-section>
<h6 class="q-my-none q-mb-md">Chart of Accounts</h6>
<q-list dense v-if="accounts.length > 0">
<q-item v-for="account in accounts" :key="account.id">
<q-item-section>
<q-item-label>{% raw %}{{ account.name }}{% endraw %}</q-item-label>
<q-item-label caption>{% raw %}{{ account.account_type }}{% endraw %}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<div v-else>
<q-spinner color="primary" size="sm"></q-spinner>
Loading accounts...
</div>
</q-card-section>
</q-card>
</div>
</div>
<!-- Add Expense Dialog -->
<q-dialog v-model="expenseDialog.show" position="top">
<q-card v-if="expenseDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="submitExpense" class="q-gutter-md">
<div class="text-h6 q-mb-md">Add Expense</div>
<q-input
filled
dense
v-model.trim="expenseDialog.description"
label="Description *"
placeholder="e.g., Groceries for the house"
></q-input>
<q-select
filled
dense
v-model="expenseDialog.currency"
:options="currencyOptions"
option-label="label"
option-value="value"
emit-value
map-options
label="Currency"
></q-select>
<q-input
filled
dense
v-model.number="expenseDialog.amount"
type="number"
:label="amountLabel"
min="0.01"
step="0.01"
></q-input>
<q-select
filled
dense
v-model="expenseDialog.expenseAccount"
:options="expenseAccounts"
option-label="name"
option-value="id"
emit-value
map-options
label="Expense Category *"
></q-select>
<q-select
filled
dense
v-model="expenseDialog.isEquity"
:options="[
{label: 'Liability (Castle owes me)', value: false},
{label: 'Equity (My contribution)', value: true}
]"
option-label="label"
option-value="value"
emit-value
map-options
label="Type *"
></q-select>
<q-input
filled
dense
v-model.trim="expenseDialog.reference"
label="Reference (optional)"
placeholder="e.g., Receipt #123"
></q-input>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit" :loading="expenseDialog.loading">
Submit Expense
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<!-- Pay Balance Dialog -->
<q-dialog v-model="payDialog.show" position="top">
<q-card v-if="payDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card" style="min-width: 400px">
<div class="text-h6 q-mb-md">Pay Balance</div>
<div v-if="balance" class="q-mb-md">
<div>
Amount owed: <strong>{% raw %}{{ formatSats(Math.abs(balance.balance)) }}{% endraw %} sats</strong>
</div>
<div v-if="balance.fiat_balances && Object.keys(balance.fiat_balances).length > 0" class="text-body2 q-mt-xs">
<span v-for="(amount, currency) in balance.fiat_balances" :key="currency" class="q-mr-md">
<strong>{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}</strong>
</span>
</div>
</div>
<div v-if="!payDialog.paymentRequest">
<q-form @submit="submitPayment" class="q-gutter-md">
<q-input
filled
dense
v-model.number="payDialog.amount"
type="number"
label="Amount to pay (sats) *"
min="1"
:max="balance ? Math.abs(balance.balance) : 0"
></q-input>
<div class="row q-mt-lg q-gutter-sm">
<q-btn unelevated color="primary" type="submit" :loading="payDialog.loading">
Generate Lightning Invoice
</q-btn>
<q-btn unelevated color="orange" @click="showManualPaymentOption" :loading="payDialog.loading">
Pay Manually (Cash/Bank)
</q-btn>
</div>
<div class="row q-mt-sm">
<q-btn v-close-popup flat color="grey">Cancel</q-btn>
</div>
</q-form>
</div>
<div v-else>
<div class="q-mb-md text-center">
<lnbits-qrcode :value="payDialog.paymentRequest" :options="{width: 280}"></lnbits-qrcode>
</div>
<div class="q-mb-md">
<q-input
filled
dense
readonly
v-model="payDialog.paymentRequest"
label="Lightning Invoice"
>
<template v-slot:append>
<q-btn
flat
dense
icon="content_copy"
@click="copyToClipboard(payDialog.paymentRequest)"
>
<q-tooltip>Copy invoice</q-tooltip>
</q-btn>
</template>
</q-input>
</div>
<div class="text-caption text-grey q-mb-md">
Scan the QR code or copy the invoice to pay with your Lightning wallet.
Your balance will update automatically after payment.
</div>
<div class="row">
<q-btn v-close-popup flat color="grey">Close</q-btn>
</div>
</div>
</q-card>
</q-dialog>
<!-- Manual Payment Request Dialog -->
<q-dialog v-model="manualPaymentDialog.show" position="top">
<q-card v-if="manualPaymentDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card" style="min-width: 400px">
<q-form @submit="submitManualPaymentRequest" class="q-gutter-md">
<div class="text-h6 q-mb-md">Request Manual Payment</div>
<div class="text-caption text-grey q-mb-md">
Request the Castle to pay you manually (cash, bank transfer, etc.) to settle your balance.
</div>
<div v-if="balance" class="q-mb-md">
<div>
Current balance: <strong>{% raw %}{{ formatSats(Math.abs(balance.balance)) }}{% endraw %} sats</strong>
</div>
<div v-if="balance.fiat_balances && Object.keys(balance.fiat_balances).length > 0" class="text-body2 q-mt-xs">
<span v-for="(amount, currency) in balance.fiat_balances" :key="currency" class="q-mr-md">
<strong>{% raw %}{{ formatFiat(Math.abs(amount), currency) }}{% endraw %}</strong>
</span>
</div>
</div>
<q-input
filled
dense
v-model.number="manualPaymentDialog.amount"
type="number"
label="Amount to request (sats) *"
min="1"
:max="balance ? Math.abs(balance.balance) : 0"
:rules="[val => !!val || 'Amount is required', val => val > 0 || 'Amount must be positive']"
></q-input>
<q-input
filled
dense
v-model="manualPaymentDialog.description"
type="text"
label="Description *"
:rules="[val => !!val || 'Description is required']"
></q-input>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit" :loading="manualPaymentDialog.loading">
Submit Request
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-sm">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<!-- Settings Dialog -->
<q-dialog v-model="settingsDialog.show" position="top">
<q-card v-if="settingsDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="submitSettings" class="q-gutter-md">
<div class="text-h6 q-mb-md">Castle Settings</div>
<q-banner v-if="!isSuperUser" class="bg-warning text-dark q-mb-md" dense rounded>
<template v-slot:avatar>
<q-icon name="lock" color="orange"></q-icon>
</template>
<div class="text-caption">
<strong>Super User Only:</strong> Only the super user can modify these settings.
</div>
</q-banner>
<q-select
filled
dense
emit-value
v-model="settingsDialog.castleWalletId"
:options="g.user.walletOptions"
label="Castle Wallet *"
:readonly="!isSuperUser"
:disable="!isSuperUser"
></q-select>
<div class="text-caption text-grey">
Select the wallet that will be used for Castle operations and transactions.
</div>
<div class="row q-mt-lg">
<q-btn
v-if="isSuperUser"
unelevated
color="primary"
type="submit"
:loading="settingsDialog.loading"
:disable="!settingsDialog.castleWalletId"
>
Save Settings
</q-btn>
<q-btn v-close-popup flat color="grey" :class="isSuperUser ? 'q-ml-auto' : ''">
{% raw %}{{ isSuperUser ? 'Cancel' : 'Close' }}{% endraw %}
</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<!-- User Wallet Dialog -->
<q-dialog v-model="userWalletDialog.show" position="top">
<q-card v-if="userWalletDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="submitUserWallet" class="q-gutter-md">
<div class="text-h6 q-mb-md">Configure Your Wallet</div>
<q-select
filled
dense
emit-value
v-model="userWalletDialog.userWalletId"
:options="g.user.walletOptions"
label="Your Wallet *"
></q-select>
<div class="text-caption text-grey">
Select the wallet you'll use for Castle transactions.
</div>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:loading="userWalletDialog.loading"
:disable="!userWalletDialog.userWalletId"
>
Save Wallet
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<!-- Receivable Dialog -->
<q-dialog v-model="receivableDialog.show" position="top">
<q-card v-if="receivableDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="submitReceivable" class="q-gutter-md">
<div class="text-h6 q-mb-md">Add Receivable</div>
<q-select
filled
dense
v-model="receivableDialog.selectedUser"
:options="userOptions"
option-label="label"
option-value="value"
emit-value
map-options
label="User *"
></q-select>
<q-input
filled
dense
v-model.trim="receivableDialog.description"
label="Description *"
placeholder="e.g., Room rental for 5 days"
></q-input>
<q-select
filled
dense
v-model="receivableDialog.currency"
:options="currencyOptions"
option-label="label"
option-value="value"
emit-value
map-options
label="Currency"
></q-select>
<q-input
filled
dense
v-model.number="receivableDialog.amount"
type="number"
:label="receivableAmountLabel"
min="0.01"
step="0.01"
></q-input>
<q-select
filled
dense
v-model="receivableDialog.revenueAccount"
:options="revenueAccounts"
option-label="name"
option-value="id"
emit-value
map-options
label="Revenue Category *"
></q-select>
<q-input
filled
dense
v-model.trim="receivableDialog.reference"
label="Reference (optional)"
placeholder="e.g., Invoice #456"
></q-input>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit" :loading="receivableDialog.loading">
Submit Receivable
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<!-- Balance Assertion Dialog -->
<q-dialog v-model="assertionDialog.show" position="top">
<q-card v-if="assertionDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="submitAssertion" class="q-gutter-md">
<div class="text-h6 q-mb-md">Create Balance Assertion</div>
<div class="text-caption text-grey q-mb-md">
Balance assertions help you verify accounting accuracy by checking if an account's actual balance matches your expected balance. If the assertion fails, you'll be alerted to investigate the discrepancy.
</div>
<q-select
filled
dense
v-model="assertionDialog.account_id"
:options="allAccounts"
option-label="name"
option-value="id"
emit-value
map-options
label="Account *"
:rules="[val => !!val || 'Account is required']"
>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>{% raw %}{{ scope.opt.name }}{% endraw %}</q-item-label>
<q-item-label caption>{% raw %}{{ scope.opt.account_type }}{% endraw %}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
filled
dense
v-model.number="assertionDialog.expected_balance_sats"
type="number"
label="Expected Balance (sats) *"
:rules="[val => val !== null && val !== undefined || 'Expected balance is required']"
>
<template v-slot:hint>
The balance you expect this account to have in satoshis
</template>
</q-input>
<q-input
filled
dense
v-model.number="assertionDialog.tolerance_sats"
type="number"
label="Tolerance (sats)"
min="0"
>
<template v-slot:hint>
Allow the actual balance to differ by ± this amount (default: 0)
</template>
</q-input>
<q-separator />
<div class="text-subtitle2">Optional: Fiat Balance Check</div>
<q-select
filled
dense
v-model="assertionDialog.fiat_currency"
:options="currencyOptions"
option-label="label"
option-value="value"
emit-value
map-options
label="Fiat Currency (optional)"
clearable
></q-select>
<q-input
v-if="assertionDialog.fiat_currency"
filled
dense
v-model.number="assertionDialog.expected_balance_fiat"
type="number"
step="0.01"
:label="`Expected Fiat Balance (${assertionDialog.fiat_currency})`"
></q-input>
<q-input
v-if="assertionDialog.fiat_currency"
filled
dense
v-model.number="assertionDialog.tolerance_fiat"
type="number"
step="0.01"
label="Fiat Tolerance"
min="0"
></q-input>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit" :loading="assertionDialog.loading">
Create & Check
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
{% endblock %}