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.
571 lines
18 KiB
HTML
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 %}
|