diff --git a/assets/images/onboarding/importer.svg b/assets/images/onboarding/importer.svg
new file mode 100644
index 00000000..5af8c205
--- /dev/null
+++ b/assets/images/onboarding/importer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/onboarding/insights.svg b/assets/images/onboarding/insights.svg
new file mode 100644
index 00000000..5008ff5c
--- /dev/null
+++ b/assets/images/onboarding/insights.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/onboarding/ready.svg b/assets/images/onboarding/ready.svg
new file mode 100644
index 00000000..5008ff5c
--- /dev/null
+++ b/assets/images/onboarding/ready.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/onboarding/wallet.svg b/assets/images/onboarding/wallet.svg
new file mode 100644
index 00000000..b838023b
--- /dev/null
+++ b/assets/images/onboarding/wallet.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/sparkles.svg b/assets/images/sparkles.svg
new file mode 100644
index 00000000..a5e85012
--- /dev/null
+++ b/assets/images/sparkles.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/assets/images/trakli-logo-white.png b/assets/images/trakli-logo-white.png
new file mode 100644
index 00000000..c0f60908
Binary files /dev/null and b/assets/images/trakli-logo-white.png differ
diff --git a/assets/translations/de.json b/assets/translations/de.json
index 4fbfe871..2098563c 100644
--- a/assets/translations/de.json
+++ b/assets/translations/de.json
@@ -92,6 +92,18 @@
"groupCreateGroup": "Gruppe erstellen",
"statistics": "Statistiken",
"profile": "Profil",
+ "ai": "KI",
+ "aiChatTitle": "Trakli fragen",
+ "aiChatEmptyHint": "Frag alles über deine Finanzen",
+ "aiChatComposerPlaceholder": "Stelle eine Frage…",
+ "aiChatNewChat": "Neuer Chat",
+ "aiChatThinking": "Denkt nach…",
+ "aiChatError": "Etwas ist schiefgelaufen. Versuche es erneut.",
+ "aiChatHistory": "Chatverlauf",
+ "aiChatUntitled": "Chat ohne Titel",
+ "aiChatNoSessions": "Noch keine Konversationen",
+ "aiChatNoSessionsHint": "Starte einen neuen Chat, um ihn hier zu sehen.",
+ "aiChatDeleteConfirm": "Diese Konversation löschen? Dies kann nicht rückgängig gemacht werden.",
"settings": "Einstellungen",
"typeHere": "Hier tippen",
"balanceAmountWithCurrency": "{}",
@@ -190,6 +202,7 @@
"about": "Über",
"switchDefaultGroup": "Standardgruppe wechseln",
"walletTransfer": "Wallet-Überweisung",
+ "transfer": "Transfer",
"walletTransferDefaultDescription": "Geldtransfer von {fromCurrency}-Wallet zu {toCurrency}-Wallet",
"sourceWallet": "Quell-Wallet",
"orangeMoney": "Orange money",
@@ -569,5 +582,163 @@
"groupsDesc": "Zusammengehörige Transaktionen bündeln",
"partiesDesc": "Personen und Organisationen, mit denen Sie Transaktionen durchführen",
"walletsDesc": "Konten, über die sich Geld bewegt",
- "cancelled": "Abgebrochen"
+ "cancelled": "Abgebrochen",
+ "budget": "Budget",
+ "budgets": "Budgets",
+ "addBudget": "Budget hinzufügen",
+ "createBudget": "Budget erstellen",
+ "editBudget": "Budget bearbeiten",
+ "deleteBudget": "Budget löschen",
+ "deleteBudgetConfirm": "Dies entfernt \"{name}\" aus Ihren Budgets. Diese Aktion kann nicht rückgängig gemacht werden.",
+ "deleteBudgetSuccess": "Budget gelöscht",
+ "deleteBudgetError": "Budget konnte nicht gelöscht werden",
+ "closeBudgetPeriod": "Zeitraum schließen",
+ "closePeriod": "Aktuellen Zeitraum schließen",
+ "closePeriodConfirm": "Zeitraum schließen",
+ "closePeriodSuccess": "Zeitraum geschlossen",
+ "closePeriodError": "Zeitraum konnte nicht geschlossen werden",
+ "budgetAmount": "Budgetbetrag",
+ "budgetPeriod": "Budgetzeitraum",
+ "budgetTargets": "Ziele",
+ "budgetPeriodHistory": "Zeitraumverlauf",
+ "budgetStatus": "Status",
+ "budgetRemaining": "Verbleibend",
+ "budgetProjected": "Prognostiziert",
+ "budgetRefunds": "Rückerstattungen",
+ "budgetRollover": "Übertrag",
+ "budgetRolloverIn": "Übertrag auf",
+ "importAiSuggested": "KI schlägt vor: {}",
+ "importAmount": "Betrag",
+ "importAnalysisFailed": "Analyse fehlgeschlagen",
+ "importAnalysisFailedHint": "Bei der Verarbeitung des Dokuments ist ein Fehler aufgetreten. Versuchen Sie, es erneut hochzuladen.",
+ "importAnalyze": "Analysieren",
+ "importAnalyzing": "Dokument wird analysiert…",
+ "importAutoCreateCategories": "Fehlende Kategorien automatisch erstellen",
+ "importAutoCreateMissingHint": "{} Element(e) ohne Auswahl — werden aus dem KI-Vorschlag erstellt.",
+ "importAutoCreateNoneNeeded": "Alle akzeptierten Elemente haben bereits eine Auswahl.",
+ "importAutoCreateParties": "Fehlende Parteien automatisch erstellen",
+ "importAutoCreateWallets": "Fehlende Geldbörsen automatisch erstellen",
+ "importCategory": "Kategorie",
+ "importCheckingDuplicates": "Prüfung auf Duplikate…",
+ "importConfirm": "Import bestätigen",
+ "importConfirmSheetTitle": "{} von {} Vorschläge bestätigen?",
+ "importCreateTransactions": "Transaktionen erstellen",
+ "importCreatedCount": "{} Transaktion(en) importiert.",
+ "importDate": "Datum",
+ "importDescription": "Beschreibung",
+ "importDocBankStatement": "Kontoauszug",
+ "importDocInvoice": "Rechnung",
+ "importDocPayStub": "Gehaltsabrechnung",
+ "importDocReceipt": "Quittung",
+ "importDocTypeLabel": "Dokumenttyp",
+ "importDocUtilityBill": "Nebenkosten",
+ "importEnriching": "Extrahierte Daten werden bereinigt…",
+ "importExtracting": "Dokument wird gelesen…",
+ "importNoAcceptedSuggestions": "Wählen Sie mindestens einen Vorschlag zur Bestätigung.",
+ "importNoActivity": "Noch keine Importe. Beginnen Sie mit einer der obigen Aktionen.",
+ "importNoSuggestions": "Keine Vorschläge aus diesem Dokument extrahiert.",
+ "importNoSuggestionsClose": "Schließen",
+ "importNoSuggestionsTitle": "Keine Vorschläge",
+ "importNoTransactionsFound": "Keine Transaktionen gefunden",
+ "importParty": "Partei",
+ "importRecentActivity": "Aktuelle Aktivität",
+ "importReviewSuggestions": "Vorschläge überprüfen",
+ "importScanDocument": "Dokument scannen",
+ "importScanDocumentDesc": "KI-gestützte Extraktion aus PDFs, Bildern und CSVs",
+ "importSourceCamera": "Kamera",
+ "importSourceCameraDesc": "Schnelles Scannen",
+ "importSourceFile": "Datei",
+ "importSourceFileDesc": "PDF, Bild oder CSV",
+ "importSourceGallery": "Galerie",
+ "importSourceGalleryDesc": "Fotobibliothek",
+ "importSourceLabel": "Quelle",
+ "importType": "Typ",
+ "importWallet": "Geldbörse",
+ "imports": "Importe",
+ "importsDesc": "Transaktionen aus Dateien oder Dokumenten importieren",
+ "budgetPeriodOpen": "Offen",
+ "closePeriodMessage": "Dies sperrt die Ausgaben des aktuellen Zeitraums und startet einen neuen. Der ungenutzte Betrag wird in den nächsten Zeitraum übertragen.",
+ "closePeriodTitle": "Diesen Zeitraum schließen?",
+ "budgetStatusOverBudget": "BUDGET ÜBERSCHRITTEN",
+ "budgetStatusForecastBreach": "PROGNOSE-VERSTOESS",
+ "budgetStatusNearLimit": "NÄHE DER GRENZE",
+ "budgetStatusOnTrack": "IM PLAN",
+ "budgetStatusAwaitingSync": "SYNCHRONISIERUNG AUSSTEHEND",
+ "budgetPeriodWeekly": "Wöchentlich · ab",
+ "budgetPeriodMonthly": "Monatlich · ab",
+ "budgetPeriodYearly": "Jährlich · ab",
+ "budgetKpiRemaining": "Verbleibend",
+ "budgetKpiProjected": "Prognostiziert",
+ "budgetKpiRefunds": "Rückerstattungen",
+ "budgetKpiRolloverIn": "Übertrag auf",
+ "budgetTargetCategory": "Kategorie",
+ "budgetTargetWallet": "Geldbörse",
+ "budgetTargetGroup": "Gruppe",
+ "budgetTargetsLabel": "Ziele",
+ "budgetPeriodHistoryLabel": "Zeitraumverlauf",
+ "budgetTargetsApplyAll": "Gilt für alle Transaktionen in diesem Zeitraum.",
+ "budgetNoPeriods": "Noch keine geschlossenen Zeiträume.",
+ "budgetNoProgress": "Der Fortschritt wird verfügbar sein, sobald dieses Budget mit dem Server synchronisiert ist.",
+ "budgetTargetUnnamed": "(unbenannt)",
+ "newBudget": "New budget",
+ "period": "Period",
+ "startDate": "Start date",
+ "endDate": "End date",
+ "target": "target",
+ "targets": "targets",
+ "selected": "selected",
+ "budgetRolloverDescription": "Carry leftover budget into next period",
+ "budgetForecastAlerts": "Forecast alerts",
+ "budgetForecastAlertsDescription": "Warn me if I'm projected to overspend",
+ "active": "Active",
+ "saveChanges": "Save changes",
+ "budgetNameRequired": "Please enter a name",
+ "budgetAmountRequired": "Please enter a valid amount",
+ "budgetCurrencyRequired": "Currency must be 3 letters",
+ "nameHint": "e.g. Groceries",
+ "amountHint": "0.00",
+ "periodWeekly": "Weekly",
+ "periodMonthly": "Monthly",
+ "periodYearly": "Yearly",
+ "periodCustom": "Custom",
+ "selectDate": "Select date",
+ "selectEndDate": "Select end date",
+ "alertAtThreshold": "Alert at {0}% used",
+ "description": "Description (optional)",
+ "selectTargets": "Select targets",
+ "done": "Done",
+ "categories": "Categories",
+ "wallets": "Wallets",
+ "groups": "Groups",
+ "noCategoriesYet": "No categories yet",
+ "noWalletsYet": "No wallets yet",
+ "noGroupsYet": "No groups yet",
+ "budgetPageTitle": "Budgets",
+ "searchBudgets": "Search budgets",
+ "filterAll": "All",
+ "filterActive": "Active",
+ "periodWeeklyShort": "/wk",
+ "periodMonthlyShort": "/mo",
+ "periodYearlyShort": "/yr",
+ "scopeAllTransactions": "All transactions",
+ "targetTypeCategorySingular": "category",
+ "targetTypeWalletSingular": "wallet",
+ "targetTypeGroupSingular": "group",
+ "edit": "Edit",
+ "delete": "Delete",
+ "statusInactive": "Inactive",
+ "noBudgetsYet": "No budgets yet",
+ "noBudgetsDescription": "Track your spending against a target.\nSet a limit and we'll watch it for you.",
+ "noBudgetsMatchFilter": "No budgets match this filter.",
+ "noMatchesForQuery": "No matches for \"{0}\".",
+ "budgetSuffix": " budget",
+ "drawerBudgets": "Budgets",
+ "drawerEveryday": "EVERYDAY",
+ "drawerOrganize": "ORGANIZE",
+ "drawerMore": "MORE",
+ "supportEmail": "support@trakli.app",
+ "supportEmailSubject": "Trakli Support Request",
+ "supportEmailCopied": "{0} (copied to clipboard)",
+ "defaultUserName": "Trakli",
+ "appTitle": "Trakli"
}
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 1016062b..ff00389e 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -6,9 +6,9 @@
"onboardTitle1": "Simplified personal finance",
"onboardDesc1": "Trakli focuses on the basics and gives just what you need to see where your money is coming from or going to.",
"onboardTitle2": "Automated",
- "onboardDesc2": "Let us automatically pick up transactions from your SMS and fill your income and expense report before you even open the app. You can also share statements with us, such as emails.",
- "onboardTitle3": "Free and open source",
- "onboardDesc3": "It does not cost anything to use Trakli",
+ "onboardDesc2": "AI reads your receipts, PDFs, CSV and XLS statements. Connect Plaid, MCP and other providers to keep balances and transactions in sync.",
+ "onboardTitle3": "Open source",
+ "onboardDesc3": "Yours to inspect, host and trust. Audit the code, run your own server, keep your data on your terms.",
"skip": "Skip",
"login": "Login",
"loginGoogle": "Sign in with Google",
@@ -33,7 +33,7 @@
"home": "Home",
"history": "History",
"wallet": "Wallet",
- "transactionHistory": "Transaction History",
+ "transactionHistory": "Transaction history",
"addTransaction": "Add transaction",
"addGroup": "Add group",
"myGroup": "My group",
@@ -102,6 +102,18 @@
"groupCreateGroup": "Create group",
"statistics": "Statistics",
"profile": "Profile",
+ "ai": "AI",
+ "aiChatTitle": "Ask Trakli",
+ "aiChatEmptyHint": "Ask anything about your finances",
+ "aiChatComposerPlaceholder": "Ask a question…",
+ "aiChatNewChat": "New chat",
+ "aiChatThinking": "Thinking…",
+ "aiChatError": "Something went wrong. Try again.",
+ "aiChatHistory": "Chat history",
+ "aiChatUntitled": "Untitled chat",
+ "aiChatNoSessions": "No conversations yet",
+ "aiChatNoSessionsHint": "Start a new chat to see it here.",
+ "aiChatDeleteConfirm": "Delete this conversation? This can't be undone.",
"settings": "Settings",
"typeHere": "Type here",
"balanceAmountWithCurrency": "{}",
@@ -188,6 +200,7 @@
"about": "About",
"switchDefaultGroup": "Switch default group",
"walletTransfer": "Wallet transfer",
+ "transfer": "Transfer",
"walletTransferDefaultDescription": "Money transfer from {fromCurrency} wallet to {toCurrency} wallet",
"sourceWallet": "Source wallet",
"orangeMoney": "Orange money",
@@ -618,5 +631,114 @@
"importConfirmSheetTitle": "Confirm {} of {} suggestions?",
"importCreateTransactions": "Create transactions",
"importAutoCreateMissingHint": "{} item(s) without a selection — will be created from the AI suggestion.",
- "importAutoCreateNoneNeeded": "All accepted items already have a selection."
+ "importAutoCreateNoneNeeded": "All accepted items already have a selection.",
+ "budget": "Budget",
+ "budgets": "Budgets",
+ "addBudget": "Add budget",
+ "createBudget": "Create budget",
+ "editBudget": "Edit budget",
+ "deleteBudget": "Delete budget",
+ "deleteBudgetConfirm": "This will remove \"{name}\" from your budgets. This cannot be undone.",
+ "deleteBudgetSuccess": "Budget deleted",
+ "deleteBudgetError": "Failed to delete budget",
+ "closeBudgetPeriod": "Close period",
+ "closePeriod": "Close current period",
+ "closePeriodConfirm": "Close period",
+ "closePeriodSuccess": "Period closed",
+ "closePeriodError": "Failed to close period",
+ "budgetAmount": "Budget amount",
+ "budgetPeriod": "Budget period",
+ "budgetTargets": "Targets",
+ "budgetPeriodHistory": "Period history",
+ "budgetStatus": "Status",
+ "budgetRemaining": "Remaining",
+ "budgetProjected": "Projected",
+ "budgetRefunds": "Refunds",
+ "budgetRollover": "Rollover",
+ "budgetRolloverIn": "Rollover in",
+ "budgetPeriodOpen": "Open",
+ "closePeriodMessage": "This will lock the current period's spending and start a new one. Unused amount will roll into the next period.",
+ "closePeriodTitle": "Close this period?",
+ "budgetStatusOverBudget": "OVER BUDGET",
+ "budgetStatusForecastBreach": "FORECAST BREACH",
+ "budgetStatusNearLimit": "NEAR LIMIT",
+ "budgetStatusOnTrack": "ON TRACK",
+ "budgetStatusAwaitingSync": "AWAITING SYNC",
+ "budgetPeriodWeekly": "Weekly · starting",
+ "budgetPeriodMonthly": "Monthly · starting",
+ "budgetPeriodYearly": "Yearly · starting",
+ "budgetKpiRemaining": "Remaining",
+ "budgetKpiProjected": "Projected",
+ "budgetKpiRefunds": "Refunds",
+ "budgetKpiRolloverIn": "Rollover in",
+ "budgetTargetCategory": "Category",
+ "budgetTargetWallet": "Wallet",
+ "budgetTargetGroup": "Group",
+ "budgetTargetsLabel": "Targets",
+ "budgetPeriodHistoryLabel": "Period history",
+ "budgetTargetsApplyAll": "Applies to all transactions in this period.",
+ "budgetNoPeriods": "No closed periods yet.",
+ "budgetNoProgress": "Progress will be available once this budget syncs with the server.",
+ "budgetTargetUnnamed": "(unnamed)",
+ "newBudget": "New budget",
+ "period": "Period",
+ "startDate": "Start date",
+ "endDate": "End date",
+ "target": "target",
+ "targets": "targets",
+ "selected": "selected",
+ "budgetRolloverDescription": "Carry leftover budget into next period",
+ "budgetForecastAlerts": "Forecast alerts",
+ "budgetForecastAlertsDescription": "Warn me if I'm projected to overspend",
+ "active": "Active",
+ "saveChanges": "Save changes",
+ "budgetNameRequired": "Please enter a name",
+ "budgetAmountRequired": "Please enter a valid amount",
+ "budgetCurrencyRequired": "Currency must be 3 letters",
+ "nameHint": "e.g. Groceries",
+ "amountHint": "0.00",
+ "periodWeekly": "Weekly",
+ "periodMonthly": "Monthly",
+ "periodYearly": "Yearly",
+ "periodCustom": "Custom",
+ "selectDate": "Select date",
+ "selectEndDate": "Select end date",
+ "alertAtThreshold": "Alert at {0}% used",
+ "description": "Description (optional)",
+ "selectTargets": "Select targets",
+ "done": "Done",
+ "categories": "Categories",
+ "wallets": "Wallets",
+ "groups": "Groups",
+ "noCategoriesYet": "No categories yet",
+ "noWalletsYet": "No wallets yet",
+ "noGroupsYet": "No groups yet",
+ "budgetPageTitle": "Budgets",
+ "searchBudgets": "Search budgets",
+ "filterAll": "All",
+ "filterActive": "Active",
+ "periodWeeklyShort": "/wk",
+ "periodMonthlyShort": "/mo",
+ "periodYearlyShort": "/yr",
+ "scopeAllTransactions": "All transactions",
+ "targetTypeCategorySingular": "category",
+ "targetTypeWalletSingular": "wallet",
+ "targetTypeGroupSingular": "group",
+ "edit": "Edit",
+ "delete": "Delete",
+ "statusInactive": "Inactive",
+ "noBudgetsYet": "No budgets yet",
+ "noBudgetsDescription": "Track your spending against a target.\nSet a limit and we'll watch it for you.",
+ "noBudgetsMatchFilter": "No budgets match this filter.",
+ "noMatchesForQuery": "No matches for \"{0}\".",
+ "budgetSuffix": " budget",
+ "drawerBudgets": "Budgets",
+ "drawerEveryday": "EVERYDAY",
+ "drawerOrganize": "ORGANIZE",
+ "drawerMore": "MORE",
+ "supportEmail": "support@trakli.app",
+ "supportEmailSubject": "Trakli Support Request",
+ "supportEmailCopied": "{0} (copied to clipboard)",
+ "defaultUserName": "Trakli",
+ "appTitle": "Trakli"
}
diff --git a/assets/translations/es.json b/assets/translations/es.json
index 6f2fec1d..d280e9c8 100644
--- a/assets/translations/es.json
+++ b/assets/translations/es.json
@@ -94,6 +94,18 @@
"groupCreateGroup": "Crear grupo",
"statistics": "Estadísticas",
"profile": "Perfil",
+ "ai": "IA",
+ "aiChatTitle": "Preguntar a Trakli",
+ "aiChatEmptyHint": "Pregunta lo que quieras sobre tus finanzas",
+ "aiChatComposerPlaceholder": "Haz una pregunta…",
+ "aiChatNewChat": "Nuevo chat",
+ "aiChatThinking": "Pensando…",
+ "aiChatError": "Algo salió mal. Inténtalo de nuevo.",
+ "aiChatHistory": "Historial",
+ "aiChatUntitled": "Chat sin título",
+ "aiChatNoSessions": "Aún no hay conversaciones",
+ "aiChatNoSessionsHint": "Inicia un chat nuevo para verlo aquí.",
+ "aiChatDeleteConfirm": "¿Eliminar esta conversación? No se puede deshacer.",
"settings": "Configuraciones",
"typeHere": "Escribe aquí",
"balanceAmountWithCurrency": "{}",
@@ -189,6 +201,7 @@
"about": "Acerca de",
"switchDefaultGroup": "Cambiar grupo predeterminado",
"walletTransfer": "Transferencia de billetera",
+ "transfer": "Transferir",
"walletTransferDefaultDescription": "Transferencia de dinero de la billetera {fromCurrency} a la billetera {toCurrency}",
"sourceWallet": "Billetera de origen",
"orangeMoney": "Orange money",
@@ -569,5 +582,163 @@
"groupsDesc": "Agrupa transacciones relacionadas",
"partiesDesc": "Personas y organizaciones con las que realizas transacciones",
"walletsDesc": "Cuentas por las que se mueve el dinero",
- "cancelled": "Cancelado"
+ "cancelled": "Cancelado",
+ "budget": "Presupuesto",
+ "budgets": "Presupuestos",
+ "addBudget": "Agregar presupuesto",
+ "createBudget": "Crear presupuesto",
+ "editBudget": "Editar presupuesto",
+ "deleteBudget": "Eliminar presupuesto",
+ "deleteBudgetConfirm": "Esto eliminará \"{name}\" de tus presupuestos. Esta acción no se puede deshacer.",
+ "deleteBudgetSuccess": "Presupuesto eliminado",
+ "deleteBudgetError": "Error al eliminar el presupuesto",
+ "closeBudgetPeriod": "Cerrar período",
+ "closePeriod": "Cerrar período actual",
+ "closePeriodConfirm": "Cerrar período",
+ "closePeriodSuccess": "Período cerrado",
+ "closePeriodError": "Error al cerrar el período",
+ "budgetAmount": "Monto del presupuesto",
+ "budgetPeriod": "Período del presupuesto",
+ "budgetTargets": "Objetivos",
+ "budgetPeriodHistory": "Historial de períodos",
+ "budgetStatus": "Estado",
+ "budgetRemaining": "Restante",
+ "budgetProjected": "Proyectado",
+ "budgetRefunds": "Reembolsos",
+ "budgetRollover": "Traslado",
+ "budgetRolloverIn": "Traslado de",
+ "importAiSuggested": "IA sugiere: {}",
+ "importAmount": "Cantidad",
+ "importAnalysisFailed": "Análisis fallido",
+ "importAnalysisFailedHint": "Algo salió mal al procesar este documento. Intenta cargarlo de nuevo.",
+ "importAnalyze": "Analizar",
+ "importAnalyzing": "Analizando documento…",
+ "importAutoCreateCategories": "Crear categorías faltantes automáticamente",
+ "importAutoCreateMissingHint": "{} elemento(s) sin selección — se crearán a partir de la sugerencia de IA.",
+ "importAutoCreateNoneNeeded": "Todos los elementos aceptados ya tienen una selección.",
+ "importAutoCreateParties": "Crear partes faltantes automáticamente",
+ "importAutoCreateWallets": "Crear carteras faltantes automáticamente",
+ "importCategory": "Categoría",
+ "importCheckingDuplicates": "Verificando duplicados…",
+ "importConfirm": "Confirmar importación",
+ "importConfirmSheetTitle": "¿Confirmar {} de {} sugerencias?",
+ "importCreateTransactions": "Crear transacciones",
+ "importCreatedCount": "{} transacción(es) importada(s).",
+ "importDate": "Fecha",
+ "importDescription": "Descripción",
+ "importDocBankStatement": "Extracto bancario",
+ "importDocInvoice": "Factura",
+ "importDocPayStub": "Talón de pago",
+ "importDocReceipt": "Recibo",
+ "importDocTypeLabel": "Tipo de documento",
+ "importDocUtilityBill": "Factura de servicios",
+ "importEnriching": "Limpiando datos extraídos…",
+ "importExtracting": "Leyendo el documento…",
+ "importNoAcceptedSuggestions": "Selecciona al menos una sugerencia para confirmar.",
+ "importNoActivity": "Sin importaciones aún. Comienza con una de las acciones anteriores.",
+ "importNoSuggestions": "No se extrajeron sugerencias de este documento.",
+ "importNoSuggestionsClose": "Cerrar",
+ "importNoSuggestionsTitle": "Sin sugerencias",
+ "importNoTransactionsFound": "No se encontraron transacciones",
+ "importParty": "Parte",
+ "importRecentActivity": "Actividad reciente",
+ "importReviewSuggestions": "Revisar sugerencias",
+ "importScanDocument": "Escanear documento",
+ "importScanDocumentDesc": "Extracción basada en IA de PDF, imágenes y CSV",
+ "importSourceCamera": "Cámara",
+ "importSourceCameraDesc": "Escaneo instantáneo",
+ "importSourceFile": "Archivo",
+ "importSourceFileDesc": "PDF, imagen o CSV",
+ "importSourceGallery": "Galería",
+ "importSourceGalleryDesc": "Biblioteca de fotos",
+ "importSourceLabel": "Fuente",
+ "importType": "Tipo",
+ "importWallet": "Cartera",
+ "imports": "Importaciones",
+ "importsDesc": "Traer transacciones desde archivos o documentos",
+ "budgetPeriodOpen": "Abierto",
+ "closePeriodMessage": "Esto bloqueará el gasto del período actual e iniciará uno nuevo. El monto no utilizado se trasladará al siguiente período.",
+ "closePeriodTitle": "¿Cerrar este período?",
+ "budgetStatusOverBudget": "PRESUPUESTO EXCEDIDO",
+ "budgetStatusForecastBreach": "PREDICCIÓN DE INCUMPLIMIENTO",
+ "budgetStatusNearLimit": "CERCA DEL LÍMITE",
+ "budgetStatusOnTrack": "EN CAMINO",
+ "budgetStatusAwaitingSync": "ESPERANDO SINCRONIZACIÓN",
+ "budgetPeriodWeekly": "Semanal · a partir de",
+ "budgetPeriodMonthly": "Mensual · a partir de",
+ "budgetPeriodYearly": "Anual · a partir de",
+ "budgetKpiRemaining": "Restante",
+ "budgetKpiProjected": "Proyectado",
+ "budgetKpiRefunds": "Reembolsos",
+ "budgetKpiRolloverIn": "Traslado de",
+ "budgetTargetCategory": "Categoría",
+ "budgetTargetWallet": "Cartera",
+ "budgetTargetGroup": "Grupo",
+ "budgetTargetsLabel": "Objetivos",
+ "budgetPeriodHistoryLabel": "Historial de períodos",
+ "budgetTargetsApplyAll": "Se aplica a todas las transacciones de este período.",
+ "budgetNoPeriods": "Sin períodos cerrados aún.",
+ "budgetNoProgress": "El progreso estará disponible una vez que este presupuesto se sincronice con el servidor.",
+ "budgetTargetUnnamed": "(sin nombre)",
+ "newBudget": "New budget",
+ "period": "Period",
+ "startDate": "Start date",
+ "endDate": "End date",
+ "target": "target",
+ "targets": "targets",
+ "selected": "selected",
+ "budgetRolloverDescription": "Carry leftover budget into next period",
+ "budgetForecastAlerts": "Forecast alerts",
+ "budgetForecastAlertsDescription": "Warn me if I'm projected to overspend",
+ "active": "Active",
+ "saveChanges": "Save changes",
+ "budgetNameRequired": "Please enter a name",
+ "budgetAmountRequired": "Please enter a valid amount",
+ "budgetCurrencyRequired": "Currency must be 3 letters",
+ "nameHint": "e.g. Groceries",
+ "amountHint": "0.00",
+ "periodWeekly": "Weekly",
+ "periodMonthly": "Monthly",
+ "periodYearly": "Yearly",
+ "periodCustom": "Custom",
+ "selectDate": "Select date",
+ "selectEndDate": "Select end date",
+ "alertAtThreshold": "Alert at {0}% used",
+ "description": "Description (optional)",
+ "selectTargets": "Select targets",
+ "done": "Done",
+ "categories": "Categories",
+ "wallets": "Wallets",
+ "groups": "Groups",
+ "noCategoriesYet": "No categories yet",
+ "noWalletsYet": "No wallets yet",
+ "noGroupsYet": "No groups yet",
+ "budgetPageTitle": "Budgets",
+ "searchBudgets": "Search budgets",
+ "filterAll": "All",
+ "filterActive": "Active",
+ "periodWeeklyShort": "/wk",
+ "periodMonthlyShort": "/mo",
+ "periodYearlyShort": "/yr",
+ "scopeAllTransactions": "All transactions",
+ "targetTypeCategorySingular": "category",
+ "targetTypeWalletSingular": "wallet",
+ "targetTypeGroupSingular": "group",
+ "edit": "Edit",
+ "delete": "Delete",
+ "statusInactive": "Inactive",
+ "noBudgetsYet": "No budgets yet",
+ "noBudgetsDescription": "Track your spending against a target.\nSet a limit and we'll watch it for you.",
+ "noBudgetsMatchFilter": "No budgets match this filter.",
+ "noMatchesForQuery": "No matches for \"{0}\".",
+ "budgetSuffix": " budget",
+ "drawerBudgets": "Budgets",
+ "drawerEveryday": "EVERYDAY",
+ "drawerOrganize": "ORGANIZE",
+ "drawerMore": "MORE",
+ "supportEmail": "support@trakli.app",
+ "supportEmailSubject": "Trakli Support Request",
+ "supportEmailCopied": "{0} (copied to clipboard)",
+ "defaultUserName": "Trakli",
+ "appTitle": "Trakli"
}
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index 8b652581..457b76e6 100644
--- a/assets/translations/fr.json
+++ b/assets/translations/fr.json
@@ -93,6 +93,18 @@
"groupCreateGroup": "Créer un groupe",
"statistics": "Statistiques",
"profile": "Profil",
+ "ai": "IA",
+ "aiChatTitle": "Demander à Trakli",
+ "aiChatEmptyHint": "Posez n'importe quelle question sur vos finances",
+ "aiChatComposerPlaceholder": "Posez une question…",
+ "aiChatNewChat": "Nouvelle discussion",
+ "aiChatThinking": "Réflexion…",
+ "aiChatError": "Une erreur s'est produite. Réessayez.",
+ "aiChatHistory": "Historique",
+ "aiChatUntitled": "Discussion sans titre",
+ "aiChatNoSessions": "Pas encore de conversations",
+ "aiChatNoSessionsHint": "Démarrez une nouvelle discussion pour la voir ici.",
+ "aiChatDeleteConfirm": "Supprimer cette conversation ? Cette action est irréversible.",
"settings": "Paramètres",
"typeHere": "Tapez ici",
"balanceAmountWithCurrency": "{}",
@@ -187,6 +199,7 @@
"about": "À propos",
"switchDefaultGroup": "Changer de groupe par défaut",
"walletTransfer": "Transfert de portefeuille",
+ "transfer": "Transfert",
"walletTransferDefaultDescription": "Transfert d'argent du portefeuille {fromCurrency} vers le portefeuille {toCurrency}",
"sourceWallet": "Portefeuille source",
"orangeMoney": "Orange money",
@@ -618,5 +631,114 @@
"importConfirmSheetTitle": "Confirmer {} suggestion(s) sur {} ?",
"importCreateTransactions": "Créer les transactions",
"importAutoCreateMissingHint": "{} élément(s) sans sélection — sera créé depuis la suggestion IA.",
- "importAutoCreateNoneNeeded": "Tous les éléments acceptés ont déjà une sélection."
+ "importAutoCreateNoneNeeded": "Tous les éléments acceptés ont déjà une sélection.",
+ "budget": "Budget",
+ "budgets": "Budgets",
+ "addBudget": "Ajouter un budget",
+ "createBudget": "Créer un budget",
+ "editBudget": "Modifier le budget",
+ "deleteBudget": "Supprimer le budget",
+ "deleteBudgetConfirm": "Cela supprimera \"{name}\" de vos budgets. Cette action ne peut pas être annulée.",
+ "deleteBudgetSuccess": "Budget supprimé",
+ "deleteBudgetError": "Échec de la suppression du budget",
+ "closeBudgetPeriod": "Fermer la période",
+ "closePeriod": "Fermer la période actuelle",
+ "closePeriodConfirm": "Fermer la période",
+ "closePeriodSuccess": "Période fermée",
+ "closePeriodError": "Échec de la fermeture de la période",
+ "budgetAmount": "Montant du budget",
+ "budgetPeriod": "Période du budget",
+ "budgetTargets": "Cibles",
+ "budgetPeriodHistory": "Historique des périodes",
+ "budgetStatus": "État",
+ "budgetRemaining": "Restant",
+ "budgetProjected": "Projeté",
+ "budgetRefunds": "Remboursements",
+ "budgetRollover": "Report",
+ "budgetRolloverIn": "Report de",
+ "budgetPeriodOpen": "Ouvert",
+ "closePeriodMessage": "Cela verrouillera les dépenses de la période actuelle et en commencera une nouvelle. Le montant inutilisé sera reporté à la période suivante.",
+ "closePeriodTitle": "Fermer cette période ?",
+ "budgetStatusOverBudget": "DÉPASSEMENT DE BUDGET",
+ "budgetStatusForecastBreach": "DÉPASSEMENT PRÉVU",
+ "budgetStatusNearLimit": "PRÈS DE LA LIMITE",
+ "budgetStatusOnTrack": "EN RÈGLE",
+ "budgetStatusAwaitingSync": "EN ATTENTE DE SYNCHRONISATION",
+ "budgetPeriodWeekly": "Hebdomadaire · à partir de",
+ "budgetPeriodMonthly": "Mensuel · à partir de",
+ "budgetPeriodYearly": "Annuel · à partir de",
+ "budgetKpiRemaining": "Restant",
+ "budgetKpiProjected": "Projeté",
+ "budgetKpiRefunds": "Remboursements",
+ "budgetKpiRolloverIn": "Report de",
+ "budgetTargetCategory": "Catégorie",
+ "budgetTargetWallet": "Portefeuille",
+ "budgetTargetGroup": "Groupe",
+ "budgetTargetsLabel": "Cibles",
+ "budgetPeriodHistoryLabel": "Historique des périodes",
+ "budgetTargetsApplyAll": "S'applique à toutes les transactions de cette période.",
+ "budgetNoPeriods": "Aucune période fermée pour le moment.",
+ "budgetNoProgress": "Le progrès sera disponible une fois ce budget synchronisé avec le serveur.",
+ "budgetTargetUnnamed": "(sans nom)",
+ "newBudget": "New budget",
+ "period": "Period",
+ "startDate": "Start date",
+ "endDate": "End date",
+ "target": "target",
+ "targets": "targets",
+ "selected": "selected",
+ "budgetRolloverDescription": "Carry leftover budget into next period",
+ "budgetForecastAlerts": "Forecast alerts",
+ "budgetForecastAlertsDescription": "Warn me if I'm projected to overspend",
+ "active": "Active",
+ "saveChanges": "Save changes",
+ "budgetNameRequired": "Please enter a name",
+ "budgetAmountRequired": "Please enter a valid amount",
+ "budgetCurrencyRequired": "Currency must be 3 letters",
+ "nameHint": "e.g. Groceries",
+ "amountHint": "0.00",
+ "periodWeekly": "Weekly",
+ "periodMonthly": "Monthly",
+ "periodYearly": "Yearly",
+ "periodCustom": "Custom",
+ "selectDate": "Select date",
+ "selectEndDate": "Select end date",
+ "alertAtThreshold": "Alert at {0}% used",
+ "description": "Description (optional)",
+ "selectTargets": "Select targets",
+ "done": "Done",
+ "categories": "Categories",
+ "wallets": "Wallets",
+ "groups": "Groups",
+ "noCategoriesYet": "No categories yet",
+ "noWalletsYet": "No wallets yet",
+ "noGroupsYet": "No groups yet",
+ "budgetPageTitle": "Budgets",
+ "searchBudgets": "Search budgets",
+ "filterAll": "All",
+ "filterActive": "Active",
+ "periodWeeklyShort": "/wk",
+ "periodMonthlyShort": "/mo",
+ "periodYearlyShort": "/yr",
+ "scopeAllTransactions": "All transactions",
+ "targetTypeCategorySingular": "category",
+ "targetTypeWalletSingular": "wallet",
+ "targetTypeGroupSingular": "group",
+ "edit": "Edit",
+ "delete": "Delete",
+ "statusInactive": "Inactive",
+ "noBudgetsYet": "No budgets yet",
+ "noBudgetsDescription": "Track your spending against a target.\nSet a limit and we'll watch it for you.",
+ "noBudgetsMatchFilter": "No budgets match this filter.",
+ "noMatchesForQuery": "No matches for \"{0}\".",
+ "budgetSuffix": " budget",
+ "drawerBudgets": "Budgets",
+ "drawerEveryday": "EVERYDAY",
+ "drawerOrganize": "ORGANIZE",
+ "drawerMore": "MORE",
+ "supportEmail": "support@trakli.app",
+ "supportEmailSubject": "Trakli Support Request",
+ "supportEmailCopied": "{0} (copied to clipboard)",
+ "defaultUserName": "Trakli",
+ "appTitle": "Trakli"
}
diff --git a/assets/translations/it.json b/assets/translations/it.json
index e594993e..4a6c207f 100644
--- a/assets/translations/it.json
+++ b/assets/translations/it.json
@@ -93,6 +93,18 @@
"groupCreateGroup": "Crea gruppo",
"statistics": "Statistiche",
"profile": "Profilo",
+ "ai": "IA",
+ "aiChatTitle": "Chiedi a Trakli",
+ "aiChatEmptyHint": "Chiedi qualsiasi cosa sulle tue finanze",
+ "aiChatComposerPlaceholder": "Fai una domanda…",
+ "aiChatNewChat": "Nuova chat",
+ "aiChatThinking": "Sto pensando…",
+ "aiChatError": "Qualcosa è andato storto. Riprova.",
+ "aiChatHistory": "Cronologia",
+ "aiChatUntitled": "Chat senza titolo",
+ "aiChatNoSessions": "Nessuna conversazione ancora",
+ "aiChatNoSessionsHint": "Avvia una nuova chat per vederla qui.",
+ "aiChatDeleteConfirm": "Eliminare questa conversazione? Non puoi annullare.",
"settings": "Impostazioni",
"typeHere": "Scrivi qui",
"balanceAmountWithCurrency": "{}",
@@ -192,6 +204,7 @@
"about": "Informazioni",
"switchDefaultGroup": "Cambia gruppo predefinito",
"walletTransfer": "Trasferimento portafoglio",
+ "transfer": "Trasferimento",
"walletTransferDefaultDescription": "Trasferimento di denaro dal portafoglio {fromCurrency} al portafoglio {toCurrency}",
"sourceWallet": "Portafoglio di origine",
"orangeMoney": "Orange money",
@@ -569,5 +582,163 @@
"groupsDesc": "Raggruppa le transazioni correlate",
"partiesDesc": "Persone e organizzazioni con cui effettui transazioni",
"walletsDesc": "Conti attraverso cui si muove il denaro",
- "cancelled": "Annullato"
+ "cancelled": "Annullato",
+ "budget": "Budget",
+ "budgets": "Budget",
+ "addBudget": "Aggiungi budget",
+ "createBudget": "Crea budget",
+ "editBudget": "Modifica budget",
+ "deleteBudget": "Elimina budget",
+ "deleteBudgetConfirm": "Questo rimuoverà \"{name}\" dai tuoi budget. Questa azione non può essere annullata.",
+ "deleteBudgetSuccess": "Budget eliminato",
+ "deleteBudgetError": "Errore nell'eliminazione del budget",
+ "closeBudgetPeriod": "Chiudi periodo",
+ "closePeriod": "Chiudi il periodo attuale",
+ "closePeriodConfirm": "Chiudi periodo",
+ "closePeriodSuccess": "Periodo chiuso",
+ "closePeriodError": "Errore nella chiusura del periodo",
+ "budgetAmount": "Importo del budget",
+ "budgetPeriod": "Periodo del budget",
+ "budgetTargets": "Obiettivi",
+ "budgetPeriodHistory": "Cronologia dei periodi",
+ "budgetStatus": "Stato",
+ "budgetRemaining": "Rimanente",
+ "budgetProjected": "Previsto",
+ "budgetRefunds": "Rimborsi",
+ "budgetRollover": "Riporto",
+ "budgetRolloverIn": "Riporto di",
+ "importAiSuggested": "IA suggerisce: {}",
+ "importAmount": "Importo",
+ "importAnalysisFailed": "Analisi non riuscita",
+ "importAnalysisFailedHint": "Qualcosa è andato storto durante l'elaborazione di questo documento. Prova a caricarlo di nuovo.",
+ "importAnalyze": "Analizza",
+ "importAnalyzing": "Analisi del documento…",
+ "importAutoCreateCategories": "Crea automaticamente le categorie mancanti",
+ "importAutoCreateMissingHint": "{} elemento(i) senza selezione — verranno creati dal suggerimento dell'IA.",
+ "importAutoCreateNoneNeeded": "Tutti gli elementi accettati hanno già una selezione.",
+ "importAutoCreateParties": "Crea automaticamente le parti mancanti",
+ "importAutoCreateWallets": "Crea automaticamente i portafogli mancanti",
+ "importCategory": "Categoria",
+ "importCheckingDuplicates": "Verifica dei duplicati…",
+ "importConfirm": "Conferma importazione",
+ "importConfirmSheetTitle": "Confermare {} di {} suggerimenti?",
+ "importCreateTransactions": "Crea transazioni",
+ "importCreatedCount": "{} transazione(i) importata(e).",
+ "importDate": "Data",
+ "importDescription": "Descrizione",
+ "importDocBankStatement": "Estratto conto",
+ "importDocInvoice": "Fattura",
+ "importDocPayStub": "Busta paga",
+ "importDocReceipt": "Ricevuta",
+ "importDocTypeLabel": "Tipo di documento",
+ "importDocUtilityBill": "Bolletta",
+ "importEnriching": "Pulizia dei dati estratti…",
+ "importExtracting": "Lettura del documento…",
+ "importNoAcceptedSuggestions": "Seleziona almeno un suggerimento per confermare.",
+ "importNoActivity": "Nessuna importazione ancora. Inizia con una delle azioni sopra.",
+ "importNoSuggestions": "Nessun suggerimento estratto da questo documento.",
+ "importNoSuggestionsClose": "Chiudi",
+ "importNoSuggestionsTitle": "Nessun suggerimento",
+ "importNoTransactionsFound": "Nessuna transazione trovata",
+ "importParty": "Parte",
+ "importRecentActivity": "Attività recente",
+ "importReviewSuggestions": "Rivedi suggerimenti",
+ "importScanDocument": "Scansiona documento",
+ "importScanDocumentDesc": "Estrazione basata su IA da PDF, immagini e CSV",
+ "importSourceCamera": "Fotocamera",
+ "importSourceCameraDesc": "Scansione istantanea",
+ "importSourceFile": "File",
+ "importSourceFileDesc": "PDF, immagine o CSV",
+ "importSourceGallery": "Galleria",
+ "importSourceGalleryDesc": "Libreria fotografica",
+ "importSourceLabel": "Fonte",
+ "importType": "Tipo",
+ "importWallet": "Portafoglio",
+ "imports": "Importazioni",
+ "importsDesc": "Porta transazioni da file o documenti",
+ "budgetPeriodOpen": "Aperto",
+ "closePeriodMessage": "Questo bloccherà la spesa del periodo attuale e ne inizierà uno nuovo. L'importo non utilizzato verrà trasferito al periodo successivo.",
+ "closePeriodTitle": "Chiudere questo periodo?",
+ "budgetStatusOverBudget": "BUDGET SUPERATO",
+ "budgetStatusForecastBreach": "PREVISIONE DI VIOLAZIONE",
+ "budgetStatusNearLimit": "VICINO AL LIMITE",
+ "budgetStatusOnTrack": "IN LINEA",
+ "budgetStatusAwaitingSync": "IN ATTESA DI SINCRONIZZAZIONE",
+ "budgetPeriodWeekly": "Settimanale · a partire da",
+ "budgetPeriodMonthly": "Mensile · a partire da",
+ "budgetPeriodYearly": "Annuale · a partire da",
+ "budgetKpiRemaining": "Rimanente",
+ "budgetKpiProjected": "Previsto",
+ "budgetKpiRefunds": "Rimborsi",
+ "budgetKpiRolloverIn": "Riporto di",
+ "budgetTargetCategory": "Categoria",
+ "budgetTargetWallet": "Portafoglio",
+ "budgetTargetGroup": "Gruppo",
+ "budgetTargetsLabel": "Obiettivi",
+ "budgetPeriodHistoryLabel": "Cronologia dei periodi",
+ "budgetTargetsApplyAll": "Si applica a tutte le transazioni in questo periodo.",
+ "budgetNoPeriods": "Nessun periodo chiuso ancora.",
+ "budgetNoProgress": "Il progresso sarà disponibile una volta che questo budget è sincronizzato con il server.",
+ "budgetTargetUnnamed": "(senza nome)",
+ "newBudget": "New budget",
+ "period": "Period",
+ "startDate": "Start date",
+ "endDate": "End date",
+ "target": "target",
+ "targets": "targets",
+ "selected": "selected",
+ "budgetRolloverDescription": "Carry leftover budget into next period",
+ "budgetForecastAlerts": "Forecast alerts",
+ "budgetForecastAlertsDescription": "Warn me if I'm projected to overspend",
+ "active": "Active",
+ "saveChanges": "Save changes",
+ "budgetNameRequired": "Please enter a name",
+ "budgetAmountRequired": "Please enter a valid amount",
+ "budgetCurrencyRequired": "Currency must be 3 letters",
+ "nameHint": "e.g. Groceries",
+ "amountHint": "0.00",
+ "periodWeekly": "Weekly",
+ "periodMonthly": "Monthly",
+ "periodYearly": "Yearly",
+ "periodCustom": "Custom",
+ "selectDate": "Select date",
+ "selectEndDate": "Select end date",
+ "alertAtThreshold": "Alert at {0}% used",
+ "description": "Description (optional)",
+ "selectTargets": "Select targets",
+ "done": "Done",
+ "categories": "Categories",
+ "wallets": "Wallets",
+ "groups": "Groups",
+ "noCategoriesYet": "No categories yet",
+ "noWalletsYet": "No wallets yet",
+ "noGroupsYet": "No groups yet",
+ "budgetPageTitle": "Budgets",
+ "searchBudgets": "Search budgets",
+ "filterAll": "All",
+ "filterActive": "Active",
+ "periodWeeklyShort": "/wk",
+ "periodMonthlyShort": "/mo",
+ "periodYearlyShort": "/yr",
+ "scopeAllTransactions": "All transactions",
+ "targetTypeCategorySingular": "category",
+ "targetTypeWalletSingular": "wallet",
+ "targetTypeGroupSingular": "group",
+ "edit": "Edit",
+ "delete": "Delete",
+ "statusInactive": "Inactive",
+ "noBudgetsYet": "No budgets yet",
+ "noBudgetsDescription": "Track your spending against a target.\nSet a limit and we'll watch it for you.",
+ "noBudgetsMatchFilter": "No budgets match this filter.",
+ "noMatchesForQuery": "No matches for \"{0}\".",
+ "budgetSuffix": " budget",
+ "drawerBudgets": "Budgets",
+ "drawerEveryday": "EVERYDAY",
+ "drawerOrganize": "ORGANIZE",
+ "drawerMore": "MORE",
+ "supportEmail": "support@trakli.app",
+ "supportEmailSubject": "Trakli Support Request",
+ "supportEmailCopied": "{0} (copied to clipboard)",
+ "defaultUserName": "Trakli",
+ "appTitle": "Trakli"
}
diff --git a/assets/translations/ru.json b/assets/translations/ru.json
index c761ea54..3bef411b 100644
--- a/assets/translations/ru.json
+++ b/assets/translations/ru.json
@@ -102,6 +102,18 @@
"groupCreateGroup": "Создать группу",
"statistics": "Статистика",
"profile": "Профиль",
+ "ai": "ИИ",
+ "aiChatTitle": "Спросить Trakli",
+ "aiChatEmptyHint": "Спросите что угодно о ваших финансах",
+ "aiChatComposerPlaceholder": "Задайте вопрос…",
+ "aiChatNewChat": "Новый чат",
+ "aiChatThinking": "Думаю…",
+ "aiChatError": "Что-то пошло не так. Попробуйте снова.",
+ "aiChatHistory": "История чатов",
+ "aiChatUntitled": "Чат без названия",
+ "aiChatNoSessions": "Пока нет разговоров",
+ "aiChatNoSessionsHint": "Начните новый чат, чтобы он появился здесь.",
+ "aiChatDeleteConfirm": "Удалить этот разговор? Это действие нельзя отменить.",
"settings": "Настройки",
"typeHere": "Введите здесь",
"balanceAmountWithCurrency": "{}",
@@ -188,6 +200,7 @@
"about": "О приложении",
"switchDefaultGroup": "Сменить группу по умолчанию",
"walletTransfer": "Перевод между кошельками",
+ "transfer": "Перевод",
"walletTransferDefaultDescription": "Перевод средств с кошелька {fromCurrency} на кошелек {toCurrency}",
"sourceWallet": "Исходный кошелек",
"orangeMoney": "Orange Money",
@@ -569,5 +582,163 @@
"groupsDesc": "Объединяйте связанные транзакции",
"partiesDesc": "Люди и организации, с которыми вы совершаете транзакции",
"walletsDesc": "Счета, через которые проходят деньги",
- "cancelled": "Отменено"
+ "cancelled": "Отменено",
+ "budget": "Бюджет",
+ "budgets": "Бюджеты",
+ "addBudget": "Добавить бюджет",
+ "createBudget": "Создать бюджет",
+ "editBudget": "Редактировать бюджет",
+ "deleteBudget": "Удалить бюджет",
+ "deleteBudgetConfirm": "Это удалит \"{name}\" из ваших бюджетов. Это действие не может быть отменено.",
+ "deleteBudgetSuccess": "Бюджет удален",
+ "deleteBudgetError": "Ошибка удаления бюджета",
+ "closeBudgetPeriod": "Закрыть период",
+ "closePeriod": "Закрыть текущий период",
+ "closePeriodConfirm": "Закрыть период",
+ "closePeriodSuccess": "Период закрыт",
+ "closePeriodError": "Ошибка закрытия периода",
+ "budgetAmount": "Сумма бюджета",
+ "budgetPeriod": "Период бюджета",
+ "budgetTargets": "Цели",
+ "budgetPeriodHistory": "История периодов",
+ "budgetStatus": "Статус",
+ "budgetRemaining": "Осталось",
+ "budgetProjected": "Прогноз",
+ "budgetRefunds": "Возвраты",
+ "budgetRollover": "Перенос",
+ "budgetRolloverIn": "Перенос на",
+ "importAiSuggested": "ИИ предлагает: {}",
+ "importAmount": "Сумма",
+ "importAnalysisFailed": "Анализ не удался",
+ "importAnalysisFailedHint": "При обработке документа произошла ошибка. Попробуйте загрузить его снова.",
+ "importAnalyze": "Анализировать",
+ "importAnalyzing": "Анализ документа…",
+ "importAutoCreateCategories": "Автоматически создавать отсутствующие категории",
+ "importAutoCreateMissingHint": "{} элемент(ов) без выбора — будут созданы на основе предложения ИИ.",
+ "importAutoCreateNoneNeeded": "Все принятые элементы уже имеют выбор.",
+ "importAutoCreateParties": "Автоматически создавать отсутствующие стороны",
+ "importAutoCreateWallets": "Автоматически создавать отсутствующие кошельки",
+ "importCategory": "Категория",
+ "importCheckingDuplicates": "Проверка дубликатов…",
+ "importConfirm": "Подтвердить импорт",
+ "importConfirmSheetTitle": "Подтвердить {} из {} предложений?",
+ "importCreateTransactions": "Создать транзакции",
+ "importCreatedCount": "Импортировано {} транзакция(и).",
+ "importDate": "Дата",
+ "importDescription": "Описание",
+ "importDocBankStatement": "Выписка из банка",
+ "importDocInvoice": "Счет-фактура",
+ "importDocPayStub": "Расчетный лист",
+ "importDocReceipt": "Квитанция",
+ "importDocTypeLabel": "Тип документа",
+ "importDocUtilityBill": "Коммунальный счет",
+ "importEnriching": "Очистка извлеченных данных…",
+ "importExtracting": "Чтение документа…",
+ "importNoAcceptedSuggestions": "Выберите хотя бы одно предложение для подтверждения.",
+ "importNoActivity": "Импорт еще не выполнялся. Начните с одного из указанных выше действий.",
+ "importNoSuggestions": "Из этого документа не извлечены предложения.",
+ "importNoSuggestionsClose": "Закрыть",
+ "importNoSuggestionsTitle": "Нет предложений",
+ "importNoTransactionsFound": "Транзакции не найдены",
+ "importParty": "Сторона",
+ "importRecentActivity": "Недавняя активность",
+ "importReviewSuggestions": "Просмотреть предложения",
+ "importScanDocument": "Сканировать документ",
+ "importScanDocumentDesc": "Извлечение на основе ИИ из PDF, изображений и CSV",
+ "importSourceCamera": "Камера",
+ "importSourceCameraDesc": "Мгновенное сканирование",
+ "importSourceFile": "Файл",
+ "importSourceFileDesc": "PDF, изображение или CSV",
+ "importSourceGallery": "Галерея",
+ "importSourceGalleryDesc": "Библиотека фото",
+ "importSourceLabel": "Источник",
+ "importType": "Тип",
+ "importWallet": "Кошелек",
+ "imports": "Импорты",
+ "importsDesc": "Импортируйте транзакции из файлов или документов",
+ "budgetPeriodOpen": "Открыто",
+ "closePeriodMessage": "Это заблокирует расходы текущего периода и начнет новый. Неиспользованная сумма перейдет в следующий период.",
+ "closePeriodTitle": "Закрыть этот период?",
+ "budgetStatusOverBudget": "БЮДЖЕТ ПРЕВЫШЕН",
+ "budgetStatusForecastBreach": "ПРОГНОЗ НАРУШЕНИЯ",
+ "budgetStatusNearLimit": "БЛИЗКО К ЛИМИТУ",
+ "budgetStatusOnTrack": "В НОРМЕ",
+ "budgetStatusAwaitingSync": "ОЖИДАНИЕ СИНХРОНИЗАЦИИ",
+ "budgetPeriodWeekly": "Еженедельно · начиная с",
+ "budgetPeriodMonthly": "Ежемесячно · начиная с",
+ "budgetPeriodYearly": "Ежегодно · начиная с",
+ "budgetKpiRemaining": "Осталось",
+ "budgetKpiProjected": "Прогноз",
+ "budgetKpiRefunds": "Возвраты",
+ "budgetKpiRolloverIn": "Перенос на",
+ "budgetTargetCategory": "Категория",
+ "budgetTargetWallet": "Кошелек",
+ "budgetTargetGroup": "Группа",
+ "budgetTargetsLabel": "Цели",
+ "budgetPeriodHistoryLabel": "История периодов",
+ "budgetTargetsApplyAll": "Применяется ко всем транзакциям в этом периоде.",
+ "budgetNoPeriods": "Нет закрытых периодов.",
+ "budgetNoProgress": "Прогресс будет доступен после синхронизации этого бюджета с сервером.",
+ "budgetTargetUnnamed": "(без названия)",
+ "newBudget": "New budget",
+ "period": "Period",
+ "startDate": "Start date",
+ "endDate": "End date",
+ "target": "target",
+ "targets": "targets",
+ "selected": "selected",
+ "budgetRolloverDescription": "Carry leftover budget into next period",
+ "budgetForecastAlerts": "Forecast alerts",
+ "budgetForecastAlertsDescription": "Warn me if I'm projected to overspend",
+ "active": "Active",
+ "saveChanges": "Save changes",
+ "budgetNameRequired": "Please enter a name",
+ "budgetAmountRequired": "Please enter a valid amount",
+ "budgetCurrencyRequired": "Currency must be 3 letters",
+ "nameHint": "e.g. Groceries",
+ "amountHint": "0.00",
+ "periodWeekly": "Weekly",
+ "periodMonthly": "Monthly",
+ "periodYearly": "Yearly",
+ "periodCustom": "Custom",
+ "selectDate": "Select date",
+ "selectEndDate": "Select end date",
+ "alertAtThreshold": "Alert at {0}% used",
+ "description": "Description (optional)",
+ "selectTargets": "Select targets",
+ "done": "Done",
+ "categories": "Categories",
+ "wallets": "Wallets",
+ "groups": "Groups",
+ "noCategoriesYet": "No categories yet",
+ "noWalletsYet": "No wallets yet",
+ "noGroupsYet": "No groups yet",
+ "budgetPageTitle": "Budgets",
+ "searchBudgets": "Search budgets",
+ "filterAll": "All",
+ "filterActive": "Active",
+ "periodWeeklyShort": "/wk",
+ "periodMonthlyShort": "/mo",
+ "periodYearlyShort": "/yr",
+ "scopeAllTransactions": "All transactions",
+ "targetTypeCategorySingular": "category",
+ "targetTypeWalletSingular": "wallet",
+ "targetTypeGroupSingular": "group",
+ "edit": "Edit",
+ "delete": "Delete",
+ "statusInactive": "Inactive",
+ "noBudgetsYet": "No budgets yet",
+ "noBudgetsDescription": "Track your spending against a target.\nSet a limit and we'll watch it for you.",
+ "noBudgetsMatchFilter": "No budgets match this filter.",
+ "noMatchesForQuery": "No matches for \"{0}\".",
+ "budgetSuffix": " budget",
+ "drawerBudgets": "Budgets",
+ "drawerEveryday": "EVERYDAY",
+ "drawerOrganize": "ORGANIZE",
+ "drawerMore": "MORE",
+ "supportEmail": "support@trakli.app",
+ "supportEmailSubject": "Trakli Support Request",
+ "supportEmailCopied": "{0} (copied to clipboard)",
+ "defaultUserName": "Trakli",
+ "appTitle": "Trakli"
}
diff --git a/doc/bloc_cubit_patterns.md b/doc/bloc_cubit_patterns.md
new file mode 100644
index 00000000..d07cbad1
--- /dev/null
+++ b/doc/bloc_cubit_patterns.md
@@ -0,0 +1,1025 @@
+# Bloc/Cubit Patterns Reference Guide
+
+A comprehensive guide to the state management patterns used in the Trakli mobile application.
+
+---
+
+## Table of Contents
+
+1. [Cubit Architecture Overview](#cubit-architecture-overview)
+2. [Examined Cubits Analysis](#examined-cubits-analysis)
+3. [State Pattern Documentation](#state-pattern-documentation)
+4. [Invocation Patterns](#invocation-patterns)
+5. [Error Handling](#error-handling)
+6. [Best Practices Guide](#best-practices-guide)
+7. [Common Patterns & Anti-Patterns](#common-patterns--anti-patterns)
+
+---
+
+## Cubit Architecture Overview
+
+This codebase uses **flutter_bloc** with the **Cubit** pattern (simplified BLoC without events). Cubits are used for state management across presentation layers, with injectable dependency injection and freezed for immutable state objects.
+
+### Key Technologies
+- **flutter_bloc**: State management library
+- **freezed**: Immutable data structures and code generation
+- **injectable**: Dependency injection
+- **Either**: Functional error handling (from dartz pattern)
+
+---
+
+## Examined Cubits Analysis
+
+### 1. BudgetCubit
+
+**Location**: `lib/presentation/budget/cubit/budget_cubit.dart`
+
+#### State Pattern
+
+```dart
+BudgetState(
+ List budgets,
+ bool isLoading,
+ bool isSaving,
+ bool isDeleting,
+ bool isProgressLoading,
+ bool isPeriodTransactionsLoading,
+ bool isClosingPeriod,
+ BudgetProgressEntity? selectedBudgetProgress,
+ BudgetTransactionsResponse? selectedBudgetTransactions,
+ List selectedBudgetTargets,
+ List selectedBudgetPeriodStates,
+ Failure failure,
+)
+```
+
+#### State Emission Flow
+
+| Operation | State Sequence |
+|-----------|-----------------|
+| **Load Budgets** | Initial → `isLoading: true` → `isLoading: false, budgets: [...]` or `isLoading: false, failure: ...` |
+| **Listen to Budgets** | Continuous stream → emit `budgets: [...]` on each server update |
+| **Add/Update Budget** | Initial → `isSaving: true` → `isSaving: false` (success/failure) |
+| **Delete Budget** | Initial → `isDeleting: true` → optimistic emit (remove from list) → `isDeleting: false` |
+| **Watch Budget Details** | Starts two simultaneous streams: targets & period states |
+| **Fetch Progress** | Initial → `isProgressLoading: true` → `isProgressLoading: false, selectedBudgetProgress: ...` |
+| **Close Period** | Initial → `isClosingPeriod: true` → calls refresh → `isClosingPeriod: false` |
+
+#### Key Methods
+
+```dart
+// Initialization (called in constructor)
+listenToBudgets()
+ - Subscribes to continuous server updates
+ - Auto-updates budget list when changes occur
+
+// Data Loading
+loadBudgets(active?)
+ - One-time fetch with isLoading flag
+
+// CRUD Operations
+addBudget(...) // await required
+updateBudget(...) // await required
+deleteBudget(...) // await required (with optimistic update)
+
+// Detail Screen Operations
+watchBudget(clientId) // Start listening to targets & periods
+fetchProgress(serverId) // await required
+fetchPeriodTransactions(serverId, limit) // await required
+closeBudgetPeriod(serverId) // await required
+
+// Utilities
+refreshPeriodStates() // await required
+```
+
+#### Invocation Patterns (from UI)
+
+```dart
+// In initState - Setup listeners
+final cubit = context.read();
+cubit.watchBudget(widget.budget.clientId); // Fire-and-forget
+if (id != null) {
+ cubit.fetchProgress(id); // Fire-and-forget
+}
+
+// In dialog/form - await for operations
+await context.read().deleteBudget(budget.clientId);
+await context.read().closeBudgetPeriod(id);
+
+// In BlocBuilder - reactive UI
+BlocBuilder(
+ builder: (context, state) {
+ return Text(state.budgets.length); // Rebuilds on state change
+ }
+)
+
+// In RefreshIndicator - combine multiple awaits
+return Future.wait([
+ context.read().fetchProgress(id),
+ context.read().refreshPeriodStates(),
+]);
+```
+
+---
+
+### 2. CategoryCubit
+
+**Location**: `lib/presentation/category/cubit/category_cubit.dart`
+
+#### State Pattern
+
+```dart
+CategoryState(
+ List categories,
+ bool isLoading,
+ bool isSaving,
+ bool isDeleting,
+ Failure failure,
+)
+```
+
+#### State Emission Flow
+
+| Operation | State Sequence |
+|-----------|-----------------|
+| **Load Categories** | Initial → `isLoading: true` → `isLoading: false, categories: [...]` |
+| **Listen to Categories** | Continuous stream → emit `categories: [...]` on each update |
+| **Add/Update Category** | Initial → `isSaving: true` → `isSaving: false` → calls `loadCategories()` |
+| **Delete Category** | Initial → `isDeleting: true` → optimistic emit → `isDeleting: false` |
+
+#### Key Methods
+
+```dart
+// Initialization (constructor)
+listenToCategories()
+ - Subscribes to continuous updates via usecase
+
+// Data Operations
+loadCategories() // await required
+addCategory(...) // await required, followed by loadCategories()
+updateCategory(...) // await required, followed by loadCategories()
+deleteCategory(clientId) // await required
+```
+
+#### Distinction from BudgetCubit
+
+- **Simpler state**: Only tracks main list + loading flags
+- **No nested data**: No `selectedCategory...` fields
+- **Reload pattern**: After add/update, calls `loadCategories()` instead of relying on streams
+- **Optimistic delete**: Removes from list immediately before API call
+
+#### Invocation Patterns (from UI)
+
+```dart
+// In BlocBuilder - dropdown/list rendering
+BlocBuilder(
+ builder: (context, state) {
+ return ListView(
+ children: state.categories.map((cat) => ...),
+ );
+ }
+)
+
+// Form submission - await and reload
+final cubit = context.read();
+await cubit.addCategory(
+ name: name,
+ slug: slug,
+ type: type,
+ description: description,
+ media: media,
+);
+// State will auto-update via loadCategories() call within cubit
+```
+
+---
+
+### 3. WalletCubit
+
+**Location**: `lib/presentation/wallets/cubit/wallet_cubit.dart`
+
+#### State Pattern
+
+```dart
+WalletState(
+ List wallets,
+ bool isLoading,
+ bool isSaving,
+ bool isDeleting,
+ Failure failure,
+ @Default(allWalletsIndex) int currentSelectedWalletIndex,
+)
+```
+
+#### State Emission Flow
+
+| Operation | State Sequence |
+|-----------|-----------------|
+| **Load Wallets** | Initial → `isLoading: true` → `isLoading: false, wallets: [...]` |
+| **Listen for Changes** | Initial → `isLoading: true` → Continuous stream → `isLoading: false` |
+| **Add/Update Wallet** | Initial → `isSaving: true` → `isSaving: false` (no explicit reload) |
+| **Delete Wallet** | Initial → `isDeleting: true` → `isDeleting: false` |
+| **Select Wallet** | Immediate → `currentSelectedWalletIndex: index` (no side effects) |
+
+#### Key Methods
+
+```dart
+// Initialization (constructor)
+listenForChanges()
+ - Subscribes via usecase, sets isLoading: true initially
+
+// Data Operations
+loadWallets() // await required
+addWallet(...) // await required
+updateWallet(...) // await required
+deleteWallet(clientId) // await required
+ensureDefaultWallet(...) // await required
+
+// Complex Operations
+createAndSaveDefaultWallet(...) // Complex: creates wallet + saves config
+
+// UI State
+setCurrentSelectedWalletIndex(index) // Synchronous, no await
+currentSelectedWallet // Getter, not a method
+
+// Utilities
+get isAllWalletsSelected // Helper property
+get currentSelectedWallet // Safe getter with bounds checking
+```
+
+#### Key Differences
+
+1. **Selection State**: Includes `currentSelectedWalletIndex` for UI state
+2. **Helper Properties**: Provides computed getters (`currentSelectedWallet`, `isAllWalletsSelected`)
+3. **Config Integration**: Can save configs (e.g., default wallet preference)
+4. **No explicit reload pattern**: Relies on stream subscription from constructor
+
+#### Invocation Patterns (from UI)
+
+```dart
+// Selecting a wallet - synchronous, fire-and-forget
+context.read().setCurrentSelectedWalletIndex(index);
+
+// Creating wallet - await operation
+await context.read().addWallet(
+ name: name,
+ type: type,
+ balance: balance,
+ currency: currency,
+ description: description,
+ icon: icon,
+);
+
+// Watching changes
+BlocBuilder(
+ builder: (context, state) {
+ return state.currentSelectedWallet?.name ?? 'All Wallets';
+ }
+)
+```
+
+---
+
+## State Pattern Documentation
+
+### Common State Structure Pattern
+
+All cubits in this codebase follow a consistent pattern:
+
+```dart
+@freezed
+class [Entity]State with _$[Entity]State {
+ const factory [Entity]State({
+ // Main data
+ required List<[Entity]Entity> [entities],
+
+ // Loading indicators (per operation)
+ required bool isLoading,
+ required bool isSaving,
+ required bool isDeleting,
+
+ // Optional: nested/selected data
+ [Entity]Entity? selected[Entity],
+ List? related[Entities],
+
+ // Error handling
+ required Failure failure,
+
+ // Optional: UI state
+ @Default(-1) int selectedIndex,
+ }) = _[Entity]State;
+
+ factory [Entity]State.initial() => const [Entity]State(
+ [entities]: [],
+ isLoading: false,
+ isSaving: false,
+ isDeleting: false,
+ failure: const Failure.none(),
+ );
+}
+```
+
+### State Immutability & Updates
+
+State is **always immutable** and updated via `copyWith()`:
+
+```dart
+// ✓ Correct: immutable copyWith
+emit(state.copyWith(isLoading: true, failure: const Failure.none()));
+
+// ✓ Correct: reset failure when starting new operation
+emit(state.copyWith(
+ isSaving: true,
+ failure: const Failure.none(), // Always reset on new operation
+));
+
+// ✗ Incorrect: mutating state directly
+state.budgets.add(newBudget); // DON'T DO THIS
+```
+
+### Failure Handling Pattern
+
+All operations follow a consistent error pattern:
+
+```dart
+result.fold(
+ (failure) => emit(state.copyWith(
+ isLoading: false,
+ failure: failure, // Store failure in state
+ )),
+ (data) => emit(state.copyWith(
+ isLoading: false,
+ data: data,
+ failure: const Failure.none(), // Always clear on success
+ )),
+);
+```
+
+---
+
+## Invocation Patterns
+
+### Pattern 1: Fire-and-Forget (No Await)
+
+**Use when**: You don't need to wait for completion, state emission will trigger rebuilds
+
+```dart
+// In initState or event handler
+context.read().watchBudget(budgetId);
+context.read().fetchProgress(serverId);
+
+// In callbacks
+context.read().loadCategories();
+```
+
+**State Handling**: UI subscribes via BlocBuilder/BlocListener for async feedback
+
+**Examples in codebase**:
+- `BudgetDetailScreen.initState()`: calls `watchBudget()` and `fetchProgress()` without await
+- `BudgetScreen`: calls `loadBudgets()` via callback without await
+
+---
+
+### Pattern 2: Await with UI Feedback
+
+**Use when**: Need to confirm completion before navigating or showing feedback
+
+```dart
+// In form submission
+Future _submit() async {
+ final cubit = context.read();
+
+ await cubit.addBudget(
+ name: name,
+ amount: amount,
+ // ... other params
+ );
+
+ // After await completes, state has been emitted
+ // BlocListener will handle navigation/snackbar
+}
+```
+
+**State Handling**: BlocListener with `listenWhen` to detect completion
+
+---
+
+### Pattern 3: Multiple Awaits (Coordinated Ops)
+
+**Use when**: Multiple operations must complete together
+
+```dart
+// In RefreshIndicator
+return Future.wait([
+ context.read().fetchProgress(id),
+ context.read().refreshPeriodStates(),
+]);
+```
+
+**State Handling**: UI waits for all operations, then rebuilds
+
+---
+
+### Pattern 4: Confirmation Dialog → Action → Feedback
+
+**Use when**: Destructive operations need confirmation
+
+```dart
+Future _confirmDelete(BudgetEntity budget) async {
+ // Step 1: Show dialog (blocks)
+ final confirm = await showDeleteConfirmationDialog(context, ...);
+ if (!confirm || !mounted) return;
+
+ // Step 2: Execute action (don't await - let listener handle feedback)
+ context.read().deleteBudget(budget.clientId);
+
+ // Step 3: BlocListener will detect completion and show snackbar
+}
+```
+
+**Key Point**: Action is NOT awaited; BlocListener detects state changes
+
+---
+
+### Pattern 5: Complex Multi-Step Operations
+
+**Use when**: Operation has multiple sub-operations
+
+```dart
+Future createAndSaveDefaultWallet(...) async {
+ emit(state.copyWith(isSaving: true, failure: const Failure.none()));
+
+ // Step 1: Check if wallet exists
+ final existingWallet = state.wallets.firstWhere(...);
+
+ if (existingWallet != null) {
+ // Step 2a: Save existing wallet as default
+ final saveResult = await saveConfigUseCase(...);
+ // Handle result...
+ } else {
+ // Step 2b: Create new wallet
+ final result = await addWalletUseCase(...);
+
+ // Step 3: Save as default
+ final saveWallet = await saveConfigUseCase(...);
+ // Handle result...
+ }
+
+ emit(state.copyWith(isSaving: false, failure: ...));
+}
+```
+
+---
+
+### BlocBuilder Usage Pattern
+
+**For reactive UI that rebuilds on state changes:**
+
+```dart
+BlocBuilder(
+ builder: (context, state) {
+ // Rebuilds whenever BudgetState changes
+ return ListView(
+ children: state.budgets.map((b) => BudgetTile(budget: b)).toList(),
+ );
+ },
+)
+```
+
+**With listenWhen (render only on specific changes):**
+
+```dart
+BlocBuilder(
+ buildWhen: (prev, curr) => prev.budgets != curr.budgets,
+ builder: (context, state) {
+ // Only rebuilds when budgets list changes
+ return BudgetList(budgets: state.budgets);
+ },
+)
+```
+
+---
+
+### BlocListener Usage Pattern
+
+**For side effects (navigation, snackbars, dialogs):**
+
+```dart
+BlocListener(
+ listenWhen: (prev, curr) {
+ // Trigger listener only when deletion completes
+ return prev.isDeleting && !curr.isDeleting;
+ },
+ listener: (context, state) {
+ // This runs when listenWhen returns true
+ if (state.failure != const Failure.none()) {
+ showSnackBar(message: 'Delete failed', isSuccess: false);
+ } else {
+ showSnackBar(message: 'Budget deleted', isSuccess: true);
+ AppNavigator.pop(context);
+ }
+ },
+ child: BlocBuilder(
+ builder: (context, state) => Scaffold(...),
+ ),
+)
+```
+
+**Key Points**:
+- `listenWhen`: Determines when listener runs (reduce unnecessary callbacks)
+- `listener`: Side effects (no return value)
+- `child`: Usually a BlocBuilder for the actual UI
+- BlocListener runs AFTER state change is complete
+
+---
+
+## Error Handling
+
+### Failure Type Hierarchy
+
+```dart
+Failure
+├── ServerError(message) // Server returned error
+├── NetworkError() // Network unavailable
+├── CacheError(message) // Local cache error
+├── SyncError(message) // Sync failed
+├── ValidationError(message, errors) // Validation failed
+├── UnauthorizedError() // Auth expired/invalid
+├── UnknownError() // Unexpected error
+├── BadRequest(errors?, error?) // 400 Bad Request
+├── NotFound() // 404
+├── Duplicate(message) // Resource already exists
+├── Cancel() // Operation cancelled
+└── None() // No error (default)
+```
+
+### Checking for Errors
+
+```dart
+// Check if any error occurred
+if (state.failure != const Failure.none()) {
+ // Handle error
+}
+
+// Use hasError property
+if (state.failure.hasError) {
+ // Error occurred
+}
+
+// Get localized message
+showSnackBar(message: state.failure); // Automatically localizes
+```
+
+### Error State Emission Pattern
+
+```dart
+// On operation start: reset failure
+emit(state.copyWith(
+ isLoading: true,
+ failure: const Failure.none(), // Always clear previous errors
+));
+
+// On completion: emit appropriate state
+result.fold(
+ (failure) => emit(state.copyWith(
+ isLoading: false,
+ failure: failure, // Emit the failure
+ )),
+ (data) => emit(state.copyWith(
+ isLoading: false,
+ data: data,
+ failure: const Failure.none(), // Clear on success
+ )),
+);
+```
+
+### UI Error Handling Pattern
+
+```dart
+// In BlocListener - show error feedback
+BlocListener(
+ listenWhen: (prev, curr) => prev.isDeleting && !curr.isDeleting,
+ listener: (context, state) {
+ if (state.failure != const Failure.none()) {
+ showSnackBar(
+ message: state.failure, // Automatically gets customMessage
+ isSuccess: false,
+ );
+ return;
+ }
+
+ showSnackBar(
+ message: 'Deleted successfully',
+ isSuccess: true,
+ );
+ },
+)
+
+// In BlocBuilder - show loading/error states
+BlocBuilder(
+ builder: (context, state) {
+ if (state.isLoading) {
+ return const CircularProgressIndicator();
+ }
+
+ if (state.failure.hasError) {
+ return ErrorWidget(message: state.failure.customMessage);
+ }
+
+ return BudgetList(budgets: state.budgets);
+ },
+)
+```
+
+---
+
+## Best Practices Guide
+
+### 1. When to Use Await vs Fire-and-Forget
+
+| Scenario | Pattern | Example |
+|----------|---------|---------|
+| Form submission with validation | **Await** | Add budget, need success before close |
+| Loading data for display | **Fire-and-forget** | Load categories in background |
+| Confirmation dialogs | **Await dialog**, don't await action | Delete budget flow |
+| Coordinated operations | **Await Future.wait** | Refresh multiple data sources |
+| Navigation-critical operations | **Await** | Save settings, then navigate |
+| Background refresh | **Fire-and-forget** | Pull-to-refresh without blocking |
+
+### 2. BlocListener Setup for Side Effects
+
+**Always use BlocListener for:**
+- Navigation after async operation
+- Showing snackbars/dialogs
+- Triggering animations
+- Any action with no return value
+
+**Pattern:**
+
+```dart
+BlocListener(
+ // 1. Determine when to trigger
+ listenWhen: (prev, curr) {
+ return prev.isSaving && !curr.isSaving; // Operation completed
+ },
+
+ // 2. Handle side effect
+ listener: (context, state) {
+ if (state.failure.hasError) {
+ showSnackBar(message: state.failure);
+ return;
+ }
+
+ showSnackBar(message: 'Success!', isSuccess: true);
+ AppNavigator.pop(context);
+ },
+
+ // 3. Wrap the UI
+ child: BlocBuilder(
+ builder: (context, state) => Scaffold(...),
+ ),
+)
+```
+
+### 3. BlocBuilder Setup for Reactive UI
+
+**Use BlocBuilder for:**
+- Rendering lists that change
+- Conditional UI based on state
+- Loading indicators during operations
+- Any visual that depends on state
+
+**Pattern:**
+
+```dart
+BlocBuilder(
+ // Optional: only rebuild for specific changes (optimization)
+ buildWhen: (prev, curr) => prev.items != curr.items,
+
+ builder: (context, state) {
+ // Rebuilds whenever state changes (or buildWhen condition met)
+
+ // Show loading
+ if (state.isLoading) {
+ return const LoadingWidget();
+ }
+
+ // Show error (if not in BlocListener)
+ if (state.failure.hasError) {
+ return ErrorWidget(error: state.failure);
+ }
+
+ // Show data
+ return ListView(
+ children: state.items.map((item) => ItemTile(item: item)).toList(),
+ );
+ },
+)
+```
+
+### 4. Error State Handling Patterns
+
+**Pattern: Comprehensive Error Handling**
+
+```dart
+// In cubit
+Future fetchData() async {
+ emit(state.copyWith(
+ isLoading: true,
+ failure: const Failure.none(), // Always reset
+ ));
+
+ final result = await repository.getData();
+
+ result.fold(
+ (failure) => emit(state.copyWith(
+ isLoading: false,
+ failure: failure,
+ )),
+ (data) => emit(state.copyWith(
+ isLoading: false,
+ data: data,
+ failure: const Failure.none(),
+ )),
+ );
+}
+
+// In UI
+BlocBuilder(
+ builder: (context, state) {
+ if (state.isLoading && state.data == null) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ // Show data even while loading (for refresh scenarios)
+ if (state.data != null) {
+ return DataWidget(data: state.data!);
+ }
+
+ // No data and not loading = error
+ if (state.failure.hasError) {
+ return ErrorWidget(
+ message: state.failure.customMessage,
+ onRetry: () => context.read().fetchData(),
+ );
+ }
+
+ return const EmptyWidget();
+ },
+)
+```
+
+### 5. Navigation State Management Patterns
+
+**Pattern: Post-Async Navigation**
+
+```dart
+// Trigger action (don't await)
+context.read().addBudget(...);
+
+// Handle navigation in BlocListener
+BlocListener(
+ listenWhen: (prev, curr) => prev.isSaving && !curr.isSaving,
+ listener: (context, state) {
+ if (state.failure.hasError) {
+ showSnackBar(message: state.failure);
+ return; // Don't navigate
+ }
+
+ AppNavigator.pop(context); // Navigate after success
+ },
+ child: Form(...),
+)
+```
+
+**Pattern: Conditional Navigation Based on State**
+
+```dart
+// Read state after operation
+await context.read().addWallet(...);
+
+// Check current state
+final state = context.read().state;
+if (state.failure.hasError) {
+ showSnackBar(message: state.failure);
+} else {
+ AppNavigator.pop(context);
+}
+```
+
+---
+
+## Common Patterns & Anti-Patterns
+
+### Anti-Pattern 1: Not Resetting Failure State
+
+```dart
+// ✗ BAD: Old failure remains if new operation fails
+emit(state.copyWith(isLoading: true)); // Forgot failure: none()
+
+// ✓ GOOD: Always reset on new operation
+emit(state.copyWith(
+ isLoading: true,
+ failure: const Failure.none(),
+));
+```
+
+### Anti-Pattern 2: Mutating State Directly
+
+```dart
+// ✗ BAD: Violates immutability
+state.budgets.add(newBudget);
+emit(state);
+
+// ✓ GOOD: Use copyWith for immutability
+emit(state.copyWith(
+ budgets: [...state.budgets, newBudget],
+));
+```
+
+### Anti-Pattern 3: Not Awaiting Form Operations
+
+```dart
+// ✗ BAD: Form closes before operation completes
+void _onSubmit() {
+ context.read().addBudget(...);
+ Navigator.pop(context); // Happens immediately!
+}
+
+// ✓ GOOD: Let BlocListener handle navigation
+Future _onSubmit() async {
+ context.read().addBudget(...);
+ // Don't navigate here; let BlocListener do it
+}
+```
+
+### Anti-Pattern 4: Unnecessary Await on Fire-and-Forget
+
+```dart
+// ✗ INEFFICIENT: Unnecessary blocking
+void initState() {
+ super.initState();
+ Future.microtask(() async {
+ await context.read().watchBudget(id);
+ });
+}
+
+// ✓ CORRECT: Just call it, no await
+void initState() {
+ super.initState();
+ context.read().watchBudget(id);
+}
+```
+
+### Anti-Pattern 5: Not Cancelling Subscriptions
+
+```dart
+// ✗ BAD: StreamSubscription not cancelled on close
+class MyCubit extends Cubit {
+ StreamSubscription? _sub;
+
+ void listen() {
+ _sub = repository.listen().listen((_) {
+ // ...
+ });
+ }
+ // close() not overridden - memory leak!
+}
+
+// ✓ GOOD: Cancel on close
+class MyCubit extends Cubit {
+ StreamSubscription? _sub;
+
+ @override
+ Future close() {
+ _sub?.cancel();
+ return super.close();
+ }
+}
+```
+
+### Anti-Pattern 6: Loading State Blocking Data Display
+
+```dart
+// ✗ BAD: Can't see data while loading
+if (state.isLoading) return LoadingWidget();
+if (state.data != null) return DataWidget(state.data!);
+
+// ✓ GOOD: Show data during refresh
+if (state.data != null) {
+ return Stack(
+ children: [
+ DataWidget(state.data!),
+ if (state.isLoading)
+ const Opacity(opacity: 0.3, child: LoadingOverlay()),
+ ],
+ );
+}
+```
+
+### Best Practice 1: Optimistic Updates
+
+```dart
+// In cubit
+Future deleteItem(String id) async {
+ emit(state.copyWith(isDeleting: true, failure: const Failure.none()));
+
+ // 1. Optimistically update UI
+ final updated = state.items.where((i) => i.id != id).toList();
+ emit(state.copyWith(items: updated));
+
+ // 2. Make API call
+ final result = await repository.delete(id);
+
+ // 3. Rollback if failed
+ result.fold(
+ (failure) => emit(state.copyWith(
+ items: state.items, // Restore original
+ isDeleting: false,
+ failure: failure,
+ )),
+ (_) => emit(state.copyWith(
+ isDeleting: false,
+ failure: const Failure.none(),
+ )),
+ );
+}
+```
+
+### Best Practice 2: Efficient Rebuilds with buildWhen
+
+```dart
+// In UI
+BlocBuilder(
+ // Don't rebuild if only selectedBudgetProgress changed
+ buildWhen: (prev, curr) => prev.budgets != curr.budgets,
+ builder: (context, state) => BudgetList(budgets: state.budgets),
+)
+```
+
+### Best Practice 3: Composing Multiple Streams
+
+```dart
+// In cubit
+void watchBudget(String clientId) {
+ _targetsSub?.cancel();
+ _targetsSub = repository.listenToTargets(clientId).listen((either) {
+ either.fold(
+ (f) => emit(state.copyWith(failure: f)),
+ (t) => emit(state.copyWith(selectedTargets: t)),
+ );
+ });
+
+ _periodsSub?.cancel();
+ _periodsSub = repository.listenToPeriods(clientId).listen((either) {
+ either.fold(
+ (f) => emit(state.copyWith(failure: f)),
+ (p) => emit(state.copyWith(selectedPeriods: p)),
+ );
+ });
+}
+```
+
+---
+
+## Integration Checklist
+
+When implementing new cubits, ensure:
+
+- [ ] Use `@freezed` for immutable state
+- [ ] Use `@injectable` for dependency injection
+- [ ] Override `close()` to cancel subscriptions
+- [ ] Call `emit(state.copyWith(failure: const Failure.none()))` at operation start
+- [ ] Always emit appropriate loading flags (`isLoading`, `isSaving`, etc.)
+- [ ] Store error in state, don't throw exceptions
+- [ ] Use BlocListener for side effects (navigation, snackbars)
+- [ ] Use BlocBuilder for reactive UI
+- [ ] Implement `listenWhen` to filter unnecessary listener triggers
+- [ ] Implement `buildWhen` to optimize rebuilds
+- [ ] Consider optimistic updates for deletions
+- [ ] Document expected state transitions in comments
+- [ ] Test error scenarios in unit tests
+
+---
+
+## File References
+
+| File | Purpose |
+|------|---------|
+| `lib/presentation/budget/cubit/budget_cubit.dart` | Budget CRUD + progress tracking |
+| `lib/presentation/budget/cubit/budget_state.dart` | Budget state definition (freezed) |
+| `lib/presentation/category/cubit/category_cubit.dart` | Category CRUD |
+| `lib/presentation/wallets/cubit/wallet_cubit.dart` | Wallet management + selection |
+| `lib/core/error/failures/failures.dart` | Error type definitions |
+| `lib/presentation/utils/helpers.dart` | UI helpers (showSnackBar, etc.) |
+| `lib/presentation/utils/dialogs.dart` | Dialog helpers |
+
+---
+
+## Related Documentation
+
+- [Flutter Bloc Documentation](https://bloclibrary.dev/)
+- [Freezed Documentation](https://pub.dev/packages/freezed)
+- [Injectable Documentation](https://pub.dev/packages/injectable)
+- Memory: [Applied Helpers](APPLIED_HELPERS.md) - Reusable helper functions
+- Memory: [Refactoring Checklist](REFACTORING_CHECKLIST.md) - Screen refactoring guidelines
diff --git a/drift_schemas/default/drift_schema_v5.json b/drift_schemas/default/drift_schema_v5.json
new file mode 100644
index 00000000..ac82a4c1
--- /dev/null
+++ b/drift_schemas/default/drift_schema_v5.json
@@ -0,0 +1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"wallets","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const WalletTypeConverter()","dart_type_name":"WalletType"}},{"name":"balance","getter_name":"balance","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0.0')","default_client_dart":null,"dsl_features":[]},{"name":"currency","getter_name":"currency","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"stats","getter_name":"stats","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const WalletStatsConverter()","dart_type_name":"WalletStats"}},{"name":"icon","getter_name":"icon","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MediaConverter()","dart_type_name":"Media"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":1,"references":[],"type":"table","data":{"name":"parties","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"icon","getter_name":"icon","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MediaConverter()","dart_type_name":"Media"}},{"name":"type","getter_name":"type","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const PartyTypeConverter()","dart_type_name":"PartyType"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":2,"references":[],"type":"table","data":{"name":"groups","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"icon","getter_name":"icon","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MediaConverter()","dart_type_name":"Media"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":3,"references":[0,1,2],"type":"table","data":{"name":"transactions","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"amount","getter_name":"amount","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(TransactionType.values)","dart_type_name":"TransactionType"}},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"datetime","getter_name":"datetime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"party_id","getter_name":"partyId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"wallet_id","getter_name":"walletId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"group_id","getter_name":"groupId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"wallet_client_id","getter_name":"walletClientId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES wallets (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES wallets (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"wallets","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"party_client_id","getter_name":"partyClientId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES parties (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES parties (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"parties","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"group_client_id","getter_name":"groupClientId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES \"groups\" (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES \"groups\" (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"groups","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"transfer_id","getter_name":"transferId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"transfer_client_id","getter_name":"transferClientId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":4,"references":[],"type":"table","data":{"name":"categories","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"slug","getter_name":"slug","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(TransactionType.values)","dart_type_name":"TransactionType"}},{"name":"icon","getter_name":"icon","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MediaConverter()","dart_type_name":"Media"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":5,"references":[],"type":"table","data":{"name":"configs","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"key","getter_name":"key","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ConfigType.values)","dart_type_name":"ConfigType"}},{"name":"value","getter_name":"value","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const ConfigValueConverter()","dart_type_name":"dynamic"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":6,"references":[],"type":"table","data":{"name":"users","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"first_name","getter_name":"firstName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_name","getter_name":"lastName","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"phone","getter_name":"phone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"avatar","getter_name":"avatar","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":7,"references":[],"type":"table","data":{"name":"local_changes","was_declared_in_moor":false,"columns":[{"name":"entity_type","getter_name":"entityType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entity_id","getter_name":"entityId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entity_rev","getter_name":"entityRev","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted","getter_name":"deleted","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"deleted\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"deleted\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const JsonMapConverter()","dart_type_name":"Map"}},{"name":"create_at","getter_name":"createAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"concluded","getter_name":"concluded","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"concluded\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"concluded\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"concluded_moment","getter_name":"concludedMoment","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"error","getter_name":"error","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"dismissed","getter_name":"dismissed","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"dismissed\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"dismissed\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["entity_id","entity_type"]}},{"id":8,"references":[],"type":"table","data":{"name":"sync_metadata","was_declared_in_moor":false,"columns":[{"name":"entity_type","getter_name":"entityType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["entity_type"]}},{"id":9,"references":[4],"type":"table","data":{"name":"categorizables","was_declared_in_moor":false,"columns":[{"name":"categorizable_id","getter_name":"categorizableId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"categorizable_type","getter_name":"categorizableType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CategorizableType.values)","dart_type_name":"CategorizableType"}},{"name":"category_client_id","getter_name":"categoryClientId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES categories (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES categories (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"categories","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["categorizable_id","categorizable_type","category_client_id"]}},{"id":10,"references":[],"type":"table","data":{"name":"notifications","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(NotificationType.values)","dart_type_name":"NotificationType"}},{"name":"title","getter_name":"title","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"body","getter_name":"body","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const JsonMapConverter()","dart_type_name":"Map"}},{"name":"read_at","getter_name":"readAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":11,"references":[],"type":"table","data":{"name":"media_files","was_declared_in_moor":false,"columns":[{"name":"path","getter_name":"path","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"fileable_type","getter_name":"fileableType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"fileable_id","getter_name":"fileableId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"local_fileable_type","getter_name":"localFileableType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"local_fileable_id","getter_name":"localFileableId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["path"]}},{"id":12,"references":[0,3],"type":"table","data":{"name":"transfers","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"amount","getter_name":"amount","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"from_wallet_id","getter_name":"fromWalletId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"to_wallet_id","getter_name":"toWalletId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"from_wallet_client_id","getter_name":"fromWalletClientId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES wallets (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES wallets (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"wallets","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"to_wallet_client_id","getter_name":"toWalletClientId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES wallets (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES wallets (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"wallets","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"exchange_rate","getter_name":"exchangeRate","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"datetime","getter_name":"datetime","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"expense_transaction_client_id","getter_name":"expenseTransactionClientId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES transactions (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES transactions (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"transactions","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"income_transaction_client_id","getter_name":"incomeTransactionClientId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES transactions (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES transactions (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"transactions","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":13,"references":[],"type":"table","data":{"name":"budgets","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slug","getter_name":"slug","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"amount","getter_name":"amount","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringToDoubleConverter()","dart_type_name":"double"}},{"name":"currency","getter_name":"currency","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"period_type","getter_name":"periodType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BudgetPeriodType.values)","dart_type_name":"BudgetPeriodType"}},{"name":"start_date","getter_name":"startDate","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end_date","getter_name":"endDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rollover_enabled","getter_name":"rolloverEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"rollover_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"rollover_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"threshold_percent","getter_name":"thresholdPercent","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('80')","default_client_dart":null,"dsl_features":[]},{"name":"forecast_alerts_enabled","getter_name":"forecastAlertsEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"forecast_alerts_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"forecast_alerts_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"owner_type","getter_name":"ownerType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'user\\'')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"progress","getter_name":"progress","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const BudgetProgressJsonConverter()","dart_type_name":"BudgetProgressEntity"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}},{"id":14,"references":[13],"type":"table","data":{"name":"budget_targets","was_declared_in_moor":false,"columns":[{"name":"budget_client_id","getter_name":"budgetClientId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES budgets (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES budgets (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"budgets","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"target_type","getter_name":"targetType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BudgetTargetType.values)","dart_type_name":"BudgetTargetType"}},{"name":"target_client_id","getter_name":"targetClientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["budget_client_id","target_type","target_client_id"]}},{"id":15,"references":[13],"type":"table","data":{"name":"budget_period_states","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":true,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unique"]},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_id","getter_name":"clientId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"rev","getter_name":"rev","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":"const CustomExpression('\\'1\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_synced_at","getter_name":"lastSyncedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"budget_client_id","getter_name":"budgetClientId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES budgets (client_id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES budgets (client_id)"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"budgets","column":"client_id"},"initially_deferred":false,"on_update":null,"on_delete":null}}]},{"name":"period_start","getter_name":"periodStart","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"period_end","getter_name":"periodEnd","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"net_spent","getter_name":"netSpent","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0.0')","default_client_dart":null,"dsl_features":[]},{"name":"rollover_in","getter_name":"rolloverIn","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0.0')","default_client_dart":null,"dsl_features":[]},{"name":"rollover_out","getter_name":"rolloverOut","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0.0')","default_client_dart":null,"dsl_features":[]},{"name":"closed_at","getter_name":"closedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["client_id"]}}]}
\ No newline at end of file
diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart
index 72e06e85..a7607081 100644
--- a/lib/bootstrap.dart
+++ b/lib/bootstrap.dart
@@ -16,6 +16,7 @@ import 'package:trakli/core/error/crash_reporting.dart';
import 'package:trakli/core/error/crash_reporting/user_context_service.dart';
import 'package:trakli/core/error/error_handler.dart';
import 'package:trakli/core/sync/sync_database.dart';
+import 'package:trakli/data/services/budget/budget_progress_recomputer.dart';
import 'package:trakli/di/injection.dart';
import 'package:trakli/presentation/utils/globals.dart';
@@ -77,6 +78,8 @@ Future bootstrap(
await triggers.attach();
getIt.registerSingleton(triggers);
+ getIt().attachFxSelfHeal();
+
// Set up error handler with crash reporting
ErrorHandler.setCrashReportingService(crashReportingService!);
diff --git a/lib/core/module/sync_module.dart b/lib/core/module/sync_module.dart
index 14b7b764..3754fd91 100644
--- a/lib/core/module/sync_module.dart
+++ b/lib/core/module/sync_module.dart
@@ -1,6 +1,8 @@
import 'package:drift_sync_core/drift_sync_core.dart';
import 'package:injectable/injectable.dart';
import 'package:trakli/core/sync/sync_dependency_manager.dart';
+import 'package:trakli/data/sync/budget_period_state_sync_handler.dart';
+import 'package:trakli/data/sync/budget_sync_handler.dart';
import 'package:trakli/data/sync/category_sync_handler.dart';
import 'package:trakli/data/sync/config_sync_handler.dart';
import 'package:trakli/data/sync/group_sync_handler.dart';
@@ -24,6 +26,8 @@ abstract class SyncModule {
TransactionSyncHandler transactionTypeHandler,
TransferSyncHandler transferTypeHandler,
MediaSyncHandler mediaSyncHandler,
+ BudgetSyncHandler budgetTypeHandler,
+ BudgetPeriodStateSyncHandler budgetPeriodStateTypeHandler,
) {
return {
categoryTypeHandler,
@@ -35,6 +39,8 @@ abstract class SyncModule {
transactionTypeHandler,
transferTypeHandler,
mediaSyncHandler,
+ budgetTypeHandler,
+ budgetPeriodStateTypeHandler,
};
}
diff --git a/lib/core/network/interceptors/logger_interceptor.dart b/lib/core/network/interceptors/logger_interceptor.dart
index e014b947..c8972142 100644
--- a/lib/core/network/interceptors/logger_interceptor.dart
+++ b/lib/core/network/interceptors/logger_interceptor.dart
@@ -1,4 +1,7 @@
+import 'dart:convert';
+
import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
// import 'package:logger/logger.dart';
import 'package:logger/logger.dart';
@@ -50,9 +53,15 @@ class LoggerInterceptor extends Interceptor {
) {
logger.d(
'StatusCode: ${response.statusCode} \n'
- 'Headers: ${response.headers.toString()} \n'
- 'Data: ${response.data}',
- ); // Debug log
+ 'Headers: ${response.headers.toString()}',
+ );
+ final encoded = jsonEncode(response.data);
+ const chunkSize = 800;
+ for (var i = 0; i < encoded.length; i += chunkSize) {
+ final end =
+ (i + chunkSize < encoded.length) ? i + chunkSize : encoded.length;
+ debugPrint('Data[${i ~/ chunkSize}]: ${encoded.substring(i, end)}');
+ }
return super.onResponse(response, handler);
}
}
diff --git a/lib/data/budget/period_state_client_id.dart b/lib/data/budget/period_state_client_id.dart
new file mode 100644
index 00000000..5849ebb6
--- /dev/null
+++ b/lib/data/budget/period_state_client_id.dart
@@ -0,0 +1,4 @@
+/// Synthesizes a deterministic local client id for a `BudgetPeriodState`.
+///
+/// treat that case as an orphan and skip persistence.
+String periodStateClientId(int serverId) => 'bps:server-$serverId';
diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart
index 7d2021f4..de71cd40 100644
--- a/lib/data/database/app_database.dart
+++ b/lib/data/database/app_database.dart
@@ -1,38 +1,45 @@
+import 'dart:io';
+
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:drift_sync_core/drift_sync_core.dart';
-import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
+import 'package:path_provider/path_provider.dart';
+import 'package:trakli/core/utils/services/logger.dart';
+import 'package:trakli/data/database/converters/budget_progress_json_converter.dart';
import 'package:trakli/data/database/converters/media_converter.dart';
import 'package:trakli/data/database/converters/party_type_converter.dart';
+import 'package:trakli/data/database/converters/string_to_double_converter.dart';
import 'package:trakli/data/database/converters/wallet_stats_converter.dart';
import 'package:trakli/data/database/converters/wallet_type_converter.dart';
+import 'package:trakli/data/database/tables/budget_period_states.dart';
+import 'package:trakli/data/database/tables/budget_targets.dart';
+import 'package:trakli/data/database/tables/budgets.dart';
import 'package:trakli/data/database/tables/categories.dart';
+import 'package:trakli/data/database/tables/categorizables.dart';
import 'package:trakli/data/database/tables/configs.dart';
import 'package:trakli/data/database/tables/groups.dart';
import 'package:trakli/data/database/tables/local_changes.dart';
-import 'package:trakli/core/utils/services/logger.dart';
+import 'package:trakli/data/database/tables/media_files.dart';
+import 'package:trakli/data/database/tables/notifications.dart';
import 'package:trakli/data/database/tables/parties.dart';
import 'package:trakli/data/database/tables/sync_table.dart';
import 'package:trakli/data/database/tables/transactions.dart';
+import 'package:trakli/data/database/tables/transfers.dart';
import 'package:trakli/data/database/tables/users.dart';
import 'package:trakli/data/database/tables/wallets.dart';
import 'package:trakli/data/models/media.dart';
+import 'package:trakli/data/models/wallet_stats.dart';
+import 'package:trakli/domain/entities/budget_progress_entity.dart';
import 'package:trakli/domain/entities/config_entity.dart';
import 'package:trakli/domain/entities/party_entity.dart';
import 'package:trakli/presentation/utils/enums.dart';
-import 'package:trakli/data/models/wallet_stats.dart';
-import 'dart:io';
-import 'tables/sync_meta_data.dart';
-import 'package:trakli/data/database/tables/categorizables.dart';
-import 'package:trakli/data/database/tables/notifications.dart';
-import 'package:trakli/data/database/tables/media_files.dart';
-import 'package:trakli/data/database/tables/transfers.dart';
+
import 'app_database.steps.dart';
+import 'tables/sync_meta_data.dart';
part 'app_database.g.dart';
-
@DriftDatabase(tables: [
Transactions,
Parties,
@@ -47,6 +54,9 @@ part 'app_database.g.dart';
Notifications,
MediaFiles,
Transfers,
+ Budgets,
+ BudgetTargets,
+ BudgetPeriodStates,
])
class AppDatabase extends _$AppDatabase with SynchronizerDb {
final Set typeHandlers;
@@ -58,7 +68,7 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb {
super(executor ?? _openConnection());
@override
- int get schemaVersion => 4;
+ int get schemaVersion => 5;
@override
MigrationStrategy get migration {
@@ -66,7 +76,11 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb {
onCreate: (Migrator m) async {
await m.createAll();
},
- onUpgrade: _schemaUpgrade,
+ onUpgrade: (Migrator m, int from, int to) async {
+ if (from < 4) {
+ await _schemaUpgrade(m, from, 4);
+ }
+ },
);
}
@@ -270,13 +284,15 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb {
await notifications.deleteAll();
await mediaFiles.deleteAll();
await transfers.deleteAll();
+ await budgetTargets.deleteAll();
+ await budgetPeriodStates.deleteAll();
+ await budgets.deleteAll();
}
}
extension Migrations on GeneratedDatabase {
- // Extracting the `stepByStep` call into a getter ensures that you're not
- // accidentally referring to the current database schema (via a getter on the database class).
- // This ensures that each step brings the database into the correct snapshot.
+ // A getter (not a field) so each step uses its own schema snapshot, not the
+ // current one.
OnUpgrade get _schemaUpgrade => stepByStep(
from1To2: (m, schema) async {
await m.createTable(schema.notifications);
@@ -286,11 +302,17 @@ extension Migrations on GeneratedDatabase {
},
from3To4: (m, schema) async {
await m.createTable(schema.transfers);
- await m.addColumn(schema.transactions, schema.transactions.transferId);
+ await m.addColumn(
+ schema.transactions, schema.transactions.transferId);
await m.addColumn(
schema.transactions,
schema.transactions.transferClientId,
);
},
+ from4To5: (Migrator m, Schema5 schema) async {
+ await m.createTable(schema.budgets);
+ await m.createTable(schema.budgetTargets);
+ await m.createTable(schema.budgetPeriodStates);
+ },
);
}
diff --git a/lib/data/database/app_database.g.dart b/lib/data/database/app_database.g.dart
index 413c8d85..cabc908d 100644
--- a/lib/data/database/app_database.g.dart
+++ b/lib/data/database/app_database.g.dart
@@ -6488,47 +6488,2592 @@ class TransfersCompanion extends UpdateCompanion {
}
}
-abstract class _$AppDatabase extends GeneratedDatabase {
- _$AppDatabase(QueryExecutor e) : super(e);
- $AppDatabaseManager get managers => $AppDatabaseManager(this);
- late final $WalletsTable wallets = $WalletsTable(this);
- late final $PartiesTable parties = $PartiesTable(this);
- late final $GroupsTable groups = $GroupsTable(this);
- late final $TransactionsTable transactions = $TransactionsTable(this);
- late final $CategoriesTable categories = $CategoriesTable(this);
- late final $ConfigsTable configs = $ConfigsTable(this);
- late final $UsersTable users = $UsersTable(this);
- late final $LocalChangesTable localChanges = $LocalChangesTable(this);
- late final $SyncMetadataTable syncMetadata = $SyncMetadataTable(this);
- late final $CategorizablesTable categorizables = $CategorizablesTable(this);
- late final $NotificationsTable notifications = $NotificationsTable(this);
- late final $MediaFilesTable mediaFiles = $MediaFilesTable(this);
- late final $TransfersTable transfers = $TransfersTable(this);
+class $BudgetsTable extends Budgets with TableInfo<$BudgetsTable, Budget> {
@override
- Iterable> get allTables =>
- allSchemaEntities.whereType>();
+ final GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ $BudgetsTable(this.attachedDatabase, [this._alias]);
@override
- List get allSchemaEntities => [
- wallets,
- parties,
- groups,
- transactions,
- categories,
- configs,
- users,
- localChanges,
- syncMetadata,
- categorizables,
- notifications,
- mediaFiles,
- transfers
+ late final GeneratedColumn id = GeneratedColumn(
+ 'id', aliasedName, true,
+ type: DriftSqlType.int,
+ requiredDuringInsert: false,
+ defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'));
+ @override
+ late final GeneratedColumn userId = GeneratedColumn(
+ 'user_id', aliasedName, true,
+ type: DriftSqlType.int, requiredDuringInsert: false);
+ @override
+ late final GeneratedColumn clientId = GeneratedColumn(
+ 'client_id', aliasedName, false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ defaultValue: const Constant(defaultClientId));
+ @override
+ late final GeneratedColumn rev = GeneratedColumn(
+ 'rev', aliasedName, true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ defaultValue: const Constant('1'));
+ @override
+ late final GeneratedColumn createdAt = GeneratedColumn(
+ 'created_at', aliasedName, false,
+ type: DriftSqlType.dateTime,
+ requiredDuringInsert: false,
+ defaultValue: currentDateAndTime);
+ @override
+ late final GeneratedColumn updatedAt = GeneratedColumn(
+ 'updated_at', aliasedName, false,
+ type: DriftSqlType.dateTime,
+ requiredDuringInsert: false,
+ defaultValue: currentDateAndTime);
+ @override
+ late final GeneratedColumn deletedAt = GeneratedColumn(
+ 'deleted_at', aliasedName, true,
+ type: DriftSqlType.dateTime, requiredDuringInsert: false);
+ @override
+ late final GeneratedColumn lastSyncedAt = GeneratedColumn(
+ 'last_synced_at', aliasedName, true,
+ type: DriftSqlType.dateTime, requiredDuringInsert: false);
+ @override
+ late final GeneratedColumn name = GeneratedColumn(
+ 'name', aliasedName, false,
+ type: DriftSqlType.string, requiredDuringInsert: true);
+ @override
+ late final GeneratedColumn slug = GeneratedColumn(
+ 'slug', aliasedName, false,
+ type: DriftSqlType.string, requiredDuringInsert: true);
+ @override
+ late final GeneratedColumn description = GeneratedColumn(
+ 'description', aliasedName, true,
+ type: DriftSqlType.string, requiredDuringInsert: false);
+ @override
+ late final GeneratedColumnWithTypeConverter amount =
+ GeneratedColumn('amount', aliasedName, false,
+ type: DriftSqlType.string, requiredDuringInsert: true)
+ .withConverter($BudgetsTable.$converteramount);
+ @override
+ late final GeneratedColumn currency = GeneratedColumn(
+ 'currency', aliasedName, false,
+ type: DriftSqlType.string, requiredDuringInsert: true);
+ @override
+ late final GeneratedColumnWithTypeConverter
+ periodType = GeneratedColumn('period_type', aliasedName, false,
+ type: DriftSqlType.string, requiredDuringInsert: true)
+ .withConverter($BudgetsTable.$converterperiodType);
+ @override
+ late final GeneratedColumn startDate = GeneratedColumn(
+ 'start_date', aliasedName, false,
+ type: DriftSqlType.dateTime, requiredDuringInsert: true);
+ @override
+ late final GeneratedColumn endDate = GeneratedColumn(
+ 'end_date', aliasedName, true,
+ type: DriftSqlType.dateTime, requiredDuringInsert: false);
+ @override
+ late final GeneratedColumn rolloverEnabled = GeneratedColumn(
+ 'rollover_enabled', aliasedName, false,
+ type: DriftSqlType.bool,
+ requiredDuringInsert: false,
+ defaultConstraints: GeneratedColumn.constraintIsAlways(
+ 'CHECK ("rollover_enabled" IN (0, 1))'),
+ defaultValue: const Constant(false));
+ @override
+ late final GeneratedColumn thresholdPercent = GeneratedColumn(
+ 'threshold_percent', aliasedName, false,
+ type: DriftSqlType.int,
+ requiredDuringInsert: false,
+ defaultValue: const Constant(80));
+ @override
+ late final GeneratedColumn forecastAlertsEnabled =
+ GeneratedColumn('forecast_alerts_enabled', aliasedName, false,
+ type: DriftSqlType.bool,
+ requiredDuringInsert: false,
+ defaultConstraints: GeneratedColumn.constraintIsAlways(
+ 'CHECK ("forecast_alerts_enabled" IN (0, 1))'),
+ defaultValue: const Constant(false));
+ @override
+ late final GeneratedColumn isActive = GeneratedColumn(
+ 'is_active', aliasedName, false,
+ type: DriftSqlType.bool,
+ requiredDuringInsert: false,
+ defaultConstraints:
+ GeneratedColumn.constraintIsAlways('CHECK ("is_active" IN (0, 1))'),
+ defaultValue: const Constant(true));
+ @override
+ late final GeneratedColumn ownerType = GeneratedColumn(
+ 'owner_type', aliasedName, false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ defaultValue: const Constant('user'));
+ @override
+ late final GeneratedColumn ownerId = GeneratedColumn(
+ 'owner_id', aliasedName, true,
+ type: DriftSqlType.int, requiredDuringInsert: false);
+ @override
+ late final GeneratedColumnWithTypeConverter
+ progress = GeneratedColumn('progress', aliasedName, true,
+ type: DriftSqlType.string, requiredDuringInsert: false)
+ .withConverter(
+ $BudgetsTable.$converterprogressn);
+ @override
+ List get $columns => [
+ id,
+ userId,
+ clientId,
+ rev,
+ createdAt,
+ updatedAt,
+ deletedAt,
+ lastSyncedAt,
+ name,
+ slug,
+ description,
+ amount,
+ currency,
+ periodType,
+ startDate,
+ endDate,
+ rolloverEnabled,
+ thresholdPercent,
+ forecastAlertsEnabled,
+ isActive,
+ ownerType,
+ ownerId,
+ progress
];
@override
- DriftDatabaseOptions get options =>
- const DriftDatabaseOptions(storeDateTimeAsText: true);
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'budgets';
+ @override
+ Set get $primaryKey => {clientId};
+ @override
+ Budget map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return Budget(
+ id: attachedDatabase.typeMapping
+ .read(DriftSqlType.int, data['${effectivePrefix}id']),
+ userId: attachedDatabase.typeMapping
+ .read(DriftSqlType.int, data['${effectivePrefix}user_id']),
+ clientId: attachedDatabase.typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}client_id'])!,
+ rev: attachedDatabase.typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}rev']),
+ createdAt: attachedDatabase.typeMapping
+ .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
+ updatedAt: attachedDatabase.typeMapping
+ .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
+ deletedAt: attachedDatabase.typeMapping
+ .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']),
+ lastSyncedAt: attachedDatabase.typeMapping.read(
+ DriftSqlType.dateTime, data['${effectivePrefix}last_synced_at']),
+ name: attachedDatabase.typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}name'])!,
+ slug: attachedDatabase.typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}slug'])!,
+ description: attachedDatabase.typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}description']),
+ amount: $BudgetsTable.$converteramount.fromSql(attachedDatabase
+ .typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}amount'])!),
+ currency: attachedDatabase.typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}currency'])!,
+ periodType: $BudgetsTable.$converterperiodType.fromSql(attachedDatabase
+ .typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}period_type'])!),
+ startDate: attachedDatabase.typeMapping
+ .read(DriftSqlType.dateTime, data['${effectivePrefix}start_date'])!,
+ endDate: attachedDatabase.typeMapping
+ .read(DriftSqlType.dateTime, data['${effectivePrefix}end_date']),
+ rolloverEnabled: attachedDatabase.typeMapping
+ .read(DriftSqlType.bool, data['${effectivePrefix}rollover_enabled'])!,
+ thresholdPercent: attachedDatabase.typeMapping
+ .read(DriftSqlType.int, data['${effectivePrefix}threshold_percent'])!,
+ forecastAlertsEnabled: attachedDatabase.typeMapping.read(
+ DriftSqlType.bool,
+ data['${effectivePrefix}forecast_alerts_enabled'])!,
+ isActive: attachedDatabase.typeMapping
+ .read(DriftSqlType.bool, data['${effectivePrefix}is_active'])!,
+ ownerType: attachedDatabase.typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}owner_type'])!,
+ ownerId: attachedDatabase.typeMapping
+ .read(DriftSqlType.int, data['${effectivePrefix}owner_id']),
+ progress: $BudgetsTable.$converterprogressn.fromSql(attachedDatabase
+ .typeMapping
+ .read(DriftSqlType.string, data['${effectivePrefix}progress'])),
+ );
+ }
+
+ @override
+ $BudgetsTable createAlias(String alias) {
+ return $BudgetsTable(attachedDatabase, alias);
+ }
+
+ static JsonTypeConverter2 $converteramount =
+ const StringToDoubleConverter();
+ static JsonTypeConverter2
+ $converterperiodType =
+ const EnumNameConverter(BudgetPeriodType.values);
+ static JsonTypeConverter2>
+ $converterprogress = const BudgetProgressJsonConverter();
+ static JsonTypeConverter2?> $converterprogressn =
+ JsonTypeConverter2.asNullable($converterprogress);
+}
+
+class Budget extends DataClass implements Insertable {
+ final int? id;
+ final int? userId;
+ final String clientId;
+ final String? rev;
+ final DateTime createdAt;
+ final DateTime updatedAt;
+ final DateTime? deletedAt;
+ final DateTime? lastSyncedAt;
+ final String name;
+ final String slug;
+ final String? description;
+ final double amount;
+ final String currency;
+ final BudgetPeriodType periodType;
+ final DateTime startDate;
+ final DateTime? endDate;
+ final bool rolloverEnabled;
+ final int thresholdPercent;
+ final bool forecastAlertsEnabled;
+ final bool isActive;
+ final String ownerType;
+ final int? ownerId;
+ final BudgetProgressEntity? progress;
+ const Budget(
+ {this.id,
+ this.userId,
+ required this.clientId,
+ this.rev,
+ required this.createdAt,
+ required this.updatedAt,
+ this.deletedAt,
+ this.lastSyncedAt,
+ required this.name,
+ required this.slug,
+ this.description,
+ required this.amount,
+ required this.currency,
+ required this.periodType,
+ required this.startDate,
+ this.endDate,
+ required this.rolloverEnabled,
+ required this.thresholdPercent,
+ required this.forecastAlertsEnabled,
+ required this.isActive,
+ required this.ownerType,
+ this.ownerId,
+ this.progress});
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (!nullToAbsent || id != null) {
+ map['id'] = Variable(id);
+ }
+ if (!nullToAbsent || userId != null) {
+ map['user_id'] = Variable(userId);
+ }
+ map['client_id'] = Variable(clientId);
+ if (!nullToAbsent || rev != null) {
+ map['rev'] = Variable(rev);
+ }
+ map['created_at'] = Variable(createdAt);
+ map['updated_at'] = Variable(updatedAt);
+ if (!nullToAbsent || deletedAt != null) {
+ map['deleted_at'] = Variable(deletedAt);
+ }
+ if (!nullToAbsent || lastSyncedAt != null) {
+ map['last_synced_at'] = Variable(lastSyncedAt);
+ }
+ map['name'] = Variable(name);
+ map['slug'] = Variable(slug);
+ if (!nullToAbsent || description != null) {
+ map['description'] = Variable(description);
+ }
+ {
+ map['amount'] =
+ Variable($BudgetsTable.$converteramount.toSql(amount));
+ }
+ map['currency'] = Variable(currency);
+ {
+ map['period_type'] = Variable(
+ $BudgetsTable.$converterperiodType.toSql(periodType));
+ }
+ map['start_date'] = Variable(startDate);
+ if (!nullToAbsent || endDate != null) {
+ map['end_date'] = Variable(endDate);
+ }
+ map['rollover_enabled'] = Variable(rolloverEnabled);
+ map['threshold_percent'] = Variable(thresholdPercent);
+ map['forecast_alerts_enabled'] = Variable(forecastAlertsEnabled);
+ map['is_active'] = Variable(isActive);
+ map['owner_type'] = Variable(ownerType);
+ if (!nullToAbsent || ownerId != null) {
+ map['owner_id'] = Variable(ownerId);
+ }
+ if (!nullToAbsent || progress != null) {
+ map['progress'] =
+ Variable($BudgetsTable.$converterprogressn.toSql(progress));
+ }
+ return map;
+ }
+
+ BudgetsCompanion toCompanion(bool nullToAbsent) {
+ return BudgetsCompanion(
+ id: id == null && nullToAbsent ? const Value.absent() : Value(id),
+ userId:
+ userId == null && nullToAbsent ? const Value.absent() : Value(userId),
+ clientId: Value(clientId),
+ rev: rev == null && nullToAbsent ? const Value.absent() : Value(rev),
+ createdAt: Value(createdAt),
+ updatedAt: Value(updatedAt),
+ deletedAt: deletedAt == null && nullToAbsent
+ ? const Value.absent()
+ : Value(deletedAt),
+ lastSyncedAt: lastSyncedAt == null && nullToAbsent
+ ? const Value.absent()
+ : Value(lastSyncedAt),
+ name: Value(name),
+ slug: Value(slug),
+ description: description == null && nullToAbsent
+ ? const Value.absent()
+ : Value(description),
+ amount: Value(amount),
+ currency: Value(currency),
+ periodType: Value(periodType),
+ startDate: Value(startDate),
+ endDate: endDate == null && nullToAbsent
+ ? const Value.absent()
+ : Value(endDate),
+ rolloverEnabled: Value(rolloverEnabled),
+ thresholdPercent: Value(thresholdPercent),
+ forecastAlertsEnabled: Value(forecastAlertsEnabled),
+ isActive: Value(isActive),
+ ownerType: Value(ownerType),
+ ownerId: ownerId == null && nullToAbsent
+ ? const Value.absent()
+ : Value(ownerId),
+ progress: progress == null && nullToAbsent
+ ? const Value.absent()
+ : Value(progress),
+ );
+ }
+
+ factory Budget.fromJson(Map json,
+ {ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return Budget(
+ id: serializer.fromJson(json['id']),
+ userId: serializer.fromJson(json['user_id']),
+ clientId: serializer.fromJson(json['client_generated_id']),
+ rev: serializer.fromJson(json['rev']),
+ createdAt: serializer.fromJson(json['created_at']),
+ updatedAt: serializer.fromJson(json['updated_at']),
+ deletedAt: serializer.fromJson(json['deleted_at']),
+ lastSyncedAt: serializer.fromJson(json['last_synced_at']),
+ name: serializer.fromJson(json['name']),
+ slug: serializer.fromJson(json['slug']),
+ description: serializer.fromJson(json['description']),
+ amount: $BudgetsTable.$converteramount
+ .fromJson(serializer.fromJson(json['amount'])),
+ currency: serializer.fromJson(json['currency']),
+ periodType: $BudgetsTable.$converterperiodType
+ .fromJson(serializer.fromJson(json['period_type'])),
+ startDate: serializer.fromJson(json['start_date']),
+ endDate: serializer.fromJson(json['end_date']),
+ rolloverEnabled: serializer.fromJson(json['rollover_enabled']),
+ thresholdPercent: serializer.fromJson(json['threshold_percent']),
+ forecastAlertsEnabled:
+ serializer.fromJson(json['forecast_alerts_enabled']),
+ isActive: serializer.fromJson(json['is_active']),
+ ownerType: serializer.fromJson(json['owner_type']),
+ ownerId: serializer.fromJson(json['owner_id']),
+ progress: $BudgetsTable.$converterprogressn.fromJson(
+ serializer.fromJson