castle/templates/castle/index.html
padreug 6d5243b03e Filters journal entries by user account
Updates journal entry retrieval to filter entries based on
the user's accounts rather than the user ID.

This ensures that users only see journal entries that
directly affect their accounts.

Also displays fiat amount in journal entries if available in
the metadata.
2025-10-22 17:08:39 +02:00

571 lines
18 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="balance.balance >= 0 ? 'text-negative' : 'text-positive'">
{% 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>
<q-btn
v-if="balance.balance < 0 && !isSuperUser"
color="primary"
class="q-mt-md"
@click="showPayBalanceDialog"
>
Pay Balance
</q-btn>
</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 ID', field: 'user_id', 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 class="text-caption">{% 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>
<!-- 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>
<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.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>
</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>
<!-- 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>
{% endblock %}