diff --git a/crud.py b/crud.py index d753cc4..847da5c 100644 --- a/crud.py +++ b/crud.py @@ -8,12 +8,14 @@ from lnbits.helpers import urlsafe_short_hash from .models import ( Account, AccountType, + CastleSettings, CreateAccount, CreateEntryLine, CreateJournalEntry, EntryLine, JournalEntry, UserBalance, + UserCastleSettings, ) db = Database("ext_castle") @@ -345,3 +347,33 @@ async def get_account_transactions( transactions.append((entry, line)) return transactions + + +# ===== SETTINGS ===== + + +async def create_castle_settings( + user_id: str, data: CastleSettings +) -> CastleSettings: + settings = UserCastleSettings(**data.dict(), id=user_id) + await db.insert("extension_settings", settings) + return settings + + +async def get_castle_settings(user_id: str) -> Optional[CastleSettings]: + return await db.fetchone( + """ + SELECT * FROM extension_settings + WHERE id = :user_id + """, + {"user_id": user_id}, + CastleSettings, + ) + + +async def update_castle_settings( + user_id: str, data: CastleSettings +) -> CastleSettings: + settings = UserCastleSettings(**data.dict(), id=user_id) + await db.update("extension_settings", settings) + return settings diff --git a/migrations.py b/migrations.py index 4a13dad..0c8b0ba 100644 --- a/migrations.py +++ b/migrations.py @@ -125,3 +125,18 @@ async def m002_add_metadata_column(db): ALTER TABLE entry_lines ADD COLUMN metadata TEXT DEFAULT '{}'; """ ) + + +async def m003_extension_settings(db): + """ + Create extension_settings table for Castle configuration. + """ + await db.execute( + f""" + CREATE TABLE extension_settings ( + id TEXT NOT NULL PRIMARY KEY, + castle_wallet_id TEXT, + updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) diff --git a/models.py b/models.py index 185d72e..e54960a 100644 --- a/models.py +++ b/models.py @@ -102,3 +102,20 @@ class RevenueEntry(BaseModel): payment_method_account: str # e.g., "Cash", "Bank", "Lightning" reference: Optional[str] = None currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code + + +class CastleSettings(BaseModel): + """Settings for the Castle extension""" + + castle_wallet_id: Optional[str] = None # The wallet ID that represents the Castle + updated_at: datetime = Field(default_factory=lambda: datetime.now()) + + @classmethod + def is_admin_only(cls) -> bool: + return True + + +class UserCastleSettings(CastleSettings): + """User-specific settings (stored with user_id)""" + + id: str diff --git a/services.py b/services.py new file mode 100644 index 0000000..ff3e5d6 --- /dev/null +++ b/services.py @@ -0,0 +1,23 @@ +from .crud import ( + create_castle_settings, + get_castle_settings, + update_castle_settings, +) +from .models import CastleSettings + + +async def get_settings(user_id: str) -> CastleSettings: + settings = await get_castle_settings(user_id) + if not settings: + settings = await create_castle_settings(user_id, CastleSettings()) + return settings + + +async def update_settings(user_id: str, data: CastleSettings) -> CastleSettings: + settings = await get_castle_settings(user_id) + if not settings: + settings = await create_castle_settings(user_id, data) + else: + settings = await update_castle_settings(user_id, data) + + return settings diff --git a/static/js/index.js b/static/js/index.js index ade58c9..3ecdabe 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -11,6 +11,8 @@ window.app = Vue.createApp({ transactions: [], accounts: [], currencies: [], + settings: null, + isAdmin: false, expenseDialog: { show: false, description: '', @@ -25,6 +27,11 @@ window.app = Vue.createApp({ show: false, amount: null, loading: false + }, + settingsDialog: { + show: false, + castleWalletId: '', + loading: false } } }, @@ -95,6 +102,47 @@ window.app = Vue.createApp({ LNbits.utils.notifyApiError(error) } }, + async loadSettings() { + try { + const response = await LNbits.api.request( + 'GET', + '/castle/api/v1/settings', + this.g.user.wallets[0].adminkey + ) + this.settings = response.data + this.isAdmin = true + } catch (error) { + // Not admin or settings not available + this.isAdmin = false + } + }, + showSettingsDialog() { + this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || '' + this.settingsDialog.show = true + }, + async submitSettings() { + this.settingsDialog.loading = true + try { + await LNbits.api.request( + 'PUT', + '/castle/api/v1/settings', + this.g.user.wallets[0].adminkey, + { + castle_wallet_id: this.settingsDialog.castleWalletId + } + ) + this.$q.notify({ + type: 'positive', + message: 'Settings updated successfully' + }) + this.settingsDialog.show = false + await this.loadSettings() + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.settingsDialog.loading = false + } + }, async submitExpense() { this.expenseDialog.loading = true try { @@ -188,5 +236,6 @@ window.app = Vue.createApp({ await this.loadTransactions() await this.loadAccounts() await this.loadCurrencies() + await this.loadSettings() } }) diff --git a/templates/castle/index.html b/templates/castle/index.html index d54d21d..4878b80 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -11,8 +11,17 @@
Track expenses, receivables, and balances for the collective
+Track expenses, receivables, and balances for the collective
+