From c39b03cc928186dd7ba46a76a75ddf1af32f6e62 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 00:23:13 +0100 Subject: [PATCH 01/21] feat(design): Add tonal design system and shared page header Introduces a tonal palette (brand, brand-soft, income, expense, neutral, warm) wired in as theme extensions for light and dark, with a tighter shared radius scale and motion tokens. A new PageAppBar carries every content page with a context-aware leading slot: back arrow when there is a stack, brand mark that opens the drawer on root-level pages, search trigger that transforms the title into an inline field. Titles are screen-centered and rendered in brand green for presence. The bar shares the page surface with a hairline separator, so toolbar and body read as one continuous canvas. Drawer slims its rows, surfaces user identity in the header, and groups items by usage rhythm (Everyday, Organize, More) rather than alphabetic order. Empty states, tip banners, party cards, and the home wallet tile pick up the new tones and the Trakli warm accent appears in places that signal "you are here" or "this is a hint" without competing with the green primary. --- .../info_interfaces/info_interface.dart | 500 +++++++++++++----- .../utils/custom_auto_complete_search.dart | 20 +- lib/presentation/utils/custom_drawer.dart | 220 ++++++-- lib/presentation/utils/design_tokens.dart | 400 ++++++++++++++ lib/presentation/utils/education_banner.dart | 34 +- lib/presentation/utils/page_app_bar.dart | 411 ++++++++++++++ lib/presentation/utils/party_card.dart | 42 +- lib/presentation/utils/theme.dart | 40 +- lib/presentation/utils/tone_widgets.dart | 181 +++++++ lib/presentation/utils/wallet_tile.dart | 33 +- 10 files changed, 1636 insertions(+), 245 deletions(-) create mode 100644 lib/presentation/utils/design_tokens.dart create mode 100644 lib/presentation/utils/page_app_bar.dart create mode 100644 lib/presentation/utils/tone_widgets.dart diff --git a/lib/presentation/info_interfaces/info_interface.dart b/lib/presentation/info_interfaces/info_interface.dart index 441e8428..b9c63137 100644 --- a/lib/presentation/info_interfaces/info_interface.dart +++ b/lib/presentation/info_interfaces/info_interface.dart @@ -1,10 +1,19 @@ +import 'dart:math' as math; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/info_interfaces/empty_data_model.dart'; -import 'package:trakli/presentation/utils/colors.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/tone_widgets.dart'; +/// Mobile port of `OnboardingEmptyState.vue`. Wraps the prior `InfoInterface` +/// API so screens that already pass an `EmptyStateModel` keep working, but +/// the layout, surfaces, and motion follow the same tonal palette as the +/// web onboarding moment: a brand-soft surface, a glow illustration, an +/// eyebrow + title + subtitle stack, numbered steps, a primary action, and +/// a tip card. class InfoInterface extends StatelessWidget { final EmptyStateModel data; final VoidCallback? action; @@ -17,150 +26,393 @@ class InfoInterface extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, - ), - child: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircleAvatar( - radius: 30.sp, - backgroundColor: appPrimaryColor.withAlpha(30), - child: Icon( - data.icon, - size: 28.sp, - color: appPrimaryColor, - ), - ), - SizedBox(height: 20.h), - Text( - data.title.tr(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.bold, + final tones = context.tones; + final brandSoft = tones.brandSoft; + final radius = BorderRadius.circular(24.r); + + return SingleChildScrollView( + padding: EdgeInsets.all(16.r), + child: Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: brandSoft.background, + borderRadius: radius, + border: Border.all(color: tones.borderLight.withValues(alpha: 0.9)), + boxShadow: context.elevations.level1, + ), + child: Stack( + children: [ + // Backdrop bloom in the top-right corner. + Positioned( + top: -120.r, + right: -120.r, + child: IgnorePointer( + child: Container( + width: 320.r, + height: 320.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + brandSoft.accent.withValues(alpha: 0.32), + brandSoft.accent.withValues(alpha: 0.0), + ], + stops: const [0, 1], + ), + ), ), ), - SizedBox(height: 10.sp), - Text( - data.description.tr(), - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith(fontSize: 14.sp), - ), - SizedBox(height: 24.sp), - Card( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, + ), + Padding( + padding: EdgeInsets.all(24.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Illustration(icon: data.icon), + SizedBox(height: 24.h), + Eyebrow( + LocaleKeys.quickStart.tr(), + color: brandSoft.deep, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${LocaleKeys.quickStart.tr().toUpperCase()}:", - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w700, - color: appPrimaryColor, - ), - ), - SizedBox(height: 10.sp), - for (int i = 0; i < data.quickStartSteps.length; i++) - Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: _buildStep( - "${i + 1}", - data.quickStartSteps[i].tr(), - context, - ), - ), - ], + SizedBox(height: 12.h), + Text( + data.title.tr(), + style: DisplayText.level2(context), ), - ), - ), - SizedBox(height: 28.h), - SizedBox( - width: double.infinity, - height: 48.sp, - child: ElevatedButton.icon( - onPressed: action, - icon: const Icon(Icons.add), - label: Text( - data.buttonText.tr(), + SizedBox(height: 8.h), + Text( + data.description.tr(), + style: TextStyle( + fontSize: 14.sp, + height: 1.5, + color: tones.textSecondary, + ), + ), + SizedBox(height: 20.h), + _StepsList(steps: data.quickStartSteps), + SizedBox(height: 24.h), + _PrimaryAction( + label: data.buttonText.tr(), + onPressed: action, ), + if (data.tipText != null) ...[ + SizedBox(height: 16.h), + _TipCard(text: data.tipText!.tr()), + ], + ], + ), + ), + ], + ), + ), + ); + } +} + +class _Illustration extends StatelessWidget { + final IconData icon; + + const _Illustration({required this.icon}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final brandSoft = tones.brandSoft; + final size = 160.r; + + return Center( + child: SizedBox( + width: size, + height: size, + child: Stack( + alignment: Alignment.center, + children: [ + // Outer glow. + Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + brandSoft.accent.withValues(alpha: 0.32), + brandSoft.accent.withValues(alpha: 0), + ], ), ), - SizedBox(height: 20.h), - if (data.tipText != null) + ), + // Dashed orbital ring. + CustomPaint( + size: Size(size * 0.82, size * 0.82), + painter: _RingPainter( + color: brandSoft.accent.withValues(alpha: 0.55), + dashed: true, + ), + ), + // Solid inner ring. + CustomPaint( + size: Size(size * 0.6, size * 0.6), + painter: _RingPainter( + color: brandSoft.accent.withValues(alpha: 0.7), + ), + ), + // Filled center disc. + Container( + width: size * 0.44, + height: size * 0.44, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: brandSoft.accent.withValues(alpha: 0.85), + ), + ), + // Decorative orbiting dots. + _OrbitDot( + angle: -math.pi / 4, + radius: size * 0.46, + size: 8, + color: brandSoft.accent.withValues(alpha: 0.9), + ), + _OrbitDot( + angle: math.pi * 0.85, + radius: size * 0.42, + size: 6, + color: brandSoft.accent.withValues(alpha: 0.75), + ), + _OrbitDot( + angle: math.pi * 0.45, + radius: size * 0.48, + size: 7, + color: brandSoft.accent.withValues(alpha: 0.65), + ), + // Icon plate. + Container( + width: size * 0.34, + height: size * 0.34, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: tones.bgSurface, + boxShadow: context.elevations.level2, + ), + alignment: Alignment.center, + child: Icon( + icon, + size: size * 0.18, + color: brandSoft.deep, + ), + ), + ], + ), + ), + ); + } +} + +class _OrbitDot extends StatelessWidget { + final double angle; + final double radius; + final double size; + final Color color; + + const _OrbitDot({ + required this.angle, + required this.radius, + required this.size, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Transform.translate( + offset: Offset(radius * math.cos(angle), radius * math.sin(angle)), + child: Container( + width: size, + height: size, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + ), + ); + } +} + +class _RingPainter extends CustomPainter { + final Color color; + final bool dashed; + + _RingPainter({required this.color, this.dashed = false}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke; + + final rect = Offset.zero & size; + final radius = size.shortestSide / 2; + final center = rect.center; + + if (!dashed) { + canvas.drawCircle(center, radius, paint); + return; + } + + // Dashed circle: 3px dash, 4px gap (matches web stroke-dasharray="3 4"). + const dash = 3.0; + const gap = 4.0; + final circumference = 2 * math.pi * radius; + final dashCount = (circumference / (dash + gap)).floor(); + final step = (2 * math.pi) / dashCount; + final arcLength = (dash / circumference) * 2 * math.pi; + + for (var i = 0; i < dashCount; i++) { + final start = i * step; + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + start, + arcLength, + false, + paint, + ); + } + } + + @override + bool shouldRepaint(covariant _RingPainter old) => + old.color != color || old.dashed != dashed; +} + +class _StepsList extends StatelessWidget { + final List steps; + + const _StepsList({required this.steps}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var i = 0; i < steps.length; i++) + Padding( + padding: EdgeInsets.only(bottom: i == steps.length - 1 ? 0 : 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ Container( - padding: - EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + width: 28.r, + height: 28.r, decoration: BoxDecoration( - color: const Color(0xFFFFF8E1), - borderRadius: BorderRadius.circular(8.sp), - border: Border.all(color: const Color(0xFFFFE082)), + color: tones.brandSoft.accent, + borderRadius: BorderRadius.circular(8.r), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.lightbulb_outline, - color: const Color(0xFFFFA000), - size: 24.sp, - ), - SizedBox(width: 8.sp), - Expanded( - child: Text( - data.tipText!.tr(), - style: TextStyle( - fontSize: 13.sp, - color: Colors.brown[700], - height: 1.4, - ), - ), - ), - ], + alignment: Alignment.center, + child: Text( + '${i + 1}', + style: TextStyle( + color: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.w700, + ), ), ), - ], + SizedBox(width: 12.w), + Expanded( + child: Text( + steps[i].tr(), + style: TextStyle( + fontSize: 14.sp, + height: 1.45, + color: tones.textSecondary, + ), + ), + ), + ], + ), + ), + ], + ); + } +} + +class _PrimaryAction extends StatelessWidget { + final String label; + final VoidCallback? onPressed; + + const _PrimaryAction({required this.label, required this.onPressed}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final accent = tones.brandSoft.deep; + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: accent, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 16.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14.r), + ), + elevation: 0, + textStyle: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + letterSpacing: -0.1, ), ), + icon: const Icon(Icons.add, size: 20), + label: Text(label), ), ); } +} - Widget _buildStep(String number, String text, BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CircleAvatar( - radius: 12.sp, - backgroundColor: appPrimaryColor, - child: Text( - number, - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: FontWeight.bold, +class _TipCard extends StatelessWidget { + final String text; + + const _TipCard({required this.text}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + padding: EdgeInsets.all(14.r), + decoration: BoxDecoration( + color: tones.glassBg, + borderRadius: BorderRadius.circular(14.r), + border: Border.all(color: tones.borderLight), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 28.r, + height: 28.r, + decoration: BoxDecoration( + color: tones.brandSoft.accent.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8.r), + ), + alignment: Alignment.center, + child: Icon( + Icons.lightbulb_outline, + size: 16.sp, + color: tones.brandSoft.deep, ), ), - ), - SizedBox(width: 8.sp), - Expanded( - child: Text( - text, - style: Theme.of(context).textTheme.bodySmall, + SizedBox(width: 10.w), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 13.sp, + height: 1.5, + color: tones.textSecondary, + ), + ), ), - ), - ], + ], + ), ); } } diff --git a/lib/presentation/utils/custom_auto_complete_search.dart b/lib/presentation/utils/custom_auto_complete_search.dart index cbaa3ec5..e7e4fbbc 100644 --- a/lib/presentation/utils/custom_auto_complete_search.dart +++ b/lib/presentation/utils/custom_auto_complete_search.dart @@ -239,17 +239,25 @@ class _CustomAutoCompleteSearchState ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(8.r), + borderRadius: BorderRadius.circular(10.r), + borderSide: BorderSide( + color: widget.accentColor, + width: 1.5, ), - borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(10.r), + borderSide: const BorderSide( + color: Color(0xFFE2E5E9), + width: 1, + ), ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), + borderRadius: BorderRadius.circular(10.r), + borderSide: const BorderSide( + color: Color(0xFFE2E5E9), + width: 1, + ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), diff --git a/lib/presentation/utils/custom_drawer.dart b/lib/presentation/utils/custom_drawer.dart index 7343638f..2a465f42 100644 --- a/lib/presentation/utils/custom_drawer.dart +++ b/lib/presentation/utils/custom_drawer.dart @@ -6,6 +6,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:trakli/di/injection.dart'; import 'package:trakli/gen/assets.gen.dart'; +import 'package:trakli/presentation/auth/cubits/auth/auth_cubit.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/category/category_screen.dart'; import 'package:trakli/presentation/config/cubit/config_cubit.dart'; @@ -63,6 +64,9 @@ class CustomDrawer extends StatelessWidget { @override Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final authState = context.watch().state; + final user = authState.user; return Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), child: Container( @@ -70,14 +74,58 @@ class CustomDrawer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 16.sp), + SizedBox(height: 14.h), Padding( - padding: EdgeInsets.only(left: 12.w), - child: SvgPicture.asset( - Assets.images.logoGreen, - height: 44.sp, + padding: EdgeInsets.symmetric(horizontal: 14.w), + child: Row( + children: [ + SvgPicture.asset( + isDark + ? Assets.images.appLogo + : Assets.images.appLogoGreen, + height: 28.h, + ), + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + user?.fullName ?? 'Trakli', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w800, + color: Theme.of(context).colorScheme.onSurface, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (user?.email != null) ...[ + SizedBox(height: 2.h), + Text( + user!.email, + style: TextStyle( + fontSize: 11.sp, + color: Colors.grey.shade500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], ), ), + SizedBox(height: 4.h), + Divider( + height: 12, + thickness: 1, + color: Colors.grey.shade300, + ), Expanded( child: SingleChildScrollView( child: SafeArea( @@ -85,23 +133,26 @@ class CustomDrawer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _sectionLabel(context, 'EVERYDAY'), _listItem( context, onTap: () { - AppNavigator.push(context, const CategoryScreen()); + AppNavigator.push(context, const HistoryScreen()); }, - title: LocaleKeys.categories.tr(), - iconPath: Assets.images.category, - subtitle: LocaleKeys.categoryDesc.tr(), + title: LocaleKeys.transactions.tr(), + iconPath: Assets.images.refresh, + subtitle: LocaleKeys.transactionsDesc.tr(), ), _listItem( context, onTap: () { - AppNavigator.push(context, const MyGroupsScreen()); + final cubit = context.read(); + cubit.updateIndex(MainNavigationPageState.wallet); + AppNavigator.pop(context); }, - title: LocaleKeys.groups.tr(), - iconPath: Assets.images.people, - subtitle: LocaleKeys.groupsDesc.tr(), + title: LocaleKeys.wallets.tr(), + iconPath: Assets.images.wallet, + subtitle: LocaleKeys.walletsDesc.tr(), ), _listItem( context, @@ -115,23 +166,14 @@ class CustomDrawer extends StatelessWidget { _listItem( context, onTap: () { - final cubit = context.read(); - cubit.updateIndex(MainNavigationPageState.wallet); - AppNavigator.pop(context); - }, - title: LocaleKeys.wallets.tr(), - iconPath: Assets.images.wallet, - subtitle: LocaleKeys.walletsDesc.tr(), - ), - _listItem( - context, - onTap: () { - AppNavigator.push(context, const HistoryScreen()); + AppNavigator.push(context, const CategoryScreen()); }, - title: LocaleKeys.transactions.tr(), - iconPath: Assets.images.refresh, - subtitle: LocaleKeys.transactionsDesc.tr(), + title: LocaleKeys.categories.tr(), + iconPath: Assets.images.tag2, + subtitle: LocaleKeys.categoryDesc.tr(), ), + SizedBox(height: 8.h), + _sectionLabel(context, 'ORGANIZE'), _listItem( context, onTap: () { @@ -141,30 +183,52 @@ class CustomDrawer extends StatelessWidget { iconPath: Assets.images.arrowUpDown, subtitle: LocaleKeys.transfersDesc.tr(), ), - ListTile( + _listItem( + context, onTap: () { - AppNavigator.push(context, const ImportHubScreen()); + AppNavigator.push(context, const MyGroupsScreen()); }, - leading: Icon( - Icons.file_upload_outlined, - color: Theme.of(context).colorScheme.onSurface, + title: LocaleKeys.groups.tr(), + iconPath: Assets.images.people, + subtitle: LocaleKeys.groupsDesc.tr(), + ), + InkWell( + onTap: () => AppNavigator.push( + context, + const ImportHubScreen(), ), - title: Text(LocaleKeys.imports.tr()), - subtitle: Text(LocaleKeys.importsDesc.tr()), - subtitleTextStyle: - Theme.of(context).textTheme.labelSmall?.copyWith( - color: Colors.grey.shade500, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 8.h, + ), + child: Row( + children: [ + Icon( + Icons.file_upload_outlined, + size: 20.sp, + color: + Theme.of(context).colorScheme.onSurface, + ), + SizedBox(width: 14.w), + Expanded( + child: Text( + LocaleKeys.imports.tr(), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .colorScheme + .onSurface, + ), ), + ), + ], + ), + ), ), - Divider( - color: Colors.grey.shade500, - ), - _listItem( - context, - onTap: () => _launchSupportEmail(context), - title: LocaleKeys.support.tr(), - iconPath: Assets.images.support, - ), + SizedBox(height: 8.h), + _sectionLabel(context, 'MORE'), _listItem( context, onTap: () { @@ -173,6 +237,12 @@ class CustomDrawer extends StatelessWidget { title: LocaleKeys.settings.tr(), iconPath: Assets.images.setting, ), + _listItem( + context, + onTap: () => _launchSupportEmail(context), + title: LocaleKeys.support.tr(), + iconPath: Assets.images.support, + ), BlocBuilder( builder: (context, state) { if (state.showDebug == true) { @@ -212,6 +282,21 @@ class CustomDrawer extends StatelessWidget { ); } + Widget _sectionLabel(BuildContext context, String text) { + return Padding( + padding: EdgeInsets.fromLTRB(20.w, 14.h, 20.w, 4.h), + child: Text( + text, + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w800, + letterSpacing: 1.6, + color: Colors.grey.shade500, + ), + ), + ); + } + Widget _listItem( BuildContext context, { VoidCallback? onTap, @@ -219,20 +304,39 @@ class CustomDrawer extends StatelessWidget { required String iconPath, String? subtitle, }) { - return ListTile( + return InkWell( onTap: onTap, - leading: SvgPicture.asset( - iconPath, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: Row( + children: [ + SizedBox( + width: 22.r, + height: 22.r, + child: SvgPicture.asset( + iconPath, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurface, + BlendMode.srcIn, + ), + ), + ), + SizedBox(width: 14.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), - title: Text(title), - subtitle: subtitle == null ? null : Text(subtitle), - subtitleTextStyle: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Colors.grey.shade500, - ), ); } } diff --git a/lib/presentation/utils/design_tokens.dart b/lib/presentation/utils/design_tokens.dart new file mode 100644 index 00000000..3d7681bc --- /dev/null +++ b/lib/presentation/utils/design_tokens.dart @@ -0,0 +1,400 @@ +import 'package:flutter/material.dart'; + +/// Tonal surface palette mirroring the web app's `.surface--*` classes. +/// +/// Each tone supplies a background tint, an accent, a deeper variant for +/// emphasis (icons, eyebrows), and an ink color for text on top. Light and +/// dark theme values are tuned independently so glassy surfaces sit on the +/// page in both modes instead of dominating it. +@immutable +class ToneColors { + final Color background; + final Color accent; + final Color deep; + final Color ink; + + const ToneColors({ + required this.background, + required this.accent, + required this.deep, + required this.ink, + }); +} + +@immutable +class AppTones extends ThemeExtension { + final ToneColors brand; + final ToneColors brandSoft; + final ToneColors income; + final ToneColors expense; + final ToneColors neutral; + + final Color textPrimary; + final Color textSecondary; + final Color textMuted; + final Color borderLight; + final Color borderMedium; + final Color bgPage; + final Color bgSurface; + final Color bgCard; + final Color glassBg; + final Color glassBgStrong; + final Color hoverOverlay; + final Color pressOverlay; + + final Color incomeColor; + final Color expenseColor; + final Color incomeSoft; + final Color expenseSoft; + + /// Trakli secondary brand colour (warm). Used sparingly to break up the + /// green-heavy palette — month chips, decorative dots, highlight accents. + final Color accentWarm; + final Color accentWarmSoft; + + const AppTones({ + required this.brand, + required this.brandSoft, + required this.income, + required this.expense, + required this.neutral, + required this.textPrimary, + required this.textSecondary, + required this.textMuted, + required this.borderLight, + required this.borderMedium, + required this.bgPage, + required this.bgSurface, + required this.bgCard, + required this.glassBg, + required this.glassBgStrong, + required this.hoverOverlay, + required this.pressOverlay, + required this.incomeColor, + required this.expenseColor, + required this.incomeSoft, + required this.expenseSoft, + required this.accentWarm, + required this.accentWarmSoft, + }); + + static const AppTones light = AppTones( + brand: ToneColors( + background: Color(0xFFE6F2EC), + accent: Color(0xFF4AAD84), + deep: Color(0xFF047844), + ink: Color(0xFF111827), + ), + brandSoft: ToneColors( + background: Color(0xFFF1F8F3), + accent: Color(0xFF7AC4A5), + deep: Color(0xFF036A3C), + ink: Color(0xFF111827), + ), + income: ToneColors( + background: Color(0xFFE6F2EC), + accent: Color(0xFF16A34A), + deep: Color(0xFF15803D), + ink: Color(0xFF111827), + ), + expense: ToneColors( + background: Color(0xFFFDEDEF), + accent: Color(0xFFEF4F6B), + deep: Color(0xFFBE123C), + ink: Color(0xFF111827), + ), + neutral: ToneColors( + background: Color(0xFFFAFBFB), + accent: Color(0xFFC4CCC8), + deep: Color(0xFF4B5563), + ink: Color(0xFF111827), + ), + textPrimary: Color(0xFF111827), + textSecondary: Color(0xFF2D3748), + textMuted: Color(0xFF4B5563), + borderLight: Color(0xFFE2E5E9), + borderMedium: Color(0xFFCED4D8), + bgPage: Color(0xFFFAFBFC), + bgSurface: Color(0xFFFFFFFF), + bgCard: Color(0xFFF5F7F6), + glassBg: Color(0xC7FFFFFF), + glassBgStrong: Color(0xEBFFFFFF), + hoverOverlay: Color(0x0F000000), + pressOverlay: Color(0x1F000000), + incomeColor: Color(0xFF16A34A), + expenseColor: Color(0xFFEF4F6B), + incomeSoft: Color(0x1F16A34A), + expenseSoft: Color(0x1FEF4F6B), + accentWarm: Color(0xFFFF9500), + accentWarmSoft: Color(0xFFFFF1DC), + ); + + static const AppTones dark = AppTones( + brand: ToneColors( + background: Color(0x29059669), + accent: Color(0xFF6EE7B7), + deep: Color(0xFF34D399), + ink: Color(0xFFF9FAFB), + ), + brandSoft: ToneColors( + background: Color(0x1A059669), + accent: Color(0xFFA7F3D0), + deep: Color(0xFF34D399), + ink: Color(0xFFF9FAFB), + ), + income: ToneColors( + background: Color(0x244ADE80), + accent: Color(0xFF4ADE80), + deep: Color(0xFF4ADE80), + ink: Color(0xFFF9FAFB), + ), + expense: ToneColors( + background: Color(0x24FB7185), + accent: Color(0xFFFB7185), + deep: Color(0xFFFB7185), + ink: Color(0xFFF9FAFB), + ), + neutral: ToneColors( + background: Color(0x0AFFFFFF), + accent: Color(0xFF4B5563), + deep: Color(0xFFE5E7EB), + ink: Color(0xFFF9FAFB), + ), + textPrimary: Color(0xFFF9FAFB), + textSecondary: Color(0xFFE5E7EB), + textMuted: Color(0xFF9CA3AF), + borderLight: Color(0xFF374151), + borderMedium: Color(0xFF4B5563), + bgPage: Color(0xFF0F172A), + bgSurface: Color(0xFF111827), + bgCard: Color(0xFF1E293B), + glassBg: Color(0x24FFFFFF), + glassBgStrong: Color(0x42FFFFFF), + hoverOverlay: Color(0x14FFFFFF), + pressOverlay: Color(0x29FFFFFF), + incomeColor: Color(0xFF4ADE80), + expenseColor: Color(0xFFFB7185), + incomeSoft: Color(0x2E4ADE80), + expenseSoft: Color(0x2EFB7185), + accentWarm: Color(0xFFFFB252), + accentWarmSoft: Color(0x29FFB252), + ); + + ToneColors tone(AppTone tone) { + switch (tone) { + case AppTone.brand: + return brand; + case AppTone.brandSoft: + return brandSoft; + case AppTone.income: + return income; + case AppTone.expense: + return expense; + case AppTone.neutral: + return neutral; + case AppTone.warm: + return ToneColors( + background: accentWarmSoft, + accent: accentWarm, + deep: accentWarm, + ink: textPrimary, + ); + } + } + + @override + ThemeExtension copyWith() => this; + + @override + ThemeExtension lerp(ThemeExtension? other, double t) { + if (other is! AppTones) return this; + return t < 0.5 ? this : other; + } +} + +enum AppTone { brand, brandSoft, income, expense, neutral, warm } + +/// Layered elevation tokens. The web side stacks two shadows per +/// level; in Flutter we approximate with a multi-stop BoxShadow list. +@immutable +class AppElevations extends ThemeExtension { + final List level1; + final List level2; + final List level3; + final List level4; + final List level5; + + const AppElevations({ + required this.level1, + required this.level2, + required this.level3, + required this.level4, + required this.level5, + }); + + static const AppElevations light = AppElevations( + level1: [ + BoxShadow(color: Color(0x12000000), blurRadius: 2, offset: Offset(0, 1)), + BoxShadow( + color: Color(0x14000000), + blurRadius: 6, + spreadRadius: 1, + offset: Offset(0, 2), + ), + ], + level2: [ + BoxShadow(color: Color(0x08000000), blurRadius: 2, offset: Offset(0, 1)), + BoxShadow( + color: Color(0x0F000000), + blurRadius: 6, + spreadRadius: 2, + offset: Offset(0, 2), + ), + ], + level3: [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + spreadRadius: 3, + offset: Offset(0, 4), + ), + BoxShadow(color: Color(0x14000000), blurRadius: 3, offset: Offset(0, 1)), + ], + level4: [ + BoxShadow( + color: Color(0x0F000000), + blurRadius: 10, + spreadRadius: 4, + offset: Offset(0, 6), + ), + BoxShadow(color: Color(0x14000000), blurRadius: 3, offset: Offset(0, 2)), + ], + level5: [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 12, + spreadRadius: 6, + offset: Offset(0, 8), + ), + BoxShadow(color: Color(0x1A000000), blurRadius: 4, offset: Offset(0, 4)), + ], + ); + + static const AppElevations dark = AppElevations( + level1: [ + BoxShadow(color: Color(0x80000000), blurRadius: 2, offset: Offset(0, 1)), + BoxShadow( + color: Color(0x4D000000), + blurRadius: 3, + spreadRadius: 1, + offset: Offset(0, 1), + ), + ], + level2: [ + BoxShadow(color: Color(0x80000000), blurRadius: 2, offset: Offset(0, 1)), + BoxShadow( + color: Color(0x59000000), + blurRadius: 6, + spreadRadius: 2, + offset: Offset(0, 2), + ), + ], + level3: [ + BoxShadow( + color: Color(0x73000000), + blurRadius: 8, + spreadRadius: 3, + offset: Offset(0, 4), + ), + BoxShadow(color: Color(0x80000000), blurRadius: 3, offset: Offset(0, 1)), + ], + level4: [ + BoxShadow( + color: Color(0x80000000), + blurRadius: 10, + spreadRadius: 4, + offset: Offset(0, 6), + ), + BoxShadow(color: Color(0x80000000), blurRadius: 3, offset: Offset(0, 2)), + ], + level5: [ + BoxShadow( + color: Color(0x8C000000), + blurRadius: 12, + spreadRadius: 6, + offset: Offset(0, 8), + ), + BoxShadow(color: Color(0x80000000), blurRadius: 4, offset: Offset(0, 4)), + ], + ); + + @override + ThemeExtension copyWith() => this; + + @override + ThemeExtension lerp( + ThemeExtension? other, + double t, + ) { + return t < 0.5 ? this : (other ?? this); + } +} + +/// Shared corner-radius scale. Use these everywhere instead of hardcoded +/// `BorderRadius.circular(N.r)` so the system stays consistent. The scale +/// is intentionally tighter than the prior ad-hoc radii (~30% smaller) so +/// surfaces read calmer on a phone. +class AppRadii { + /// Tiny chip / inner element (4) + static const double xs = 4; + + /// Small surface (6) — leading icon squares, small badges + static const double sm = 6; + + /// Default tile / inner card (8) + static const double md = 8; + + /// Surface card (11) + static const double lg = 11; + + /// Large feature card (14) + static const double xl = 14; + + /// Hero card (17) + static const double xxl = 17; + + /// Pill / fully rounded + static const double pill = 999; +} + +/// Motion tokens, matching the web's easings + durations so +/// component-level animations feel like they came out of the same kit. +class AppMotion { + static const Duration fast = Duration(milliseconds: 150); + static const Duration base = Duration(milliseconds: 250); + static const Duration slow = Duration(milliseconds: 400); + static const Duration deliberate = Duration(milliseconds: 600); + + // Standard / emphasized easings used across the app. + static const Cubic standard = Cubic(0.2, 0, 0, 1); + static const Cubic emphasized = Cubic(0.3, 0, 0, 1); + static const Cubic decelerate = Cubic(0, 0, 0, 1); + static const Cubic accelerate = Cubic(0.3, 0, 1, 1); +} + +/// Convenience extensions so screens read like: +/// `context.tones.brand.accent` +/// `context.elevations.level2` +extension ToneContext on BuildContext { + AppTones get tones => + Theme.of(this).extension() ?? + (Theme.of(this).brightness == Brightness.dark + ? AppTones.dark + : AppTones.light); + + AppElevations get elevations => + Theme.of(this).extension() ?? + (Theme.of(this).brightness == Brightness.dark + ? AppElevations.dark + : AppElevations.light); +} diff --git a/lib/presentation/utils/education_banner.dart b/lib/presentation/utils/education_banner.dart index c2b2dab9..d3eefed0 100644 --- a/lib/presentation/utils/education_banner.dart +++ b/lib/presentation/utils/education_banner.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:trakli/presentation/utils/colors.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; class EducationBanner extends StatelessWidget { final String message; @@ -16,18 +16,15 @@ class EducationBanner extends StatelessWidget { @override Widget build(BuildContext context) { + final tones = context.tones; + final warm = tones.accentWarm; return Container( margin: EdgeInsets.only(bottom: 16.h), padding: EdgeInsets.all(12.r), decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? appPrimaryColor.withValues(alpha: 0.08) - : neutralN700.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: Theme.of(context).primaryColor.withValues(alpha: 0.2), - width: 1, - ), + color: tones.accentWarmSoft, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: warm.withValues(alpha: 0.45)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -35,23 +32,20 @@ class EducationBanner extends StatelessWidget { Container( padding: EdgeInsets.all(8.r), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withValues(alpha: 0.15), + color: warm.withValues(alpha: 0.18), shape: BoxShape.circle, ), - child: Icon( - icon, - size: 20.sp, - color: Theme.of(context).primaryColor, - ), + child: Icon(icon, size: 20.sp, color: warm), ), SizedBox(width: 12.w), Expanded( child: Text( message, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(fontSize: 12.5.sp, height: 1.4), + style: TextStyle( + fontSize: 12.5.sp, + height: 1.4, + color: tones.textPrimary, + ), ), ), if (onDismiss != null) ...[ @@ -61,7 +55,7 @@ class EducationBanner extends StatelessWidget { child: Icon( Icons.close, size: 18.sp, - color: const Color(0xFF576760), + color: tones.textMuted, ), ), ], diff --git a/lib/presentation/utils/page_app_bar.dart b/lib/presentation/utils/page_app_bar.dart new file mode 100644 index 00000000..315f589a --- /dev/null +++ b/lib/presentation/utils/page_app_bar.dart @@ -0,0 +1,411 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:trakli/gen/assets.gen.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/globals.dart'; + +/// Reusable top bar for content pages. +/// +/// Default mode: back button (optional) · title · trailing actions. +/// Search mode: when [onSearchChanged] is supplied and the leading +/// "search" action is tapped, the title slot transforms into an inline +/// search input that *uses the top-bar space*. Clearing/dismissing +/// returns to the title. Eyebrow line (optional) sits inline above the +/// title, kept small so the bar stays at a normal toolbar height. +class PageAppBar extends StatefulWidget implements PreferredSizeWidget { + final String title; + final String? eyebrow; + final List actions; + final bool showBack; + final VoidCallback? onBack; + + /// If non-null, a search icon appears on the right and tapping it + /// transforms the title into a search input. The field calls this on + /// every keystroke and on clear. + final ValueChanged? onSearchChanged; + final String searchHint; + + /// Custom leading widget. When provided, [showBack] is ignored. + /// Use this on the home dashboard to show a drawer trigger instead of + /// the default back arrow. + final Widget? leading; + + /// Optional widget replacing the title text (eg. brand logo). + final Widget? titleWidget; + + const PageAppBar({ + super.key, + required this.title, + this.eyebrow, + this.actions = const [], + this.showBack = true, + this.onBack, + this.onSearchChanged, + this.searchHint = 'Search', + this.leading, + this.titleWidget, + }); + + @override + Size get preferredSize => Size.fromHeight(58.h); + + @override + State createState() => _PageAppBarState(); +} + +class _PageAppBarState extends State { + bool _searching = false; + final TextEditingController _controller = TextEditingController(); + final FocusNode _focus = FocusNode(); + + @override + void dispose() { + _controller.dispose(); + _focus.dispose(); + super.dispose(); + } + + void _openSearch() { + setState(() => _searching = true); + WidgetsBinding.instance.addPostFrameCallback((_) => _focus.requestFocus()); + } + + void _closeSearch() { + _controller.clear(); + widget.onSearchChanged?.call(''); + setState(() => _searching = false); + } + + + @override + Widget build(BuildContext context) { + final tones = context.tones; + + // (the calmer green) mixed at 13% over bgPage — readable as green on + // most displays while still gentle on the eyes. + return Material( + color: tones.bgPage, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: tones.borderLight, + width: 1, + ), + ), + ), + child: SafeArea( + bottom: false, + child: SizedBox( + height: widget.preferredSize.height, + child: Padding( + padding: EdgeInsets.only(left: 8.w, right: 8.w), + child: _searching + ? Row( + children: [ + _BarIconButton( + icon: Icons.arrow_back, + onTap: _closeSearch, + ), + SizedBox(width: 8.w), + Expanded( + child: _SearchField( + controller: _controller, + focusNode: _focus, + hint: widget.searchHint, + onChanged: widget.onSearchChanged!, + onClear: () { + _controller.clear(); + widget.onSearchChanged?.call(''); + }, + ), + ), + ], + ) + : Stack( + alignment: Alignment.center, + children: [ + // Centered title floats above the row so it lands + // at the optical screen center, not the centre of + // the row's remaining space (which is shifted by + // unequal leading and trailing widths). + Padding( + padding: EdgeInsets.symmetric(horizontal: 72.w), + child: widget.titleWidget ?? + _TitleBlock( + title: widget.title, + eyebrow: widget.eyebrow, + ), + ), + Row( + children: [ + if (widget.leading != null) + widget.leading! + else if (widget.showBack) + _BarIconButton( + icon: Icons.arrow_back, + onTap: widget.onBack ?? + () => Navigator.of(context).maybePop(), + ) + else + const _BrandMark(), + const Spacer(), + if (widget.onSearchChanged != null) + _BarIconButton( + icon: Icons.search, + onTap: _openSearch, + ), + ...widget.actions, + ], + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _TitleBlock extends StatelessWidget { + final String title; + final String? eyebrow; + + const _TitleBlock({ + required this.title, + this.eyebrow, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (eyebrow != null) + Text( + eyebrow!.toUpperCase(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 9.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.brand.deep, + ), + ), + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w800, + color: tones.brand.deep, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + } +} + +class _SearchField extends StatelessWidget { + final TextEditingController controller; + final FocusNode focusNode; + final String hint; + final ValueChanged onChanged; + final VoidCallback onClear; + + const _SearchField({ + required this.controller, + required this.focusNode, + required this.hint, + required this.onChanged, + required this.onClear, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return TextField( + controller: controller, + focusNode: focusNode, + autofocus: true, + onChanged: onChanged, + textInputAction: TextInputAction.search, + style: TextStyle( + fontSize: 16.sp, + color: tones.textPrimary, + fontWeight: FontWeight.w500, + ), + cursorColor: tones.accentWarm, + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + isCollapsed: true, + contentPadding: EdgeInsets.symmetric(vertical: 12.h), + hintText: hint, + hintStyle: TextStyle( + fontSize: 16.sp, + color: tones.textMuted, + fontWeight: FontWeight.w500, + ), + suffixIcon: ValueListenableBuilder( + valueListenable: controller, + builder: (_, value, __) { + if (value.text.isEmpty) return const SizedBox.shrink(); + return IconButton( + onPressed: onClear, + icon: Icon(Icons.close, size: 18.sp, color: tones.textMuted), + padding: EdgeInsets.zero, + constraints: BoxConstraints( + minWidth: 32.r, + minHeight: 32.r, + ), + ); + }, + ), + ), + ); + } +} + +/// Compact brand mark shown at the leading slot of bottom-nav pages +/// (no back, no custom leading), so the toolbar isn't visually unbalanced +/// with an empty top-left. Matches the Gmail/Drive convention of using +/// the app's brand logo as the root-page anchor. +class _BrandMark extends StatelessWidget { + const _BrandMark(); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final mark = SizedBox( + width: 40.r, + height: 40.r, + child: Center( + child: SvgPicture.asset( + isDark ? Assets.images.appLogo : Assets.images.appLogoGreen, + width: 22.w, + height: 28.h, + ), + ), + ); + + return Material( + color: Colors.transparent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => scaffoldKey.currentState?.openDrawer(), + child: mark, + ), + ); + } +} + +class _BarIconButton extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + + const _BarIconButton({required this.icon, required this.onTap}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Material( + color: Colors.transparent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: onTap, + child: Container( + width: 40.r, + height: 40.r, + alignment: Alignment.center, + child: Icon(icon, size: 22.sp, color: tones.brand.deep), + ), + ), + ); + } +} + +/// Action button used in PageAppBar.actions. Renders as either a circular +/// icon (default) or a brand-filled pill (primary). With [label], the pill +/// shows text + icon — use for destination chips like "Reports". +class PageAppBarAction extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + final String? tooltip; + final bool primary; + final String? label; + + const PageAppBarAction({ + super.key, + required this.icon, + required this.onTap, + this.tooltip, + this.primary = false, + this.label, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final hasLabel = label != null; + final bg = primary || hasLabel ? tones.brand.deep : Colors.transparent; + final fg = primary || hasLabel ? Colors.white : tones.textPrimary; + + final shape = (primary || hasLabel) + ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)) + : const CircleBorder(); + + final child = hasLabel + ? Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: fg, size: 16.sp), + SizedBox(width: 6.w), + Text( + label!, + style: TextStyle( + color: fg, + fontWeight: FontWeight.w700, + fontSize: 12.sp, + letterSpacing: 0.1, + ), + ), + ], + ), + ) + : SizedBox( + width: 40.r, + height: 40.r, + child: Center( + child: Icon(icon, color: fg, size: 20.sp), + ), + ); + + final widget = SizedBox( + height: 40.r, + child: Material( + color: bg, + shape: shape, + child: InkWell( + customBorder: shape, + onTap: onTap, + child: child, + ), + ), + ); + return tooltip != null ? Tooltip(message: tooltip!, child: widget) : widget; + } +} diff --git a/lib/presentation/utils/party_card.dart b/lib/presentation/utils/party_card.dart index 103b0d29..5a68d89d 100644 --- a/lib/presentation/utils/party_card.dart +++ b/lib/presentation/utils/party_card.dart @@ -9,8 +9,10 @@ import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/parties/add_party_screen.dart'; import 'package:trakli/presentation/parties/cubit/party_cubit.dart'; +import 'package:trakli/presentation/parties/party_detail_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/colors.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/helpers.dart'; @@ -30,13 +32,15 @@ class PartyCard extends StatelessWidget { double get _netBalance => receivedAmount - spentAmount; + AppTone _toneForBalance() { + if (_netBalance > 0) return AppTone.income; + if (_netBalance < 0) return AppTone.expense; + return AppTone.brand; + } + Color _getAvatarColor(BuildContext context) { - if (_netBalance > 0) { - return const Color(0xFF22C55E); - } else if (_netBalance < 0) { - return const Color(0xFFEF4444); - } - return Theme.of(context).primaryColor; + final palette = context.tones.tone(_toneForBalance()); + return palette.deep; } void _handleEdit(BuildContext context) { @@ -121,8 +125,27 @@ class PartyCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - child: Stack( + final tones = context.tones; + return Material( + color: Colors.transparent, + child: Ink( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: InkWell( + borderRadius: BorderRadius.circular(16.r), + onTap: () => AppNavigator.push( + context, + PartyDetailScreen(party: party), + ), + splashColor: tones.pressOverlay, + highlightColor: tones.hoverOverlay, + child: ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: Stack( children: [ SizedBox( width: double.infinity, @@ -322,6 +345,9 @@ class PartyCard extends StatelessWidget { ), ), ], + ), + ), + ), ), ); } diff --git a/lib/presentation/utils/theme.dart b/lib/presentation/utils/theme.dart index acc8950d..3fcee283 100644 --- a/lib/presentation/utils/theme.dart +++ b/lib/presentation/utils/theme.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/presentation/utils/colors.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; final lightTheme = ThemeData( primaryColor: const Color(0xFF047844), @@ -9,6 +10,7 @@ final lightTheme = ThemeData( hintColor: appYellow, scaffoldBackgroundColor: const Color(0xFFEBEDEC), useMaterial3: true, + extensions: const [AppTones.light, AppElevations.light], colorScheme: ColorScheme.light( surface: Colors.white, onSurface: neutralN900, @@ -107,28 +109,31 @@ final lightTheme = ThemeData( ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: const Color(0xFFF5F6F7), + fillColor: Colors.white, contentPadding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 12.h, + horizontal: 14.w, + vertical: 14.h, ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: const BorderSide( - color: Colors.transparent, - ), + hintStyle: TextStyle( + color: const Color(0xFF6B7280), + fontSize: 14.sp, ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: const BorderSide( - color: Color(0xFF047844), - ), + labelStyle: TextStyle( + color: const Color(0xFF374151), + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.r), + borderSide: const BorderSide(color: Color(0xFFE2E5E9), width: 1), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide( - color: Colors.transparent, - ), + borderRadius: BorderRadius.circular(10.r), + borderSide: const BorderSide(color: Color(0xFFE2E5E9), width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.r), + borderSide: const BorderSide(color: Color(0xFF047844), width: 1.5), ), floatingLabelStyle: TextStyle( color: appPrimaryColor, @@ -200,6 +205,7 @@ final darkTheme = ThemeData( hintColor: appYellow, scaffoldBackgroundColor: neutralN900, useMaterial3: true, + extensions: const [AppTones.dark, AppElevations.dark], colorScheme: ColorScheme.dark( surface: neutralN700, onSurface: Colors.white, diff --git a/lib/presentation/utils/tone_widgets.dart b/lib/presentation/utils/tone_widgets.dart new file mode 100644 index 00000000..19da0236 --- /dev/null +++ b/lib/presentation/utils/tone_widgets.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Flutter port of the web app's `.tone-card`: a rounded surface that picks +/// up the active tonal palette, has a hairline border, and rests on an +/// elevation-1 shadow. Hover/press lift is handled by the parent (Inkwell) +/// since cards in the mobile app are often interactive. +class ToneCard extends StatelessWidget { + final AppTone tone; + final Widget child; + final EdgeInsetsGeometry padding; + final BorderRadiusGeometry? borderRadius; + final List? boxShadow; + final VoidCallback? onTap; + final Color? overrideBackground; + final BorderSide? overrideBorder; + + const ToneCard({ + super.key, + this.tone = AppTone.neutral, + required this.child, + this.padding = const EdgeInsets.all(20), + this.borderRadius, + this.boxShadow, + this.onTap, + this.overrideBackground, + this.overrideBorder, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + final radius = borderRadius ?? BorderRadius.circular(18.r); + final border = overrideBorder ?? + BorderSide(color: tones.borderLight.withValues(alpha: 0.9), width: 1); + final shadow = boxShadow ?? context.elevations.level1; + + final decoration = BoxDecoration( + color: overrideBackground ?? palette.background, + borderRadius: radius, + border: Border.fromBorderSide(border), + boxShadow: shadow, + ); + + final content = Padding(padding: padding, child: child); + + if (onTap == null) { + return DecoratedBox(decoration: decoration, child: content); + } + + return Material( + color: Colors.transparent, + child: Ink( + decoration: decoration, + child: InkWell( + borderRadius: radius is BorderRadius ? radius : BorderRadius.circular(18.r), + onTap: onTap, + splashColor: tones.pressOverlay, + highlightColor: tones.hoverOverlay, + child: content, + ), + ), + ); + } +} + +/// Tiny all-caps tracking-wide label used as a section eyebrow. +class Eyebrow extends StatelessWidget { + final String text; + final Color? color; + + const Eyebrow(this.text, {super.key, this.color}); + + @override + Widget build(BuildContext context) { + return Text( + text.toUpperCase(), + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.6, + color: color ?? context.tones.textMuted, + ), + ); + } +} + +/// Pill-shaped glass surface used for trailing badges (eg. "Personal" tag). +class GlassPill extends StatelessWidget { + final Widget child; + final EdgeInsetsGeometry padding; + + const GlassPill({ + super.key, + required this.child, + this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + padding: padding, + decoration: BoxDecoration( + color: tones.glassBg, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: DefaultTextStyle( + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: tones.textPrimary, + ), + child: child, + ), + ); + } +} + +/// Display heading styles. Mirrors `.display-1/-2/-3` from web. +class DisplayText { + static TextStyle level1(BuildContext context) => TextStyle( + fontWeight: FontWeight.w700, + fontSize: 32.sp, + height: 1.02, + letterSpacing: -0.9, + color: context.tones.textPrimary, + ); + + static TextStyle level2(BuildContext context) => TextStyle( + fontWeight: FontWeight.w700, + fontSize: 22.sp, + height: 1.05, + letterSpacing: -0.55, + color: context.tones.textPrimary, + ); + + static TextStyle level3(BuildContext context) => TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18.sp, + height: 1.1, + letterSpacing: -0.27, + color: context.tones.textPrimary, + ); +} + +/// A small square icon chip with a glassy background. Used inside stats +/// strips so each tone has a visual anchor before its label. +class ToneIconBadge extends StatelessWidget { + final IconData icon; + final AppTone tone; + final double size; + + const ToneIconBadge({ + super.key, + required this.icon, + this.tone = AppTone.brand, + this.size = 26, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + return Container( + width: size.r, + height: size.r, + decoration: BoxDecoration( + color: tones.glassBg, + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: tones.borderLight), + ), + alignment: Alignment.center, + child: Icon(icon, size: (size * 0.55).sp, color: palette.deep), + ); + } +} diff --git a/lib/presentation/utils/wallet_tile.dart b/lib/presentation/utils/wallet_tile.dart index 88b7099b..409ba6a9 100644 --- a/lib/presentation/utils/wallet_tile.dart +++ b/lib/presentation/utils/wallet_tile.dart @@ -13,6 +13,7 @@ import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/transfers/wallet_transfer_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/colors.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/helpers.dart'; @@ -113,20 +114,28 @@ class WalletTile extends StatelessWidget { vertical: 2.h, ), decoration: BoxDecoration( - color: Colors.white.withAlpha(51), + color: context.tones.accentWarm, borderRadius: BorderRadius.circular(6.r), - border: Border.all( - color: Colors.white, - width: 1, - ), ), - child: Text( - LocaleKeys.defaultWallet.tr(), - style: TextStyle( - fontSize: 11.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.star_rounded, + size: 12.sp, + color: Colors.white, + ), + SizedBox(width: 3.w), + Text( + LocaleKeys.defaultWallet.tr(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + color: Colors.white, + letterSpacing: 0.2, + ), + ), + ], ), ), ], From 97e33bcfc348829a3c49763feab2f0cbcafe40cf Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 00:24:46 +0100 Subject: [PATCH 02/21] enh(entities): Reshape parties, wallets, categories, groups as a list pattern All four entity surfaces follow one rhythm now: a slim summary chip up top, optional filter pills below, then a single rounded surface card holding compact tile rows with hairline dividers. Tapping a row opens a dedicated detail screen that shows hero + Received/Spent/Net totals + a six-month activity chart + the recent transactions for that entity. Edit and delete are demoted to the row overflow menu and the detail screen's app bar. Sorting now leads with whatever the user actually uses (most active first, default wallet pinned to the top), and search lives in the top bar via the search icon transforming the title into an inline input. Add screens (party, wallet, category, group) drop the old saturated green custom app bar in favour of the shared PageAppBar so they look like they belong to the same app. --- .../category/add_category_screen.dart | 11 +- .../category/category_detail_screen.dart | 509 ++++++++++++++++ .../category/category_screen.dart | 423 +++++++------ .../category/widgets/category_list_tile.dart | 151 +++++ lib/presentation/groups/add_group_screen.dart | 10 +- .../groups/group_detail_screen.dart | 502 +++++++++++++++ lib/presentation/groups/my_groups_screen.dart | 199 ++++-- .../groups/widgets/group_list_tile.dart | 150 +++++ .../parties/add_party_screen.dart | 10 +- .../parties/party_detail_screen.dart | 570 ++++++++++++++++++ lib/presentation/parties/party_screen.dart | 426 ++++++++++--- .../parties/widgets/parties_stats_strip.dart | 248 ++++++++ .../parties/widgets/parties_summary.dart | 198 ++++++ .../parties/widgets/party_list_tile.dart | 234 +++++++ .../wallets/add_wallet_screen.dart | 10 +- .../wallets/wallet_detail_screen.dart | 538 +++++++++++++++++ lib/presentation/wallets/wallet_screen.dart | 462 +++++++++++--- .../wallets/widgets/wallet_list_tile.dart | 235 ++++++++ .../wallets/widgets/wallets_stats_strip.dart | 295 +++++++++ .../wallets/widgets/wallets_summary.dart | 199 ++++++ 20 files changed, 4924 insertions(+), 456 deletions(-) create mode 100644 lib/presentation/category/category_detail_screen.dart create mode 100644 lib/presentation/category/widgets/category_list_tile.dart create mode 100644 lib/presentation/groups/group_detail_screen.dart create mode 100644 lib/presentation/groups/widgets/group_list_tile.dart create mode 100644 lib/presentation/parties/party_detail_screen.dart create mode 100644 lib/presentation/parties/widgets/parties_stats_strip.dart create mode 100644 lib/presentation/parties/widgets/parties_summary.dart create mode 100644 lib/presentation/parties/widgets/party_list_tile.dart create mode 100644 lib/presentation/wallets/wallet_detail_screen.dart create mode 100644 lib/presentation/wallets/widgets/wallet_list_tile.dart create mode 100644 lib/presentation/wallets/widgets/wallets_stats_strip.dart create mode 100644 lib/presentation/wallets/widgets/wallets_summary.dart diff --git a/lib/presentation/category/add_category_screen.dart b/lib/presentation/category/add_category_screen.dart index bb294647..f133a1b5 100644 --- a/lib/presentation/category/add_category_screen.dart +++ b/lib/presentation/category/add_category_screen.dart @@ -5,9 +5,7 @@ import 'package:trakli/domain/entities/category_entity.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/category/cubit/category_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/colors.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/forms/add_category_form.dart'; import 'package:trakli/presentation/utils/helpers.dart'; @@ -36,13 +34,10 @@ class _AddCategoryScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - leading: const CustomBackButton(), - titleText: widget.category != null + appBar: PageAppBar( + title: widget.category != null ? LocaleKeys.editCategory.tr() : LocaleKeys.addCategory.tr(), - backgroundColor: appPrimaryColor, - headerTextColor: Colors.white, ), body: BlocListener( listenWhen: (previous, current) => diff --git a/lib/presentation/category/category_detail_screen.dart b/lib/presentation/category/category_detail_screen.dart new file mode 100644 index 00000000..b0a7a239 --- /dev/null +++ b/lib/presentation/category/category_detail_screen.dart @@ -0,0 +1,509 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/category_entity.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/category/add_category_screen.dart'; +import 'package:trakli/presentation/category/cubit/category_cubit.dart'; +import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/helpers.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; +import 'package:trakli/presentation/utils/transaction_tile.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +class CategoryDetailScreen extends StatelessWidget { + final CategoryEntity category; + + const CategoryDetailScreen({super.key, required this.category}); + + AppTone get _tone => category.type == TransactionType.income + ? AppTone.income + : AppTone.expense; + + List _matching( + List txns, + ) { + return txns + .where((t) => t.categories.any((c) => c.clientId == category.clientId)) + .toList() + ..sort((a, b) => + b.transaction.datetime.compareTo(a.transaction.datetime)); + } + + ({double total, int count, double avg, double biggest}) _totals( + List matches, + ) { + if (matches.isEmpty) { + return (total: 0, count: 0, avg: 0, biggest: 0); + } + double total = 0; + double biggest = 0; + for (final t in matches) { + final amt = t.transaction.amount; + total += amt; + if (amt > biggest) biggest = amt; + } + return ( + total: total, + count: matches.length, + avg: total / matches.length, + biggest: biggest, + ); + } + + List<_MonthBucket> _monthly(List matches) { + final now = DateTime.now(); + final buckets = {}; + for (var i = 5; i >= 0; i--) { + final m = DateTime(now.year, now.month - i, 1); + buckets[m] = _MonthBucket(month: m); + } + for (final t in matches) { + final key = DateTime( + t.transaction.datetime.year, t.transaction.datetime.month, 1); + final bucket = buckets[key]; + if (bucket == null) continue; + bucket.amount += t.transaction.amount; + } + return buckets.values.toList(); + } + + void _confirmDelete(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + dialogType: DialogType.negative, + title: LocaleKeys.deleteCategory.tr(), + subTitle: LocaleKeys.deleteCategoryConfirm + .tr(namedArgs: {'name': category.name}), + mainAction: () { + context.read().deleteCategory(category.clientId); + AppNavigator.pop(context); + AppNavigator.pop(context); + }, + secondaryAction: () => AppNavigator.pop(context), + mainActionText: LocaleKeys.delete.tr(), + secondaryActionText: LocaleKeys.cancel.tr(), + ), + ); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final tone = _tone; + + return BlocBuilder( + builder: (context, state) { + final matches = _matching(state.transactions); + final totals = _totals(matches); + final monthly = _monthly(matches); + + return Scaffold( + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: category.name, + actions: [ + PageAppBarAction( + icon: Icons.edit_outlined, + onTap: () => AppNavigator.push( + context, + AddCategoryScreen( + category: category, + accentColor: category.type == TransactionType.income + ? tones.incomeColor + : tones.expenseColor, + type: category.type, + ), + ), + ), + PageAppBarAction( + icon: Icons.delete_outline, + onTap: () => _confirmDelete(context), + ), + ], + ), + body: ListView( + padding: + EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + children: [ + _HeaderRow(category: category, tone: tone), + SizedBox(height: 14.h), + _TotalsCard(totals: totals, tone: tone), + SizedBox(height: 16.h), + _ActivityCard(monthly: monthly, tone: tone), + SizedBox(height: 16.h), + _RecentSection(matches: matches, tone: tone), + SizedBox(height: 24.h), + ], + ), + ); + }, + ); + } +} + +class _MonthBucket { + final DateTime month; + double amount = 0; + _MonthBucket({required this.month}); +} + +class _HeaderRow extends StatelessWidget { + final CategoryEntity category; + final AppTone tone; + const _HeaderRow({required this.category, required this.tone}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 6.h), + child: Row( + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + alignment: Alignment.center, + child: ImageWidget( + mediaEntity: category.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: Icons.label_outline, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + category.name, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 2.h), + Text( + category.type == TransactionType.income + ? LocaleKeys.transactionIncome.tr() + : LocaleKeys.transactionExpense.tr(), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: palette.deep, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _TotalsCard extends StatelessWidget { + final ({double total, int count, double avg, double biggest}) totals; + final AppTone tone; + + const _TotalsCard({required this.totals, required this.tone}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 14.h), + child: Row( + children: [ + Expanded( + child: _TotalCell( + label: 'Total', + value: CurrencyFormater.formatAmountWithSymbol( + context, + totals.total, + compact: true, + ), + color: palette.deep, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Avg', + value: CurrencyFormater.formatAmountWithSymbol( + context, + totals.avg, + compact: true, + ), + color: tones.textPrimary, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Largest', + value: CurrencyFormater.formatAmountWithSymbol( + context, + totals.biggest, + compact: true, + ), + color: tones.textPrimary, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Count', + value: '${totals.count}', + color: tones.textPrimary, + ), + ), + ], + ), + ); + } +} + +class _TotalCell extends StatelessWidget { + final String label; + final String value; + final Color color; + + const _TotalCell({ + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label.toUpperCase(), + style: TextStyle( + fontSize: 9.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + SizedBox(height: 4.h), + Text( + value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w700, + color: color, + letterSpacing: -0.2, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ); + } +} + +class _ActivityCard extends StatelessWidget { + final List<_MonthBucket> monthly; + final AppTone tone; + + const _ActivityCard({required this.monthly, required this.tone}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + final hasData = monthly.any((m) => m.amount > 0); + + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.all(16.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'LAST 6 MONTHS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + SizedBox(height: 4.h), + Text( + 'Activity', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + ), + SizedBox(height: 12.h), + if (!hasData) + Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Center( + child: Text( + 'No activity in the last 6 months.', + style: + TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + SizedBox( + height: 160.h, + child: SfCartesianChart( + margin: EdgeInsets.zero, + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + axisLine: AxisLine(width: 0.5, color: tones.borderLight), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + primaryYAxis: NumericAxis( + axisLine: const AxisLine(width: 0), + majorTickLines: const MajorTickLines(size: 0), + majorGridLines: MajorGridLines( + width: 0.5, + color: tones.borderLight.withValues(alpha: 0.6), + dashArray: const [4, 4], + ), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + ColumnSeries<_MonthBucket, String>( + name: 'Amount', + dataSource: monthly, + xValueMapper: (m, _) => _monthLabel(m.month), + yValueMapper: (m, _) => m.amount, + color: palette.deep, + width: 0.55, + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + ], + ), + ); + } + + static String _monthLabel(DateTime d) { + const labels = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', + ]; + return labels[d.month - 1]; + } +} + +class _RecentSection extends StatelessWidget { + final List matches; + final AppTone tone; + + const _RecentSection({required this.matches, required this.tone}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final recent = matches.take(10).toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + Text( + 'RECENT TRANSACTIONS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + const Spacer(), + Text( + '${recent.length} of ${matches.length}', + style: TextStyle(fontSize: 11.sp, color: tones.textMuted), + ), + ], + ), + ), + SizedBox(height: 8.h), + if (recent.isEmpty) + Container( + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + ), + child: Center( + child: Text( + 'No transactions in this category yet.', + style: TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + for (final txn in recent) + Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: TransactionTile( + transaction: txn, + accentColor: tone == AppTone.income + ? tones.incomeColor + : tones.expenseColor, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/category/category_screen.dart b/lib/presentation/category/category_screen.dart index 843a9073..0f6769c6 100644 --- a/lib/presentation/category/category_screen.dart +++ b/lib/presentation/category/category_screen.dart @@ -2,19 +2,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:trakli/domain/entities/category_entity.dart'; -import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/category/add_category_screen.dart'; +import 'package:trakli/presentation/category/category_detail_screen.dart'; import 'package:trakli/presentation/category/cubit/category_cubit.dart'; +import 'package:trakli/presentation/category/widgets/category_list_tile.dart'; import 'package:trakli/presentation/info_interfaces/data.dart'; import 'package:trakli/presentation/info_interfaces/info_interface.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/category_tile.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class CategoryScreen extends StatefulWidget { const CategoryScreen({super.key}); @@ -23,70 +22,38 @@ class CategoryScreen extends StatefulWidget { State createState() => _CategoryScreenState(); } -class _CategoryScreenState extends State - with SingleTickerProviderStateMixin { - late TabController tabController; +class _CategoryScreenState extends State { + TransactionType _type = TransactionType.income; + String _query = ''; - @override - void initState() { - super.initState(); - tabController = TabController(length: 2, vsync: this); - tabController.addListener(() { - setState(() {}); - }); - } - - @override - void dispose() { - tabController.dispose(); - super.dispose(); - } - - void addAction() { + void _add() { AppNavigator.push( context, AddCategoryScreen( - accentColor: (tabController.index == 0) - ? Theme.of(context).primaryColor - : const Color(0xFFEB5757), - type: (tabController.index == 0) - ? TransactionType.income - : TransactionType.expense, + accentColor: _type == TransactionType.income + ? context.tones.incomeColor + : context.tones.expenseColor, + type: _type, ), ); } @override Widget build(BuildContext context) { + final tones = context.tones; + return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.categories.tr(), - headerTextColor: const Color(0xFFEBEDEC), + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: LocaleKeys.categories.tr(), + onSearchChanged: (v) => setState(() => _query = v), + searchHint: 'Search categories', actions: [ - InkWell( - onTap: () { - addAction(); - }, - child: Container( - width: 40.w, - height: 40.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: Theme.of(context).scaffoldBackgroundColor, - ), - padding: EdgeInsets.all(8.r), - child: Center( - child: Icon( - Icons.add, - size: 24.r, - color: Theme.of(context).primaryColor, - ), - ), - ), + PageAppBarAction( + icon: Icons.add, + onTap: _add, + primary: true, ), - SizedBox(width: 16.w), ], ), body: BlocBuilder( @@ -95,144 +62,242 @@ class _CategoryScreenState extends State return const Center(child: CircularProgressIndicator()); } + final all = state.categories; + final q = _query.trim().toLowerCase(); + final filtered = all.where((c) { + if (c.type != _type) return false; + if (q.isEmpty) return true; + return '${c.name} ${c.description ?? ''}' + .toLowerCase() + .contains(q); + }).toList(); + + final hasAnyOfType = + all.any((c) => c.type == _type); + return Column( children: [ - TabBar( - controller: tabController, - indicatorSize: TabBarIndicatorSize.tab, - indicatorWeight: 3, - indicator: BoxDecoration( - color: (tabController.index == 0) - ? Theme.of(context).primaryColor.withAlpha(25) - : const Color(0xFFEB5757).withAlpha(38), - border: Border( - bottom: BorderSide( - width: 3, - color: (tabController.index == 0) - ? Theme.of(context).primaryColor - : const Color(0xFFEB5757), - ), - )), - unselectedLabelStyle: TextStyle( - fontSize: 16.sp, + Padding( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 4.h), + child: _TypeSegmented( + selected: _type, + onChange: (t) => setState(() => _type = t), ), - labelStyle: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: (tabController.index == 0) - ? Theme.of(context).primaryColor - : const Color(0xFFEB5757), - ), - tabs: [ - Tab( - child: Row( - spacing: 4.w, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.images.arrowSwapDown, - colorFilter: ColorFilter.mode( - (tabController.index == 0) - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn, - ), - ), - Text(LocaleKeys.transactionIncome.tr()) - ], - ), - ), - Tab( - child: Row( - spacing: 4.w, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.images.arrowSwapUp, - colorFilter: ColorFilter.mode( - (tabController.index == 0) - ? Theme.of(context).colorScheme.onSurface - : const Color(0xFFEB5757), - BlendMode.srcIn, - ), - ), - Text(LocaleKeys.transactionExpense.tr()) - ], - ), - ), - ], ), Expanded( - child: TabBarView( - controller: tabController, - children: [ - categoriesList( - categories: state.categories - .where((c) => c.type == TransactionType.income) - .toList(), - accentColor: Theme.of(context).primaryColor, - ), - categoriesList( - categories: state.categories - .where((c) => c.type == TransactionType.expense) - .toList(), - type: TransactionType.expense, - accentColor: const Color(0xFFEB5757), - ), - ], - ), + child: !hasAnyOfType + ? InfoInterface( + action: _add, + data: emptyCategoryData, + ) + : filtered.isEmpty + ? _NoMatches(query: _query) + : _CategoriesList( + categories: filtered, + type: _type, + ), ), - SizedBox(height: 16.h), ], ); }, ), ); } +} - Widget categoriesList({ - required List categories, - TransactionType type = TransactionType.income, - required Color accentColor, - }) { - if (categories.isEmpty) { - return InfoInterface( - action: () { - addAction(); - }, - data: emptyCategoryData, - ); - } - - return ListView.separated( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, +class _CategoriesList extends StatelessWidget { + final List categories; + final TransactionType type; + + const _CategoriesList({ + required this.categories, + required this.type, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final tone = + type == TransactionType.income ? AppTone.income : AppTone.expense; + final accent = type == TransactionType.income + ? tones.incomeColor + : tones.expenseColor; + + return ListView( + padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 24.h), + children: [ + Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + for (var i = 0; i < categories.length; i++) ...[ + CategoryListTile( + category: categories[i], + tone: tone, + onTap: () => AppNavigator.push( + context, + CategoryDetailScreen(category: categories[i]), + ), + onEdit: () => AppNavigator.push( + context, + AddCategoryScreen( + category: categories[i], + accentColor: accent, + type: type, + ), + ), + onDelete: () => context + .read() + .deleteCategory(categories[i].clientId), + ), + if (i != categories.length - 1) + Divider( + height: 1, + thickness: 1, + indent: 70.w, + color: tones.borderLight.withValues(alpha: 0.6), + ), + ], + ], + ), + ), + ], + ); + } +} + +class _TypeSegmented extends StatelessWidget { + final TransactionType selected; + final ValueChanged onChange; + + const _TypeSegmented({required this.selected, required this.onChange}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), ), - itemBuilder: (context, index) { - final category = categories[index]; - return CategoryTile( - category: category, - accentColor: accentColor, - onEdit: () { - AppNavigator.push( - context, - AddCategoryScreen( - category: category, - accentColor: accentColor, - type: type, + padding: EdgeInsets.all(4.r), + child: Row( + children: [ + Expanded( + child: _SegmentItem( + label: LocaleKeys.transactionIncome.tr(), + icon: Icons.south_west, + active: selected == TransactionType.income, + activeBg: tones.income.background, + activeFg: tones.income.deep, + onTap: () => onChange(TransactionType.income), + ), + ), + Expanded( + child: _SegmentItem( + label: LocaleKeys.transactionExpense.tr(), + icon: Icons.north_east, + active: selected == TransactionType.expense, + activeBg: tones.expense.background, + activeFg: tones.expense.deep, + onTap: () => onChange(TransactionType.expense), + ), + ), + ], + ), + ); + } +} + +class _SegmentItem extends StatelessWidget { + final String label; + final IconData icon; + final bool active; + final Color activeBg; + final Color activeFg; + final VoidCallback onTap; + + const _SegmentItem({ + required this.label, + required this.icon, + required this.active, + required this.activeBg, + required this.activeFg, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + padding: EdgeInsets.symmetric(vertical: 13.h, horizontal: 8.w), + decoration: BoxDecoration( + color: active ? activeBg : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 16.sp, + color: active ? activeFg : tones.textMuted, + ), + SizedBox(width: 6.w), + Text( + label, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: active ? activeFg : tones.textSecondary, + letterSpacing: -0.1, ), - ); - }, - onDelete: () { - context.read().deleteCategory(category.clientId); - }, - ); - }, - separatorBuilder: (context, index) { - return SizedBox(height: 8.h); - }, - itemCount: categories.length, + ), + ], + ), + ), + ); + } +} + +class _NoMatches extends StatelessWidget { + final String query; + const _NoMatches({required this.query}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 48.h), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.search_off, size: 40.sp, color: tones.textMuted), + SizedBox(height: 12.h), + Text( + query.isEmpty + ? 'No categories match this filter.' + : 'No matches for "$query".', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14.sp, color: tones.textSecondary), + ), + ], + ), + ), ); } } diff --git a/lib/presentation/category/widgets/category_list_tile.dart b/lib/presentation/category/widgets/category_list_tile.dart new file mode 100644 index 00000000..8671452a --- /dev/null +++ b/lib/presentation/category/widgets/category_list_tile.dart @@ -0,0 +1,151 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/domain/entities/category_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/helpers.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +class CategoryListTile extends StatelessWidget { + final CategoryEntity category; + final AppTone tone; + final VoidCallback? onTap; + final VoidCallback? onEdit; + final VoidCallback? onDelete; + + const CategoryListTile({ + super.key, + required this.category, + required this.tone, + this.onTap, + this.onEdit, + this.onDelete, + }); + + void _confirmDelete(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + dialogType: DialogType.negative, + title: LocaleKeys.deleteCategory.tr(), + subTitle: LocaleKeys.deleteCategoryConfirm + .tr(namedArgs: {'name': category.name}), + mainAction: () { + Navigator.pop(context); + onDelete?.call(); + }, + mainActionText: LocaleKeys.delete.tr(), + secondaryActionText: LocaleKeys.cancel.tr(), + ), + ); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap ?? onEdit, + splashColor: tones.pressOverlay, + highlightColor: tones.hoverOverlay, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: ImageWidget( + mediaEntity: category.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: Icons.label_outline, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + category.name, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (category.description?.isNotEmpty == true) ...[ + SizedBox(height: 2.h), + Text( + category.description!, + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ] else ...[ + SizedBox(height: 2.h), + Text( + category.type == TransactionType.income + ? LocaleKeys.transactionIncome.tr() + : LocaleKeys.transactionExpense.tr(), + style: TextStyle( + fontSize: 11.sp, + color: palette.deep, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ), + PopupMenuButton( + icon: Icon( + Icons.more_vert, + size: 20.sp, + color: tones.textMuted, + ), + onSelected: (v) { + if (v == 'edit') onEdit?.call(); + if (v == 'delete') _confirmDelete(context); + }, + itemBuilder: (_) => [ + PopupMenuItem( + value: 'edit', + child: Text(LocaleKeys.edit.tr()), + ), + PopupMenuItem( + value: 'delete', + child: Text( + LocaleKeys.delete.tr(), + style: TextStyle(color: tones.expenseColor), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/groups/add_group_screen.dart b/lib/presentation/groups/add_group_screen.dart index 4ca69b8b..8f9af2ac 100644 --- a/lib/presentation/groups/add_group_screen.dart +++ b/lib/presentation/groups/add_group_screen.dart @@ -2,9 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:trakli/domain/entities/group_entity.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/forms/add_groups_form.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class AddGroupScreen extends StatelessWidget { final GroupEntity? group; @@ -13,11 +12,8 @@ class AddGroupScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - headerTextColor: const Color(0xFFEBEDEC), - titleText: LocaleKeys.groupAddGroup.tr(), + appBar: PageAppBar( + title: LocaleKeys.groupAddGroup.tr(), ), body: AddGroupsForm( group: group, diff --git a/lib/presentation/groups/group_detail_screen.dart b/lib/presentation/groups/group_detail_screen.dart new file mode 100644 index 00000000..0a6a0055 --- /dev/null +++ b/lib/presentation/groups/group_detail_screen.dart @@ -0,0 +1,502 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/group_entity.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/groups/add_group_screen.dart'; +import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; +import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/helpers.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; +import 'package:trakli/presentation/utils/transaction_tile.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +class GroupDetailScreen extends StatelessWidget { + final GroupEntity group; + + const GroupDetailScreen({super.key, required this.group}); + + List _matching( + List txns, + ) { + return txns + .where((t) => t.group?.clientId == group.clientId) + .toList() + ..sort((a, b) => + b.transaction.datetime.compareTo(a.transaction.datetime)); + } + + ({double received, double spent, double net}) _totals( + List matches, + ) { + double received = 0; + double spent = 0; + for (final t in matches) { + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + received += amt; + } else { + spent += amt; + } + } + return (received: received, spent: spent, net: received - spent); + } + + List<_MonthBucket> _monthly(List matches) { + final now = DateTime.now(); + final buckets = {}; + for (var i = 5; i >= 0; i--) { + final m = DateTime(now.year, now.month - i, 1); + buckets[m] = _MonthBucket(month: m); + } + for (final t in matches) { + final key = DateTime( + t.transaction.datetime.year, t.transaction.datetime.month, 1); + final bucket = buckets[key]; + if (bucket == null) continue; + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + bucket.received += amt; + } else { + bucket.spent += amt; + } + } + return buckets.values.toList(); + } + + void _confirmDelete(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + dialogType: DialogType.negative, + title: LocaleKeys.deleteGroup.tr(), + subTitle: LocaleKeys.deleteGroupConfirm + .tr(namedArgs: {'name': group.name}), + mainAction: () { + context.read().deleteGroup(group.clientId); + AppNavigator.pop(context); + AppNavigator.pop(context); + }, + secondaryAction: () => AppNavigator.pop(context), + mainActionText: LocaleKeys.delete.tr(), + secondaryActionText: LocaleKeys.cancel.tr(), + ), + ); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + + return BlocBuilder( + builder: (context, state) { + final matches = _matching(state.transactions); + final totals = _totals(matches); + final monthly = _monthly(matches); + final tone = totals.net >= 0 ? AppTone.brand : AppTone.expense; + + return Scaffold( + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: group.name, + actions: [ + PageAppBarAction( + icon: Icons.edit_outlined, + onTap: () => AppNavigator.push( + context, + AddGroupScreen(group: group), + ), + ), + PageAppBarAction( + icon: Icons.delete_outline, + onTap: () => _confirmDelete(context), + ), + ], + ), + body: ListView( + padding: + EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + children: [ + _HeaderRow(group: group, tone: tone), + SizedBox(height: 14.h), + _TotalsCard(totals: totals), + SizedBox(height: 16.h), + _ActivityCard(monthly: monthly), + SizedBox(height: 16.h), + _RecentSection(matches: matches), + SizedBox(height: 24.h), + ], + ), + ); + }, + ); + } +} + +class _MonthBucket { + final DateTime month; + double received = 0; + double spent = 0; + _MonthBucket({required this.month}); +} + +class _HeaderRow extends StatelessWidget { + final GroupEntity group; + final AppTone tone; + const _HeaderRow({required this.group, required this.tone}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 6.h), + child: Row( + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + alignment: Alignment.center, + child: group.icon != null + ? ImageWidget( + mediaEntity: group.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: Icons.folder_outlined, + ) + : Icon( + Icons.folder_outlined, + size: 22.sp, + color: palette.deep, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + group.name, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (group.description?.isNotEmpty == true) ...[ + SizedBox(height: 2.h), + Text( + group.description!, + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ); + } +} + +class _TotalsCard extends StatelessWidget { + final ({double received, double spent, double net}) totals; + const _TotalsCard({required this.totals}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 14.h), + child: Row( + children: [ + Expanded( + child: _TotalCell( + label: 'Received', + value: totals.received, + color: tones.incomeColor, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Spent', + value: totals.spent, + color: tones.expenseColor, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Net', + value: totals.net, + color: totals.net >= 0 ? tones.incomeColor : tones.expenseColor, + ), + ), + ], + ), + ); + } +} + +class _TotalCell extends StatelessWidget { + final String label; + final double value; + final Color color; + + const _TotalCell({ + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + SizedBox(height: 4.h), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + value, + compact: true, + ), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: color, + letterSpacing: -0.2, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ); + } +} + +class _ActivityCard extends StatelessWidget { + final List<_MonthBucket> monthly; + const _ActivityCard({required this.monthly}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final hasData = monthly.any((m) => m.received > 0 || m.spent > 0); + + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.all(16.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'LAST 6 MONTHS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + SizedBox(height: 4.h), + Text( + 'Activity', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + ), + SizedBox(height: 12.h), + if (!hasData) + Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Center( + child: Text( + 'No activity in the last 6 months.', + style: + TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + SizedBox( + height: 160.h, + child: SfCartesianChart( + margin: EdgeInsets.zero, + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + axisLine: AxisLine(width: 0.5, color: tones.borderLight), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + primaryYAxis: NumericAxis( + axisLine: const AxisLine(width: 0), + majorTickLines: const MajorTickLines(size: 0), + majorGridLines: MajorGridLines( + width: 0.5, + color: tones.borderLight.withValues(alpha: 0.6), + dashArray: const [4, 4], + ), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + legend: Legend( + isVisible: true, + position: LegendPosition.bottom, + textStyle: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + ), + ), + series: >[ + ColumnSeries<_MonthBucket, String>( + name: 'Received', + dataSource: monthly, + xValueMapper: (m, _) => _monthLabel(m.month), + yValueMapper: (m, _) => m.received, + color: tones.incomeColor, + width: 0.55, + spacing: 0.18, + borderRadius: BorderRadius.circular(4), + ), + ColumnSeries<_MonthBucket, String>( + name: 'Spent', + dataSource: monthly, + xValueMapper: (m, _) => _monthLabel(m.month), + yValueMapper: (m, _) => m.spent, + color: tones.expenseColor, + width: 0.55, + spacing: 0.18, + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + ], + ), + ); + } + + static String _monthLabel(DateTime d) { + const labels = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', + ]; + return labels[d.month - 1]; + } +} + +class _RecentSection extends StatelessWidget { + final List matches; + const _RecentSection({required this.matches}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final recent = matches.take(10).toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + Text( + 'RECENT TRANSACTIONS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + const Spacer(), + Text( + '${recent.length} of ${matches.length}', + style: TextStyle(fontSize: 11.sp, color: tones.textMuted), + ), + ], + ), + ), + SizedBox(height: 8.h), + if (recent.isEmpty) + Container( + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + ), + child: Center( + child: Text( + 'No transactions in this group yet.', + style: TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + for (final txn in recent) + Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: TransactionTile( + transaction: txn, + accentColor: txn.transaction.type == TransactionType.income + ? tones.incomeColor + : tones.expenseColor, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/groups/my_groups_screen.dart b/lib/presentation/groups/my_groups_screen.dart index 0e448eb9..887dfb2e 100644 --- a/lib/presentation/groups/my_groups_screen.dart +++ b/lib/presentation/groups/my_groups_screen.dart @@ -7,11 +7,11 @@ import 'package:trakli/core/constants/ui_constants.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/groups/add_group_screen.dart'; import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; +import 'package:trakli/presentation/groups/widgets/group_list_tile.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/education_banner.dart'; -import 'package:trakli/presentation/utils/group_tile.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class MyGroupsScreen extends StatefulWidget { const MyGroupsScreen({super.key}); @@ -22,40 +22,24 @@ class MyGroupsScreen extends StatefulWidget { class _MyGroupsScreenState extends State { bool _bannerDismissed = false; + String _query = ''; @override Widget build(BuildContext context) { return BlocProvider( create: (context) => GetIt.I()..getGroups(), child: Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.groupsMyGroups.tr(), - headerTextColor: const Color(0xFFEBEDEC), + backgroundColor: context.tones.bgPage, + appBar: PageAppBar( + title: LocaleKeys.groupsMyGroups.tr(), + onSearchChanged: (v) => setState(() => _query = v), + searchHint: 'Search groups', actions: [ - InkWell( - onTap: () { - AppNavigator.push(context, const AddGroupScreen()); - }, - child: Container( - width: 40.w, - height: 40.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: Theme.of(context).scaffoldBackgroundColor, - ), - padding: EdgeInsets.all(8.r), - child: Center( - child: Icon( - Icons.add, - size: 24.r, - color: Theme.of(context).primaryColor, - ), - ), - ), + PageAppBarAction( + icon: Icons.add, + onTap: () => AppNavigator.push(context, const AddGroupScreen()), + primary: true, ), - SizedBox(width: 16.w), ], ), body: BlocBuilder( @@ -68,54 +52,67 @@ class _MyGroupsScreenState extends State { return Center( child: Text( state.failure.customMessage, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: context.tones.expenseColor), ), ); } if (state.groups.isEmpty) { - return Center( - child: Text( - LocaleKeys.noGroupsFound.tr(), - style: TextStyle( - fontSize: 16.sp, - color: Colors.grey[600], - ), - ), - ); + return _EmptyGroups(); } + final q = _query.trim().toLowerCase(); + final filtered = q.isEmpty + ? state.groups + : state.groups + .where((g) => + '${g.name} ${g.description ?? ''}' + .toLowerCase() + .contains(q)) + .toList(); + return ListView( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, - ), + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 24.h), children: [ - if (state.groups.length < UiConstants.educationBannerThreshold && + if (state.groups.length < + UiConstants.educationBannerThreshold && !_bannerDismissed) Padding( - padding: EdgeInsets.only(bottom: 16.h), + padding: EdgeInsets.only(bottom: 14.h), child: EducationBanner( message: LocaleKeys.groupEducationBanner.tr(), icon: Icons.folder_outlined, - onDismiss: () { - setState(() { - _bannerDismissed = true; - }); - }, + onDismiss: () => + setState(() => _bannerDismissed = true), + ), + ), + if (filtered.isEmpty) + _NoMatches(query: _query) + else + Container( + decoration: BoxDecoration( + color: context.tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: context.tones.borderLight), + boxShadow: context.elevations.level1, + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + for (var i = 0; i < filtered.length; i++) ...[ + GroupListTile(group: filtered[i]), + if (i != filtered.length - 1) + Divider( + height: 1, + thickness: 1, + indent: 70.w, + color: context.tones.borderLight + .withValues(alpha: 0.6), + ), + ], + ], ), ), - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return GroupTile(group: state.groups[index]); - }, - separatorBuilder: (context, index) { - return SizedBox(height: 8.h); - }, - itemCount: state.groups.length, - ), ], ); }, @@ -124,3 +121,83 @@ class _MyGroupsScreenState extends State { ); } } + +class _EmptyGroups extends StatelessWidget { + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 28.w, vertical: 48.h), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64.r, + height: 64.r, + decoration: BoxDecoration( + color: tones.brand.background, + borderRadius: BorderRadius.circular(AppRadii.xl), + ), + alignment: Alignment.center, + child: Icon( + Icons.folder_outlined, + size: 32.sp, + color: tones.brand.deep, + ), + ), + SizedBox(height: 14.h), + Text( + LocaleKeys.noGroupsFound.tr(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + ), + ), + SizedBox(height: 6.h), + Text( + 'Create a group to organize related transactions.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, + height: 1.5, + ), + ), + ], + ), + ), + ); + } +} + +class _NoMatches extends StatelessWidget { + final String query; + const _NoMatches({required this.query}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 48.h), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.search_off, size: 40.sp, color: tones.textMuted), + SizedBox(height: 12.h), + Text( + query.isEmpty + ? 'No groups match this filter.' + : 'No matches for "$query".', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14.sp, color: tones.textSecondary), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/groups/widgets/group_list_tile.dart b/lib/presentation/groups/widgets/group_list_tile.dart new file mode 100644 index 00000000..93a73f25 --- /dev/null +++ b/lib/presentation/groups/widgets/group_list_tile.dart @@ -0,0 +1,150 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/domain/entities/group_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/groups/add_group_screen.dart'; +import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; +import 'package:trakli/presentation/groups/group_detail_screen.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/helpers.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +/// Compact group row matching the parties / wallets / transaction list +/// pattern. Tinted leading square, name + optional description, edit / +/// delete pop-up via overflow menu. +class GroupListTile extends StatelessWidget { + final GroupEntity group; + + const GroupListTile({super.key, required this.group}); + + void _handleEdit(BuildContext context) { + AppNavigator.push(context, AddGroupScreen(group: group)); + } + + void _confirmDelete(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + dialogType: DialogType.negative, + title: LocaleKeys.deleteGroup.tr(), + subTitle: LocaleKeys.deleteGroupConfirm + .tr(namedArgs: {'name': group.name}), + mainAction: () { + context.read().deleteGroup(group.clientId); + AppNavigator.pop(context); + }, + secondaryAction: () => AppNavigator.pop(context), + mainActionText: LocaleKeys.delete.tr(), + secondaryActionText: LocaleKeys.cancel.tr(), + ), + ); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(AppTone.brand); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => AppNavigator.push( + context, + GroupDetailScreen(group: group), + ), + splashColor: tones.pressOverlay, + highlightColor: tones.hoverOverlay, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: group.icon != null + ? ImageWidget( + mediaEntity: group.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: Icons.folder_outlined, + ) + : Icon( + Icons.folder_outlined, + size: 22.sp, + color: palette.deep, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + group.name, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (group.description?.isNotEmpty == true) ...[ + SizedBox(height: 2.h), + Text( + group.description!, + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + PopupMenuButton( + icon: Icon( + Icons.more_vert, + size: 20.sp, + color: tones.textMuted, + ), + onSelected: (v) { + if (v == 'edit') _handleEdit(context); + if (v == 'delete') _confirmDelete(context); + }, + itemBuilder: (_) => [ + PopupMenuItem( + value: 'edit', + child: Text(LocaleKeys.edit.tr()), + ), + PopupMenuItem( + value: 'delete', + child: Text( + LocaleKeys.delete.tr(), + style: TextStyle(color: tones.expenseColor), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/parties/add_party_screen.dart b/lib/presentation/parties/add_party_screen.dart index c76eb770..be28030c 100644 --- a/lib/presentation/parties/add_party_screen.dart +++ b/lib/presentation/parties/add_party_screen.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:trakli/domain/entities/party_entity.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/parties/widgets/add_party_form.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class AddPartyScreen extends StatelessWidget { final PartyEntity? party; @@ -17,11 +16,8 @@ class AddPartyScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - headerTextColor: const Color(0xFFEBEDEC), - titleText: party != null + appBar: PageAppBar( + title: party != null ? LocaleKeys.partyEditParty.tr() : LocaleKeys.partyAddParty.tr(), ), diff --git a/lib/presentation/parties/party_detail_screen.dart b/lib/presentation/parties/party_detail_screen.dart new file mode 100644 index 00000000..74973744 --- /dev/null +++ b/lib/presentation/parties/party_detail_screen.dart @@ -0,0 +1,570 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/party_entity.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/parties/add_party_screen.dart'; +import 'package:trakli/presentation/parties/cubit/party_cubit.dart'; +import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; +import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/helpers.dart'; +import 'package:trakli/presentation/utils/transaction_tile.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +/// Insights screen for a single party. Mobile-native composition: a tonal +/// hero card that drives the page palette, a totals row, a 6-month activity +/// chart, and a list of recent transactions involving this party. No +/// transaction-fetching logic of its own — reads from the existing +/// TransactionCubit so other flows stay untouched. +class PartyDetailScreen extends StatelessWidget { + final PartyEntity party; + + const PartyDetailScreen({super.key, required this.party}); + + ({double received, double spent, double net}) _totals( + List txns, + ) { + double received = 0; + double spent = 0; + for (final t in txns) { + if (t.party?.clientId != party.clientId) continue; + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + received += amt; + } else { + spent += amt; + } + } + return (received: received, spent: spent, net: received - spent); + } + + List _recent( + List txns, + ) { + final mine = txns + .where((t) => t.party?.clientId == party.clientId) + .toList() + ..sort( + (a, b) => b.transaction.datetime.compareTo(a.transaction.datetime), + ); + return mine.take(10).toList(); + } + + List<_MonthBucket> _monthly(List txns) { + final now = DateTime.now(); + final buckets = {}; + for (var i = 5; i >= 0; i--) { + final m = DateTime(now.year, now.month - i, 1); + buckets[m] = _MonthBucket(month: m); + } + for (final t in txns) { + if (t.party?.clientId != party.clientId) continue; + final key = DateTime(t.transaction.datetime.year, + t.transaction.datetime.month, 1); + final bucket = buckets[key]; + if (bucket == null) continue; + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + bucket.received += amt; + } else { + bucket.spent += amt; + } + } + return buckets.values.toList(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final totals = _totals(state.transactions); + final recent = _recent(state.transactions); + final monthly = _monthly(state.transactions); + final tones = context.tones; + final heroTone = totals.net >= 0 + ? (totals.received == 0 && totals.spent == 0 + ? AppTone.brandSoft + : AppTone.brand) + : AppTone.expense; + + return Scaffold( + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: party.name, + actions: [ + PageAppBarAction( + icon: Icons.edit_outlined, + onTap: () => AppNavigator.push( + context, + AddPartyScreen(party: party), + ), + ), + PageAppBarAction( + icon: Icons.delete_outline, + onTap: () => _confirmDelete(context), + ), + ], + ), + body: ListView( + padding: + EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + children: [ + _HeroCard(party: party, tone: heroTone), + SizedBox(height: 14.h), + _TotalsRow(totals: totals), + SizedBox(height: 16.h), + _ActivityCard(monthly: monthly), + SizedBox(height: 16.h), + _RecentSection(transactions: recent), + SizedBox(height: 24.h), + ], + ), + ); + }, + ); + } + + void _confirmDelete(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + dialogType: DialogType.negative, + title: LocaleKeys.deleteParty.tr(), + subTitle: + LocaleKeys.deletePartyConfirm.tr(namedArgs: {'name': party.name}), + mainAction: () { + context.read().deleteParty(party.clientId); + AppNavigator.pop(context); + AppNavigator.pop(context); + }, + secondaryAction: () => AppNavigator.pop(context), + mainActionText: LocaleKeys.delete.tr(), + secondaryActionText: LocaleKeys.cancel.tr(), + ), + ); + } +} + +class _MonthBucket { + final DateTime month; + double received = 0; + double spent = 0; + _MonthBucket({required this.month}); +} + +class _HeroCard extends StatelessWidget { + final PartyEntity party; + final AppTone tone; + + const _HeroCard({required this.party, required this.tone}); + + IconData _iconForType(PartyType? type) { + switch (type) { + case PartyType.individual: + return Icons.person_outline; + case PartyType.business: + return Icons.business_outlined; + case PartyType.organization: + return Icons.corporate_fare_outlined; + case PartyType.partnership: + return Icons.handshake_outlined; + case PartyType.nonProfit: + return Icons.volunteer_activism_outlined; + case PartyType.governmentAgency: + return Icons.account_balance_outlined; + case PartyType.educationalInstitution: + return Icons.school_outlined; + case PartyType.healthcareProvider: + return Icons.local_hospital_outlined; + default: + return Icons.person_outline; + } + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 6.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + alignment: Alignment.center, + child: party.icon != null + ? ImageWidget( + mediaEntity: party.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: _iconForType(party.type), + ) + : Icon( + _iconForType(party.type), + size: 22.sp, + color: palette.deep, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + party.name, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (party.type != null || party.description?.isNotEmpty == true) ...[ + SizedBox(height: 2.h), + Row( + children: [ + if (party.type != null) + Text( + party.type!.customName, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: palette.deep, + ), + ), + if (party.type != null && + party.description?.isNotEmpty == true) ...[ + SizedBox(width: 6.w), + Container( + width: 2, + height: 2, + color: tones.textMuted, + ), + SizedBox(width: 6.w), + ], + if (party.description?.isNotEmpty == true) + Flexible( + child: Text( + party.description!, + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ], + ), + ), + ], + ), + ); + } +} + +class _TotalsRow extends StatelessWidget { + final ({double received, double spent, double net}) totals; + + const _TotalsRow({required this.totals}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 14.h), + child: Row( + children: [ + Expanded( + child: _TotalCell( + label: 'Received', + value: totals.received, + color: tones.incomeColor, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Spent', + value: totals.spent, + color: tones.expenseColor, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Net', + value: totals.net, + color: totals.net >= 0 ? tones.incomeColor : tones.expenseColor, + ), + ), + ], + ), + ); + } +} + +class _TotalCell extends StatelessWidget { + final String label; + final double value; + final Color color; + + const _TotalCell({ + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + SizedBox(height: 4.h), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + value, + compact: true, + ), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: color, + letterSpacing: -0.2, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ); + } +} + +class _ActivityCard extends StatelessWidget { + final List<_MonthBucket> monthly; + + const _ActivityCard({required this.monthly}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final hasData = + monthly.any((m) => m.received > 0 || m.spent > 0); + + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.all(16.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'LAST 6 MONTHS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textMuted, + ), + ), + SizedBox(height: 4.h), + Text( + 'Activity', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + ), + SizedBox(height: 12.h), + if (!hasData) + Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Center( + child: Text( + 'No activity in the last 6 months.', + style: + TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + SizedBox( + height: 180.h, + child: SfCartesianChart( + margin: EdgeInsets.zero, + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + axisLine: + AxisLine(width: 0.5, color: tones.borderLight), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + primaryYAxis: NumericAxis( + axisLine: const AxisLine(width: 0), + majorTickLines: const MajorTickLines(size: 0), + majorGridLines: MajorGridLines( + width: 0.5, + color: tones.borderLight.withValues(alpha: 0.6), + dashArray: const [4, 4], + ), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + legend: Legend( + isVisible: true, + position: LegendPosition.bottom, + textStyle: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + ), + ), + series: >[ + ColumnSeries<_MonthBucket, String>( + name: 'Received', + dataSource: monthly, + xValueMapper: (m, _) => _monthLabel(m.month), + yValueMapper: (m, _) => m.received, + color: tones.incomeColor, + width: 0.55, + spacing: 0.18, + borderRadius: BorderRadius.circular(4), + ), + ColumnSeries<_MonthBucket, String>( + name: 'Spent', + dataSource: monthly, + xValueMapper: (m, _) => _monthLabel(m.month), + yValueMapper: (m, _) => m.spent, + color: tones.expenseColor, + width: 0.55, + spacing: 0.18, + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + ], + ), + ); + } + + static String _monthLabel(DateTime d) { + const labels = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', + ]; + return labels[d.month - 1]; + } +} + +class _RecentSection extends StatelessWidget { + final List transactions; + + const _RecentSection({required this.transactions}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + Text( + 'RECENT TRANSACTIONS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textMuted, + ), + ), + const Spacer(), + Text( + '${transactions.length} shown', + style: TextStyle(fontSize: 11.sp, color: tones.textMuted), + ), + ], + ), + ), + SizedBox(height: 8.h), + if (transactions.isEmpty) + Container( + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + ), + child: Center( + child: Text( + 'No transactions with this party yet.', + style: TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + for (final txn in transactions) + Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: TransactionTile( + transaction: txn, + accentColor: txn.transaction.type == TransactionType.income + ? tones.incomeColor + : tones.expenseColor, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/parties/party_screen.dart b/lib/presentation/parties/party_screen.dart index d19ee449..f47d7797 100644 --- a/lib/presentation/parties/party_screen.dart +++ b/lib/presentation/parties/party_screen.dart @@ -2,20 +2,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:trakli/core/constants/ui_constants.dart'; import 'package:trakli/domain/entities/party_entity.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/info_interfaces/data.dart'; import 'package:trakli/presentation/info_interfaces/info_interface.dart'; import 'package:trakli/presentation/parties/add_party_screen.dart'; import 'package:trakli/presentation/parties/cubit/party_cubit.dart'; +import 'package:trakli/presentation/parties/widgets/parties_summary.dart'; +import 'package:trakli/presentation/parties/widgets/party_list_tile.dart'; import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; -import 'package:trakli/presentation/utils/education_banner.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/enums.dart'; -import 'package:trakli/presentation/utils/party_card.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class PartyScreen extends StatefulWidget { const PartyScreen({super.key}); @@ -25,22 +24,17 @@ class PartyScreen extends StatefulWidget { } class _PartyScreenState extends State { - bool _bannerDismissed = false; + String _query = ''; + _PartyFilter _filter = _PartyFilter.all; - void addAction() { - AppNavigator.push(context, const AddPartyScreen()); - } - - Map _computePartyStats( + Map _computeStats( List parties, TransactionState transactionState, ) { - final stats = {}; - + final stats = {}; for (final party in parties) { double received = 0; double spent = 0; - for (final txn in transactionState.transactions) { if (txn.party?.clientId == party.clientId) { final amount = txn.transaction.amount; @@ -51,111 +45,126 @@ class _PartyScreenState extends State { } } } - - stats[party.clientId] = PartyStats( - receivedAmount: received, - spentAmount: spent, - ); + stats[party.clientId] = + _PartyTotals(received: received, spent: spent); } - return stats; } + void _add() { + AppNavigator.push(context, const AddPartyScreen()); + } + @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, partyState) { return BlocBuilder( builder: (context, transactionState) { - final partyStats = _computePartyStats( - partyState.parties, - transactionState, - ); + final stats = _computeStats(partyState.parties, transactionState); + final tones = context.tones; + + final filtered = _applyFilters(partyState.parties, stats); + final totals = _aggregateTotals(partyState.parties, stats); + final isEmpty = partyState.parties.isEmpty; return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.parties.tr(), - headerTextColor: const Color(0xFFEBEDEC), + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: LocaleKeys.parties.tr(), + onSearchChanged: isEmpty + ? null + : (v) => setState(() => _query = v), + searchHint: 'Search parties', actions: [ - InkWell( - onTap: () { - addAction(); - }, - child: Container( - width: 42.r, - height: 42.r, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: Theme.of(context).scaffoldBackgroundColor, - ), - padding: EdgeInsets.all(8.r), - child: Center( - child: Icon( - Icons.add, - size: 24.r, - color: Theme.of(context).primaryColor, - ), - ), - ), + PageAppBarAction( + icon: Icons.add, + onTap: _add, + primary: true, ), - SizedBox(width: 16.w), ], ), body: partyState.isLoading ? const Center(child: CircularProgressIndicator()) - : partyState.parties.isEmpty + : isEmpty ? InfoInterface( - action: () { - addAction(); - }, + action: _add, data: emptyPartyData, ) - : SingleChildScrollView( - padding: EdgeInsets.all(16.r), - child: Column( - children: [ - if (partyState.parties.length < - UiConstants.educationBannerThreshold && - !_bannerDismissed) - Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: EducationBanner( - message: - LocaleKeys.partyEducationBanner.tr(), - icon: Icons.people_outline, - onDismiss: () { - setState(() { - _bannerDismissed = true; - }); - }, - ), - ), - LayoutBuilder( - builder: (context, constraints) { - final cardWidth = - (constraints.maxWidth - 12.w) / 2; - return Wrap( - spacing: 12.w, - runSpacing: 12.h, - children: partyState.parties.map((party) { - final stats = partyStats[party.clientId]; - return SizedBox( - width: cardWidth, - child: PartyCard( - party: party, - receivedAmount: - stats?.receivedAmount ?? 0, - spentAmount: stats?.spentAmount ?? 0, - ), - ); - }).toList(), - ); - }, + : Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB( + 16.w, 8.h, 16.w, 10.h), + child: PartiesSummary( + partyCount: partyState.parties.length, + activeCount: totals.activeCount, + received: totals.received, + spent: totals.spent, ), - ], - ), + ), + _FilterChips( + selected: _filter, + onChanged: (f) => setState(() => _filter = f), + counts: { + _PartyFilter.all: partyState.parties.length, + _PartyFilter.earner: totals.earnerCount, + _PartyFilter.spender: totals.spenderCount, + _PartyFilter.idle: totals.idleCount, + }, + ), + Expanded( + child: filtered.isEmpty + ? _NoMatches(query: _query) + : ListView( + padding: EdgeInsets.fromLTRB( + 16.w, 8.h, 16.w, 24.h), + children: [ + Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: + BorderRadius.circular(AppRadii.lg), + border: Border.all( + color: tones.borderLight), + boxShadow: + context.elevations.level1, + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + for (var i = 0; + i < filtered.length; + i++) ...[ + PartyListTile( + party: filtered[i], + receivedAmount: stats[ + filtered[i] + .clientId] + ?.received ?? + 0, + spentAmount: stats[filtered[i] + .clientId] + ?.spent ?? + 0, + ), + if (i != filtered.length - 1) + Divider( + height: 1, + thickness: 1, + indent: 70.w, + color: tones.borderLight + .withValues( + alpha: 0.6), + ), + ], + ], + ), + ), + ], + ), + ), + ], ), ); }, @@ -163,12 +172,233 @@ class _PartyScreenState extends State { }, ); } + + List _applyFilters( + List parties, + Map stats, + ) { + final q = _query.trim().toLowerCase(); + Iterable scope = parties; + switch (_filter) { + case _PartyFilter.all: + break; + case _PartyFilter.earner: + scope = scope.where((p) => (stats[p.clientId]?.received ?? 0) > 0); + break; + case _PartyFilter.spender: + scope = scope.where((p) => (stats[p.clientId]?.spent ?? 0) > 0); + break; + case _PartyFilter.idle: + scope = scope.where((p) => + (stats[p.clientId]?.received ?? 0) == 0 && + (stats[p.clientId]?.spent ?? 0) == 0); + break; + } + if (q.isNotEmpty) { + scope = scope.where((p) { + final hay = '${p.name} ${p.description ?? ''} ' + '${p.type?.customName ?? ''}' + .toLowerCase(); + return hay.contains(q); + }); + } + final list = scope.toList() + ..sort((a, b) { + final na = (stats[a.clientId]?.net ?? 0).abs(); + final nb = (stats[b.clientId]?.net ?? 0).abs(); + final cmp = nb.compareTo(na); + if (cmp != 0) return cmp; + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + return list; + } + + _AggregateTotals _aggregateTotals( + List parties, + Map stats, + ) { + double received = 0; + double spent = 0; + int earnerCount = 0; + int spenderCount = 0; + int idleCount = 0; + int activeCount = 0; + for (final p in parties) { + final s = stats[p.clientId]; + final r = s?.received ?? 0; + final sp = s?.spent ?? 0; + received += r; + spent += sp; + if (r > 0) earnerCount++; + if (sp > 0) spenderCount++; + if (r == 0 && sp == 0) { + idleCount++; + } else { + activeCount++; + } + } + return _AggregateTotals( + received: received, + spent: spent, + earnerCount: earnerCount, + spenderCount: spenderCount, + idleCount: idleCount, + activeCount: activeCount, + ); + } +} + +class _PartyTotals { + final double received; + final double spent; + const _PartyTotals({required this.received, required this.spent}); + double get net => received - spent; +} + +class _AggregateTotals { + final double received; + final double spent; + final int earnerCount; + final int spenderCount; + final int idleCount; + final int activeCount; + const _AggregateTotals({ + required this.received, + required this.spent, + required this.earnerCount, + required this.spenderCount, + required this.idleCount, + required this.activeCount, + }); +} + +enum _PartyFilter { all, earner, spender, idle } + +class _FilterChips extends StatelessWidget { + final _PartyFilter selected; + final ValueChanged<_PartyFilter> onChanged; + final Map<_PartyFilter, int> counts; + + const _FilterChips({ + required this.selected, + required this.onChanged, + required this.counts, + }); + + String _label(_PartyFilter f) { + switch (f) { + case _PartyFilter.all: + return 'All'; + case _PartyFilter.earner: + return 'Earners'; + case _PartyFilter.spender: + return 'Spenders'; + case _PartyFilter.idle: + return 'No activity'; + } + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return SizedBox( + height: 38.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 16.w), + itemBuilder: (_, i) { + final filter = _PartyFilter.values[i]; + final active = filter == selected; + return GestureDetector( + onTap: () => onChanged(filter), + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: active ? tones.brand.deep : tones.bgSurface, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: active ? tones.brand.deep : tones.borderLight, + ), + ), + child: Row( + children: [ + Text( + _label(filter), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: active ? Colors.white : tones.textPrimary, + ), + ), + SizedBox(width: 6.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, vertical: 2.h), + decoration: BoxDecoration( + color: active + ? Colors.white.withValues(alpha: 0.18) + : tones.brand.background, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + '${counts[filter] ?? 0}', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + color: active ? Colors.white : tones.brand.deep, + ), + ), + ), + ], + ), + ), + ); + }, + separatorBuilder: (_, __) => SizedBox(width: 8.w), + itemCount: _PartyFilter.values.length, + ), + ); + } } +class _NoMatches extends StatelessWidget { + final String query; + const _NoMatches({required this.query}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 48.h), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.search_off, size: 40.sp, color: tones.textMuted), + SizedBox(height: 12.h), + Text( + query.isEmpty + ? 'No parties match this filter.' + : 'No matches for "$query".', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14.sp, color: tones.textSecondary), + ), + ], + ), + ), + ); + } +} + +/// Kept for compatibility with widgets that referenced the old PartyStats +/// type before this screen was rewritten as a list. class PartyStats { final double receivedAmount; final double spentAmount; - const PartyStats({ required this.receivedAmount, required this.spentAmount, diff --git a/lib/presentation/parties/widgets/parties_stats_strip.dart b/lib/presentation/parties/widgets/parties_stats_strip.dart new file mode 100644 index 00000000..f56b7df9 --- /dev/null +++ b/lib/presentation/parties/widgets/parties_stats_strip.dart @@ -0,0 +1,248 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/party_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/parties/party_screen.dart' show PartyStats; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Mobile port of PartiesStatsStrip.vue. Renders a 2x2 grid of tonal cards +/// summarizing the parties book: count, net trade, top earner, top spender. +class PartiesStatsStrip extends StatelessWidget { + final List parties; + final Map stats; + + const PartiesStatsStrip({ + super.key, + required this.parties, + required this.stats, + }); + + ({ + double received, + double spent, + double net, + PartyEntity? topEarner, + PartyEntity? topSpender, + }) _computeTotals() { + double received = 0; + double spent = 0; + PartyEntity? topEarner; + PartyEntity? topSpender; + double topEarnerAmount = 0; + double topSpenderAmount = 0; + + for (final p in parties) { + final s = stats[p.clientId]; + final r = s?.receivedAmount ?? 0; + final sp = s?.spentAmount ?? 0; + received += r; + spent += sp; + if (r > topEarnerAmount) { + topEarner = p; + topEarnerAmount = r; + } + if (sp > topSpenderAmount) { + topSpender = p; + topSpenderAmount = sp; + } + } + + return ( + received: received, + spent: spent, + net: received - spent, + topEarner: topEarner, + topSpender: topSpender, + ); + } + + @override + Widget build(BuildContext context) { + final totals = _computeTotals(); + final tones = context.tones; + + final cells = <_StatCell>[ + _StatCell( + label: LocaleKeys.parties.tr(), + value: parties.length.toString(), + icon: Icons.people_outline, + tone: AppTone.brand, + ), + _StatCell( + label: 'Net trade', + value: CurrencyFormater.formatAmountWithSymbol( + context, + totals.net, + compact: true, + ), + sub: 'Lifetime', + icon: Icons.trending_up, + tone: totals.net >= 0 ? AppTone.income : AppTone.expense, + ), + _StatCell( + label: 'Top earner', + value: totals.topEarner?.name ?? '—', + sub: totals.topEarner == null + ? 'No income yet' + : CurrencyFormater.formatAmountWithSymbol( + context, + stats[totals.topEarner!.clientId]?.receivedAmount ?? 0, + compact: true, + ), + icon: Icons.arrow_downward, + tone: AppTone.income, + ), + _StatCell( + label: 'Top spend', + value: totals.topSpender?.name ?? '—', + sub: totals.topSpender == null + ? 'No spend yet' + : CurrencyFormater.formatAmountWithSymbol( + context, + stats[totals.topSpender!.clientId]?.spentAmount ?? 0, + compact: true, + ), + icon: Icons.arrow_upward, + tone: AppTone.expense, + ), + ]; + + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: Column( + children: [ + Row( + children: [ + Expanded(child: _StatTile(cell: cells[0])), + const _Divider(vertical: true), + Expanded(child: _StatTile(cell: cells[1])), + ], + ), + const _Divider(vertical: false), + Row( + children: [ + Expanded(child: _StatTile(cell: cells[2])), + const _Divider(vertical: true), + Expanded(child: _StatTile(cell: cells[3])), + ], + ), + ], + ), + ); + } +} + +class _StatCell { + final String label; + final String value; + final String? sub; + final IconData icon; + final AppTone tone; + + const _StatCell({ + required this.label, + required this.value, + this.sub, + required this.icon, + required this.tone, + }); +} + +class _StatTile extends StatelessWidget { + final _StatCell cell; + + const _StatTile({required this.cell}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(cell.tone); + + return Container( + color: palette.background, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 30.r, + height: 30.r, + decoration: BoxDecoration( + color: tones.glassBg, + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: tones.borderLight), + ), + alignment: Alignment.center, + child: Icon(cell.icon, size: 14.sp, color: palette.deep), + ), + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + cell.label.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + color: palette.deep.withValues(alpha: 0.85), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 2.h), + Text( + cell.value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w700, + color: palette.ink, + fontFeatures: const [FontFeature.tabularFigures()], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (cell.sub != null) ...[ + SizedBox(height: 1.h), + Text( + cell.sub!, + style: TextStyle( + fontSize: 10.sp, + color: palette.ink.withValues(alpha: 0.6), + fontFeatures: const [FontFeature.tabularFigures()], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ); + } +} + +class _Divider extends StatelessWidget { + final bool vertical; + const _Divider({required this.vertical}); + + @override + Widget build(BuildContext context) { + final color = context.tones.borderLight; + return vertical + ? Container(width: 1, height: 50.h, color: color) + : Container(height: 1, color: color); + } +} diff --git a/lib/presentation/parties/widgets/parties_summary.dart b/lib/presentation/parties/widgets/parties_summary.dart new file mode 100644 index 00000000..4bbf1a03 --- /dev/null +++ b/lib/presentation/parties/widgets/parties_summary.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Slim, single-row parties summary. ~80h tall: tonal net chip on the +/// left, Received + Spent micro-stats next to it, "X active" on the right. +/// Keeps the surface premium without eating half the viewport. +class PartiesSummary extends StatelessWidget { + final int partyCount; + final int activeCount; + final double received; + final double spent; + + const PartiesSummary({ + super.key, + required this.partyCount, + required this.activeCount, + required this.received, + required this.spent, + }); + + double get _net => received - spent; + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final tone = _net >= 0 ? AppTone.brand : AppTone.expense; + final palette = tones.tone(tone); + final netColor = + _net >= 0 ? tones.incomeColor : tones.expenseColor; + + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 38.r, + height: 38.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: Icon( + _net >= 0 ? Icons.trending_up : Icons.trending_down, + size: 18.sp, + color: palette.deep, + ), + ), + SizedBox(width: 10.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'NET', + style: TextStyle( + fontSize: 9.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: tones.textMuted, + ), + ), + SizedBox(height: 1.h), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + _net, + compact: true, + ), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: netColor, + letterSpacing: -0.3, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + SizedBox(width: 14.w), + Container(width: 1, height: 30.h, color: tones.borderLight), + SizedBox(width: 14.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _InlineStat( + label: 'In', + value: received, + color: tones.incomeColor, + ), + SizedBox(height: 4.h), + _InlineStat( + label: 'Out', + value: spent, + color: tones.expenseColor, + ), + ], + ), + ), + Container( + padding: + EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: tones.brand.background, + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$activeCount', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + color: tones.brand.deep, + ), + ), + Text( + ' / $partyCount', + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w600, + color: tones.brand.deep.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _InlineStat extends StatelessWidget { + final String label; + final double value; + final Color color; + + const _InlineStat({ + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + SizedBox(width: 6.w), + Text( + label, + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w600, + color: tones.textMuted, + ), + ), + SizedBox(width: 6.w), + Flexible( + child: Text( + CurrencyFormater.formatAmountWithSymbol( + context, + value, + compact: true, + ), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + fontFeatures: const [FontFeature.tabularFigures()], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/parties/widgets/party_list_tile.dart b/lib/presentation/parties/widgets/party_list_tile.dart new file mode 100644 index 00000000..9b721573 --- /dev/null +++ b/lib/presentation/parties/widgets/party_list_tile.dart @@ -0,0 +1,234 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/party_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/parties/party_detail_screen.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +/// Compact party row that visually matches the home-screen transaction +/// tile pattern: tinted leading square (tone-aware), name + secondary line, +/// net amount on the right with a chevron. Tap opens PartyDetailScreen. +class PartyListTile extends StatelessWidget { + final PartyEntity party; + final double receivedAmount; + final double spentAmount; + + const PartyListTile({ + super.key, + required this.party, + required this.receivedAmount, + required this.spentAmount, + }); + + double get _net => receivedAmount - spentAmount; + + AppTone get _tone { + if (_net > 0) return AppTone.income; + if (_net < 0) return AppTone.expense; + return AppTone.brand; + } + + IconData _iconForType(PartyType? type) { + switch (type) { + case PartyType.individual: + return Icons.person_outline; + case PartyType.business: + return Icons.business_outlined; + case PartyType.organization: + return Icons.corporate_fare_outlined; + case PartyType.partnership: + return Icons.handshake_outlined; + case PartyType.nonProfit: + return Icons.volunteer_activism_outlined; + case PartyType.governmentAgency: + return Icons.account_balance_outlined; + case PartyType.educationalInstitution: + return Icons.school_outlined; + case PartyType.healthcareProvider: + return Icons.local_hospital_outlined; + default: + return Icons.person_outline; + } + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(_tone); + final hasActivity = receivedAmount > 0 || spentAmount > 0; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => AppNavigator.push( + context, + PartyDetailScreen(party: party), + ), + splashColor: tones.pressOverlay, + highlightColor: tones.hoverOverlay, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: party.icon != null + ? ImageWidget( + mediaEntity: party.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: _iconForType(party.type), + ) + : Icon( + _iconForType(party.type), + size: 22.sp, + color: palette.deep, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + party.name, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 2.h), + if (hasActivity) + _ActivityLine( + received: receivedAmount, + spent: spentAmount, + ) + else + Text( + party.type?.customName ?? LocaleKeys.parties.tr(), + style: TextStyle( + fontSize: 12.sp, + color: tones.textMuted, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + SizedBox(width: 8.w), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + _net, + compact: true, + ), + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: hasActivity + ? (_net >= 0 + ? tones.incomeColor + : tones.expenseColor) + : tones.textMuted, + letterSpacing: -0.2, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + SizedBox(height: 2.h), + Text( + 'NET', + style: TextStyle( + fontSize: 9.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: tones.textMuted, + ), + ), + ], + ), + SizedBox(width: 4.w), + Icon( + Icons.chevron_right, + size: 18.sp, + color: tones.textMuted, + ), + ], + ), + ), + ), + ); + } +} + +class _ActivityLine extends StatelessWidget { + final double received; + final double spent; + + const _ActivityLine({required this.received, required this.spent}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Row( + children: [ + if (received > 0) ...[ + Icon(Icons.south_west, size: 11.sp, color: tones.incomeColor), + SizedBox(width: 3.w), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + received, + compact: true, + ), + style: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + if (received > 0 && spent > 0) ...[ + SizedBox(width: 8.w), + Container(width: 2, height: 2, color: tones.textMuted), + SizedBox(width: 8.w), + ], + if (spent > 0) ...[ + Icon(Icons.north_east, size: 11.sp, color: tones.expenseColor), + SizedBox(width: 3.w), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + spent, + compact: true, + ), + style: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ], + ); + } +} diff --git a/lib/presentation/wallets/add_wallet_screen.dart b/lib/presentation/wallets/add_wallet_screen.dart index f944e149..c45db915 100644 --- a/lib/presentation/wallets/add_wallet_screen.dart +++ b/lib/presentation/wallets/add_wallet_screen.dart @@ -2,9 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:trakli/core/error/failures/failures.dart'; import 'package:trakli/domain/entities/wallet_entity.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/forms/add_wallet_form.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; @@ -31,13 +30,10 @@ class AddWalletScreen extends StatelessWidget { } }, child: Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - headerTextColor: Colors.white, - titleText: wallet != null + appBar: PageAppBar( + title: wallet != null ? LocaleKeys.editWallet.tr() : LocaleKeys.addWallet.tr(), - leading: const CustomBackButton(), ), body: AddWalletForm(wallet: wallet), ), diff --git a/lib/presentation/wallets/wallet_detail_screen.dart b/lib/presentation/wallets/wallet_detail_screen.dart new file mode 100644 index 00000000..a7791dfb --- /dev/null +++ b/lib/presentation/wallets/wallet_detail_screen.dart @@ -0,0 +1,538 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/helpers.dart' show showCustomDialog; +import 'package:trakli/presentation/utils/page_app_bar.dart'; +import 'package:trakli/presentation/utils/transaction_tile.dart'; +import 'package:trakli/presentation/wallets/add_wallet_screen.dart'; +import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +/// Insights screen for a single wallet. Mirrors PartyDetailScreen: slim +/// header row, totals row (Income / Expense / Balance), 6-month activity +/// chart, recent transactions. Reads from TransactionCubit so no other +/// flow is affected. +class WalletDetailScreen extends StatelessWidget { + final WalletEntity wallet; + + const WalletDetailScreen({super.key, required this.wallet}); + + ({double income, double expense, double balance}) _totals( + List txns, + ) { + double income = 0; + double expense = 0; + for (final t in txns) { + if (t.transaction.walletClientId != wallet.clientId) continue; + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + income += amt; + } else { + expense += amt; + } + } + return (income: income, expense: expense, balance: income - expense); + } + + List _recent( + List txns, + ) { + final mine = txns + .where((t) => t.transaction.walletClientId == wallet.clientId) + .toList() + ..sort( + (a, b) => b.transaction.datetime.compareTo(a.transaction.datetime), + ); + return mine.take(10).toList(); + } + + List<_MonthBucket> _monthly(List txns) { + final now = DateTime.now(); + final buckets = {}; + for (var i = 5; i >= 0; i--) { + final m = DateTime(now.year, now.month - i, 1); + buckets[m] = _MonthBucket(month: m); + } + for (final t in txns) { + if (t.transaction.walletClientId != wallet.clientId) continue; + final key = DateTime( + t.transaction.datetime.year, t.transaction.datetime.month, 1); + final bucket = buckets[key]; + if (bucket == null) continue; + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + bucket.income += amt; + } else { + bucket.expense += amt; + } + } + return buckets.values.toList(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final totals = _totals(state.transactions); + final recent = _recent(state.transactions); + final monthly = _monthly(state.transactions); + final tones = context.tones; + final tone = totals.balance >= 0 + ? (totals.income == 0 && totals.expense == 0 + ? AppTone.brandSoft + : AppTone.brand) + : AppTone.expense; + + return Scaffold( + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: wallet.name, + actions: [ + PageAppBarAction( + icon: Icons.edit_outlined, + onTap: () => AppNavigator.push( + context, + AddWalletScreen(wallet: wallet), + ), + ), + PageAppBarAction( + icon: Icons.delete_outline, + onTap: () => _confirmDelete(context), + ), + ], + ), + body: ListView( + padding: + EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + children: [ + _HeaderRow(wallet: wallet, tone: tone), + SizedBox(height: 14.h), + _TotalsRow(totals: totals), + SizedBox(height: 16.h), + _ActivityCard(monthly: monthly), + SizedBox(height: 16.h), + _RecentSection(transactions: recent), + SizedBox(height: 24.h), + ], + ), + ); + }, + ); + } + + void _confirmDelete(BuildContext context) { + showCustomDialog( + widget: PopUpDialog( + dialogType: DialogType.negative, + title: LocaleKeys.deleteWallet.tr(), + subTitle: LocaleKeys.deleteWalletConfirm + .tr(namedArgs: {'name': wallet.name}), + mainAction: () { + context.read().deleteWallet(wallet.clientId); + AppNavigator.pop(context); + AppNavigator.pop(context); + }, + secondaryAction: () => AppNavigator.pop(context), + mainActionText: LocaleKeys.delete.tr(), + secondaryActionText: LocaleKeys.cancel.tr(), + ), + ); + } +} + +class _MonthBucket { + final DateTime month; + double income = 0; + double expense = 0; + _MonthBucket({required this.month}); +} + +class _HeaderRow extends StatelessWidget { + final WalletEntity wallet; + final AppTone tone; + + const _HeaderRow({required this.wallet, required this.tone}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 6.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + alignment: Alignment.center, + child: wallet.icon != null + ? ImageWidget( + mediaEntity: wallet.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: Icons.account_balance_wallet_outlined, + ) + : Icon( + Icons.account_balance_wallet_outlined, + size: 22.sp, + color: palette.deep, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + wallet.name, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 2.h), + Row( + children: [ + Text( + wallet.currencyCode, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: palette.deep, + ), + ), + if (wallet.description?.isNotEmpty == true) ...[ + SizedBox(width: 6.w), + Container(width: 2, height: 2, color: tones.textMuted), + SizedBox(width: 6.w), + Flexible( + child: Text( + wallet.description!, + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ], + ), + ], + ), + ), + ], + ), + ); + } +} + +class _TotalsRow extends StatelessWidget { + final ({double income, double expense, double balance}) totals; + + const _TotalsRow({required this.totals}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 14.h), + child: Row( + children: [ + Expanded( + child: _TotalCell( + label: 'Income', + value: totals.income, + color: tones.incomeColor, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Expense', + value: totals.expense, + color: tones.expenseColor, + ), + ), + Container(width: 1, height: 36.h, color: tones.borderLight), + Expanded( + child: _TotalCell( + label: 'Balance', + value: totals.balance, + color: totals.balance >= 0 + ? tones.incomeColor + : tones.expenseColor, + ), + ), + ], + ), + ); + } +} + +class _TotalCell extends StatelessWidget { + final String label; + final double value; + final Color color; + + const _TotalCell({ + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + SizedBox(height: 4.h), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + value, + compact: true, + ), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: color, + letterSpacing: -0.2, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ); + } +} + +class _ActivityCard extends StatelessWidget { + final List<_MonthBucket> monthly; + + const _ActivityCard({required this.monthly}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final hasData = monthly.any((m) => m.income > 0 || m.expense > 0); + + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.all(16.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'LAST 6 MONTHS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textMuted, + ), + ), + SizedBox(height: 4.h), + Text( + 'Activity', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + ), + SizedBox(height: 12.h), + if (!hasData) + Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Center( + child: Text( + 'No activity in the last 6 months.', + style: TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + SizedBox( + height: 180.h, + child: SfCartesianChart( + margin: EdgeInsets.zero, + plotAreaBorderWidth: 0, + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + axisLine: AxisLine(width: 0.5, color: tones.borderLight), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + primaryYAxis: NumericAxis( + axisLine: const AxisLine(width: 0), + majorTickLines: const MajorTickLines(size: 0), + majorGridLines: MajorGridLines( + width: 0.5, + color: tones.borderLight.withValues(alpha: 0.6), + dashArray: const [4, 4], + ), + labelStyle: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + tooltipBehavior: TooltipBehavior(enable: true), + legend: Legend( + isVisible: true, + position: LegendPosition.bottom, + textStyle: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + ), + ), + series: >[ + ColumnSeries<_MonthBucket, String>( + name: 'Income', + dataSource: monthly, + xValueMapper: (m, _) => _monthLabel(m.month), + yValueMapper: (m, _) => m.income, + color: tones.incomeColor, + width: 0.55, + spacing: 0.18, + borderRadius: BorderRadius.circular(4), + ), + ColumnSeries<_MonthBucket, String>( + name: 'Expense', + dataSource: monthly, + xValueMapper: (m, _) => _monthLabel(m.month), + yValueMapper: (m, _) => m.expense, + color: tones.expenseColor, + width: 0.55, + spacing: 0.18, + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + ], + ), + ); + } + + static String _monthLabel(DateTime d) { + const labels = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', + ]; + return labels[d.month - 1]; + } +} + +class _RecentSection extends StatelessWidget { + final List transactions; + + const _RecentSection({required this.transactions}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + children: [ + Text( + 'RECENT TRANSACTIONS', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textMuted, + ), + ), + const Spacer(), + Text( + '${transactions.length} shown', + style: TextStyle(fontSize: 11.sp, color: tones.textMuted), + ), + ], + ), + ), + SizedBox(height: 8.h), + if (transactions.isEmpty) + Container( + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + ), + child: Center( + child: Text( + 'No transactions for this wallet yet.', + style: TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ), + ) + else + // TransactionTile carries its own Card; stack directly with + // small vertical gaps to match the home list grouping. + for (final txn in transactions) + Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: TransactionTile( + transaction: txn, + accentColor: txn.transaction.type == TransactionType.income + ? tones.incomeColor + : tones.expenseColor, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/wallets/wallet_screen.dart b/lib/presentation/wallets/wallet_screen.dart index 09712dfe..a9d5b3a1 100644 --- a/lib/presentation/wallets/wallet_screen.dart +++ b/lib/presentation/wallets/wallet_screen.dart @@ -2,22 +2,58 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/constants/config_constants.dart'; import 'package:trakli/core/error/failures/failures.dart'; -import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; -import 'package:trakli/presentation/exchange_rate/cubit/exchange_rate_cubit.dart'; +import 'package:trakli/presentation/config/cubit/config_cubit.dart'; +import 'package:trakli/presentation/currency/cubit/currency_cubit.dart'; import 'package:trakli/presentation/info_interfaces/data.dart'; import 'package:trakli/presentation/info_interfaces/info_interface.dart'; -import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; -import 'package:trakli/presentation/utils/wallet_tile.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/wallets/add_wallet_screen.dart'; import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; +import 'package:trakli/presentation/wallets/widgets/wallet_list_tile.dart'; +import 'package:trakli/presentation/wallets/widgets/wallets_summary.dart'; -class WalletScreen extends StatelessWidget { +class WalletScreen extends StatefulWidget { const WalletScreen({super.key}); + @override + State createState() => _WalletScreenState(); +} + +class _WalletScreenState extends State { + String _query = ''; + _WalletFilter _filter = _WalletFilter.all; + + void _add() { + AppNavigator.push(context, const AddWalletScreen()); + } + + _Totals _walletTotals( + WalletEntity wallet, + List txns, + ) { + double income = 0; + double expense = 0; + for (final t in txns) { + if (t.transaction.walletClientId != wallet.clientId) continue; + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + income += amt; + } else { + expense += amt; + } + } + return _Totals(income: income, expense: expense); + } + @override Widget build(BuildContext context) { return BlocConsumer( @@ -32,108 +68,356 @@ class WalletScreen extends StatelessWidget { } }, builder: (context, state) { - final exchangeRateEntity = - context.read().state.entity; - final transactions = context.watch().state.transactions; - final totals = calculateIncomeExpense( - transactions, - exchangeRateEntity: exchangeRateEntity, + final tones = context.tones; + final transactions = + context.watch().state.transactions; + final defaultWalletId = context + .watch() + .state + .getConfigByKey(ConfigConstants.defaultWallet) + ?.value as String?; + final defaultCurrencyCode = + context.watch().state.currency?.code; + + final perWallet = { + for (final w in state.wallets) + w.clientId: _walletTotals(w, transactions), + }; + + // Aggregate is shown only in the default currency to keep the + // hero number meaningful (no cross-currency mixing). + final scope = defaultCurrencyCode == null + ? state.wallets + : state.wallets + .where((w) => w.currencyCode == defaultCurrencyCode) + .toList(); + double aggIncome = 0; + double aggExpense = 0; + int activeCount = 0; + for (final w in scope) { + final t = perWallet[w.clientId]!; + aggIncome += t.income; + aggExpense += t.expense; + if (t.income > 0 || t.expense > 0) activeCount++; + } + + final filtered = _applyFilters( + state.wallets, + perWallet, + defaultWalletId, ); - final totalBalance = totals.totalIncome - totals.totalExpense; + + final isEmpty = state.wallets.isEmpty; return Scaffold( - appBar: CustomAppBar( - titleText: LocaleKeys.wallet.tr(), + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: LocaleKeys.wallet.tr(), + showBack: false, + onSearchChanged: + isEmpty ? null : (v) => setState(() => _query = v), + searchHint: 'Search wallets', actions: [ - GestureDetector( - onTap: () { - AppNavigator.push(context, const AddWalletScreen()); - }, - child: Container( - width: 42.r, - height: 42.r, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: - Theme.of(context).primaryColor.withValues(alpha: 0.2), - ), - child: Icon( - Icons.add, - size: 20.sp, - color: Theme.of(context).primaryColor, - ), - ), + PageAppBarAction( + icon: Icons.add, + onTap: _add, + primary: true, ), - SizedBox(width: 16.w), ], ), body: state.isLoading ? const Center(child: CircularProgressIndicator()) - : state.wallets.isEmpty + : isEmpty ? InfoInterface( - action: () { - AppNavigator.push(context, const AddWalletScreen()); - }, + action: _add, data: emptyWalletData, ) - : Padding( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, - ), - child: Column( - children: [ - Text( - LocaleKeys.totalBalance.tr(), - style: TextStyle( - fontSize: 14.sp, - ), + : Column( + children: [ + Padding( + padding: + EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 10.h), + child: WalletsSummary( + walletCount: state.wallets.length, + activeCount: activeCount, + income: aggIncome, + expense: aggExpense, + defaultCurrency: defaultCurrencyCode, ), - Text( - LocaleKeys.balanceAmountWithCurrency.tr( - args: [ - CurrencyFormater.formatAmountWithSymbol( - context, - totalBalance, - ) - ], - ), - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w700, - ), + ), + _FilterChips( + selected: _filter, + onChanged: (f) => setState(() => _filter = f), + counts: _filterCounts( + state.wallets, + perWallet, + defaultCurrencyCode, ), - SizedBox(height: 16.h), - Expanded( - child: state.wallets.isEmpty - ? Center( - child: Text( - LocaleKeys.noWalletsYet.tr(), - style: TextStyle( - fontSize: 16.sp, - color: Colors.grey, + ), + Expanded( + child: filtered.isEmpty + ? _NoMatches(query: _query) + : ListView( + padding: EdgeInsets.fromLTRB( + 16.w, 8.h, 16.w, 24.h), + children: [ + Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular( + AppRadii.lg), + border: Border.all( + color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + for (var i = 0; + i < filtered.length; + i++) ...[ + WalletListTile( + wallet: filtered[i], + income: perWallet[filtered[i] + .clientId] + ?.income ?? + 0, + expense: perWallet[filtered[i] + .clientId] + ?.expense ?? + 0, + isDefault: filtered[i] + .clientId == + defaultWalletId, + ), + if (i != filtered.length - 1) + Divider( + height: 1, + thickness: 1, + indent: 70.w, + color: tones.borderLight + .withValues(alpha: 0.6), + ), + ], + ], ), ), - ) - : ListView.separated( - itemBuilder: (context, index) { - final wallet = state.wallets[index]; - return WalletTile( - wallet: wallet, - showDefaultWallet: true, - ); - }, - separatorBuilder: (context, index) { - return SizedBox(height: 16.h); - }, - itemCount: state.wallets.length, - ), - ) - ], - ), + ], + ), + ), + ], ), ); }, ); } + + Map<_WalletFilter, int> _filterCounts( + List wallets, + Map stats, + String? defaultCurrencyCode, + ) { + int defaultCurrencyCount = 0; + int activeCount = 0; + int idleCount = 0; + for (final w in wallets) { + final t = stats[w.clientId]!; + if (defaultCurrencyCode != null && + w.currencyCode == defaultCurrencyCode) { + defaultCurrencyCount++; + } + if (t.income > 0 || t.expense > 0) { + activeCount++; + } else { + idleCount++; + } + } + return { + _WalletFilter.all: wallets.length, + _WalletFilter.active: activeCount, + _WalletFilter.idle: idleCount, + _WalletFilter.defaultCurrency: defaultCurrencyCount, + }; + } + + List _applyFilters( + List wallets, + Map stats, + String? defaultWalletId, + ) { + final q = _query.trim().toLowerCase(); + Iterable scope = wallets; + switch (_filter) { + case _WalletFilter.all: + break; + case _WalletFilter.active: + scope = scope.where((w) { + final t = stats[w.clientId]!; + return t.income > 0 || t.expense > 0; + }); + break; + case _WalletFilter.idle: + scope = scope.where((w) { + final t = stats[w.clientId]!; + return t.income == 0 && t.expense == 0; + }); + break; + case _WalletFilter.defaultCurrency: + final defaultCurrencyCode = + context.read().state.currency?.code; + if (defaultCurrencyCode != null) { + scope = scope.where((w) => w.currencyCode == defaultCurrencyCode); + } + break; + } + if (q.isNotEmpty) { + scope = scope.where((w) { + final hay = '${w.name} ${w.description ?? ''} ${w.currencyCode}' + .toLowerCase(); + return hay.contains(q); + }); + } + final list = scope.toList() + ..sort((a, b) { + // Default wallet first; then by absolute balance descending. + if (a.clientId == defaultWalletId) return -1; + if (b.clientId == defaultWalletId) return 1; + final ta = stats[a.clientId]!; + final tb = stats[b.clientId]!; + final ba = (ta.income - ta.expense).abs(); + final bb = (tb.income - tb.expense).abs(); + final cmp = bb.compareTo(ba); + if (cmp != 0) return cmp; + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + return list; + } +} + +class _Totals { + final double income; + final double expense; + const _Totals({required this.income, required this.expense}); +} + +enum _WalletFilter { all, active, idle, defaultCurrency } + +class _FilterChips extends StatelessWidget { + final _WalletFilter selected; + final ValueChanged<_WalletFilter> onChanged; + final Map<_WalletFilter, int> counts; + + const _FilterChips({ + required this.selected, + required this.onChanged, + required this.counts, + }); + + String _label(_WalletFilter f) { + switch (f) { + case _WalletFilter.all: + return 'All'; + case _WalletFilter.active: + return 'Active'; + case _WalletFilter.idle: + return 'No activity'; + case _WalletFilter.defaultCurrency: + return 'Default currency'; + } + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return SizedBox( + height: 38.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 16.w), + itemBuilder: (_, i) { + final filter = _WalletFilter.values[i]; + final active = filter == selected; + return GestureDetector( + onTap: () => onChanged(filter), + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: active ? tones.brand.deep : tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.pill), + border: Border.all( + color: active ? tones.brand.deep : tones.borderLight, + ), + ), + child: Row( + children: [ + Text( + _label(filter), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: active ? Colors.white : tones.textPrimary, + ), + ), + SizedBox(width: 6.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, vertical: 2.h), + decoration: BoxDecoration( + color: active + ? Colors.white.withValues(alpha: 0.18) + : tones.brand.background, + borderRadius: BorderRadius.circular(AppRadii.pill), + ), + child: Text( + '${counts[filter] ?? 0}', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + color: active ? Colors.white : tones.brand.deep, + ), + ), + ), + ], + ), + ), + ); + }, + separatorBuilder: (_, __) => SizedBox(width: 8.w), + itemCount: _WalletFilter.values.length, + ), + ); + } +} + +class _NoMatches extends StatelessWidget { + final String query; + const _NoMatches({required this.query}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 48.h), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.search_off, size: 40.sp, color: tones.textMuted), + SizedBox(height: 12.h), + Text( + query.isEmpty + ? 'No wallets match this filter.' + : 'No matches for "$query".', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14.sp, color: tones.textSecondary), + ), + ], + ), + ), + ); + } } diff --git a/lib/presentation/wallets/widgets/wallet_list_tile.dart b/lib/presentation/wallets/widgets/wallet_list_tile.dart new file mode 100644 index 00000000..62dbd22f --- /dev/null +++ b/lib/presentation/wallets/widgets/wallet_list_tile.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/wallets/wallet_detail_screen.dart'; +import 'package:trakli/presentation/widgets/image_widget.dart'; + +/// Compact wallet row matching the parties / transaction-row pattern: +/// tinted leading square (tone-aware), name + currency, balance on the +/// right with a chevron. Tap → WalletDetailScreen. +class WalletListTile extends StatelessWidget { + final WalletEntity wallet; + final double income; + final double expense; + final bool isDefault; + + const WalletListTile({ + super.key, + required this.wallet, + required this.income, + required this.expense, + this.isDefault = false, + }); + + double get _balance => income - expense; + + AppTone get _tone { + if (_balance > 0) return AppTone.income; + if (_balance < 0) return AppTone.expense; + return AppTone.brand; + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(_tone); + final hasActivity = income > 0 || expense > 0; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => AppNavigator.push( + context, + WalletDetailScreen(wallet: wallet), + ), + splashColor: tones.pressOverlay, + highlightColor: tones.hoverOverlay, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 44.r, + height: 44.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: wallet.icon != null + ? ImageWidget( + mediaEntity: wallet.icon, + accentColor: palette.deep, + iconSize: 22.sp, + emojiSize: 22.sp, + placeholderIcon: + Icons.account_balance_wallet_outlined, + ) + : Icon( + Icons.account_balance_wallet_outlined, + size: 22.sp, + color: palette.deep, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + wallet.name, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (isDefault) ...[ + SizedBox(width: 6.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, + vertical: 1.h, + ), + decoration: BoxDecoration( + color: tones.brand.background, + borderRadius: + BorderRadius.circular(AppRadii.sm), + ), + child: Text( + 'DEFAULT', + style: TextStyle( + fontSize: 8.sp, + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + color: tones.brand.deep, + ), + ), + ), + ], + ], + ), + SizedBox(height: 2.h), + Row( + children: [ + Text( + wallet.currencyCode, + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w600, + color: tones.textMuted, + ), + ), + if (hasActivity) ...[ + SizedBox(width: 6.w), + Container( + width: 2, + height: 2, + color: tones.textMuted, + ), + SizedBox(width: 6.w), + Icon( + Icons.south_west, + size: 11.sp, + color: tones.incomeColor, + ), + SizedBox(width: 2.w), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + income, + compact: true, + ), + style: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + SizedBox(width: 6.w), + Icon( + Icons.north_east, + size: 11.sp, + color: tones.expenseColor, + ), + SizedBox(width: 2.w), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + expense, + compact: true, + ), + style: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ], + ], + ), + ], + ), + ), + SizedBox(width: 8.w), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + _balance, + compact: true, + ), + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: hasActivity + ? (_balance >= 0 + ? tones.incomeColor + : tones.expenseColor) + : tones.textMuted, + letterSpacing: -0.2, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + SizedBox(height: 2.h), + Text( + 'BALANCE', + style: TextStyle( + fontSize: 9.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: tones.textMuted, + ), + ), + ], + ), + SizedBox(width: 4.w), + Icon( + Icons.chevron_right, + size: 18.sp, + color: tones.textMuted, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/wallets/widgets/wallets_stats_strip.dart b/lib/presentation/wallets/widgets/wallets_stats_strip.dart new file mode 100644 index 00000000..ab139299 --- /dev/null +++ b/lib/presentation/wallets/widgets/wallets_stats_strip.dart @@ -0,0 +1,295 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/currency/cubit/currency_cubit.dart'; +import 'package:trakli/presentation/exchange_rate/cubit/exchange_rate_cubit.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class WalletsStatsStrip extends StatelessWidget { + final List wallets; + final List transactions; + + const WalletsStatsStrip({ + super.key, + required this.wallets, + required this.transactions, + }); + + ({ + double income, + double expense, + double balance, + WalletEntity? topEarner, + WalletEntity? topSpender, + }) _computeTotals(String? defaultCurrencyCode) { + double income = 0; + double expense = 0; + double balance = 0; + WalletEntity? topEarner; + WalletEntity? topSpender; + double topEarnerAmount = 0; + double topSpenderAmount = 0; + + final scope = defaultCurrencyCode == null + ? wallets + : wallets.where((w) => w.currencyCode == defaultCurrencyCode); + + for (final w in scope) { + double walletIncome = 0; + double walletExpense = 0; + for (final txn in transactions) { + if (txn.transaction.walletClientId != w.clientId) continue; + final amount = txn.transaction.amount; + if (txn.transaction.type == TransactionType.income) { + walletIncome += amount; + } else { + walletExpense += amount; + } + } + income += walletIncome; + expense += walletExpense; + balance += walletIncome - walletExpense; + if (walletIncome > topEarnerAmount) { + topEarner = w; + topEarnerAmount = walletIncome; + } + if (walletExpense > topSpenderAmount) { + topSpender = w; + topSpenderAmount = walletExpense; + } + } + + return ( + income: income, + expense: expense, + balance: balance, + topEarner: topEarner, + topSpender: topSpender, + ); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final defaultCurrencyCode = context.watch().state.currency?.code; + final exchangeRateEntity = context.watch().state.entity; + + final totals = _computeTotals(defaultCurrencyCode); + final filteredCount = defaultCurrencyCode == null + ? wallets.length + : wallets.where((w) => w.currencyCode == defaultCurrencyCode).length; + + String fmt(double value) => CurrencyFormater.formatAmountWithSymbol( + context, + value, + compact: true, + ); + + // Reference exchangeRateEntity to avoid an unused warning while keeping + // it available for future cross-currency conversions. + exchangeRateEntity; + + final cells = <_StatCell>[ + _StatCell( + label: LocaleKeys.wallet.tr(), + value: wallets.length.toString(), + sub: defaultCurrencyCode != null && filteredCount != wallets.length + ? '$filteredCount in $defaultCurrencyCode' + : null, + icon: Icons.account_balance_wallet_outlined, + tone: AppTone.brand, + ), + _StatCell( + label: LocaleKeys.totalBalance.tr(), + value: fmt(totals.balance), + sub: filteredCount > 0 + ? 'Across $filteredCount wallet${filteredCount == 1 ? '' : 's'}' + : 'No wallets yet', + icon: Icons.savings_outlined, + tone: totals.balance >= 0 ? AppTone.brandSoft : AppTone.expense, + ), + _StatCell( + label: 'Top earner', + value: totals.topEarner?.name ?? '—', + sub: totals.topEarner == null + ? 'No income yet' + : fmt(_walletIncome(totals.topEarner!)), + icon: Icons.arrow_downward, + tone: AppTone.income, + ), + _StatCell( + label: 'Top spend', + value: totals.topSpender?.name ?? '—', + sub: totals.topSpender == null + ? 'No spend yet' + : fmt(_walletExpense(totals.topSpender!)), + icon: Icons.arrow_upward, + tone: AppTone.expense, + ), + ]; + + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: Column( + children: [ + Row( + children: [ + Expanded(child: _StatTile(cell: cells[0])), + const _Divider(vertical: true), + Expanded(child: _StatTile(cell: cells[1])), + ], + ), + const _Divider(vertical: false), + Row( + children: [ + Expanded(child: _StatTile(cell: cells[2])), + const _Divider(vertical: true), + Expanded(child: _StatTile(cell: cells[3])), + ], + ), + ], + ), + ); + } + + double _walletIncome(WalletEntity w) { + double sum = 0; + for (final txn in transactions) { + if (txn.transaction.walletClientId == w.clientId && + txn.transaction.type == TransactionType.income) { + sum += txn.transaction.amount; + } + } + return sum; + } + + double _walletExpense(WalletEntity w) { + double sum = 0; + for (final txn in transactions) { + if (txn.transaction.walletClientId == w.clientId && + txn.transaction.type == TransactionType.expense) { + sum += txn.transaction.amount; + } + } + return sum; + } +} + +class _StatCell { + final String label; + final String value; + final String? sub; + final IconData icon; + final AppTone tone; + + const _StatCell({ + required this.label, + required this.value, + this.sub, + required this.icon, + required this.tone, + }); +} + +class _StatTile extends StatelessWidget { + final _StatCell cell; + + const _StatTile({required this.cell}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(cell.tone); + + return Container( + color: palette.background, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 30.r, + height: 30.r, + decoration: BoxDecoration( + color: tones.glassBg, + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: tones.borderLight), + ), + alignment: Alignment.center, + child: Icon(cell.icon, size: 14.sp, color: palette.deep), + ), + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + cell.label.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + color: palette.deep.withValues(alpha: 0.85), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 2.h), + Text( + cell.value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w700, + color: palette.ink, + fontFeatures: const [FontFeature.tabularFigures()], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (cell.sub != null) ...[ + SizedBox(height: 1.h), + Text( + cell.sub!, + style: TextStyle( + fontSize: 10.sp, + color: palette.ink.withValues(alpha: 0.6), + fontFeatures: const [FontFeature.tabularFigures()], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ); + } +} + +class _Divider extends StatelessWidget { + final bool vertical; + const _Divider({required this.vertical}); + + @override + Widget build(BuildContext context) { + final color = context.tones.borderLight; + return vertical + ? Container(width: 1, height: 50.h, color: color) + : Container(height: 1, color: color); + } +} diff --git a/lib/presentation/wallets/widgets/wallets_summary.dart b/lib/presentation/wallets/widgets/wallets_summary.dart new file mode 100644 index 00000000..cca29a76 --- /dev/null +++ b/lib/presentation/wallets/widgets/wallets_summary.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Slim single-row wallets summary mirroring [PartiesSummary]: tonal +/// balance chip + In/Out micro-rows + active-count pill. +class WalletsSummary extends StatelessWidget { + final int walletCount; + final int activeCount; + final double income; + final double expense; + final String? defaultCurrency; + + const WalletsSummary({ + super.key, + required this.walletCount, + required this.activeCount, + required this.income, + required this.expense, + this.defaultCurrency, + }); + + double get _balance => income - expense; + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final tone = _balance >= 0 ? AppTone.brand : AppTone.expense; + final palette = tones.tone(tone); + final balanceColor = + _balance >= 0 ? tones.incomeColor : tones.expenseColor; + + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 38.r, + height: 38.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: Icon( + Icons.account_balance_wallet_outlined, + size: 18.sp, + color: palette.deep, + ), + ), + SizedBox(width: 10.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'BALANCE', + style: TextStyle( + fontSize: 9.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: tones.textMuted, + ), + ), + SizedBox(height: 1.h), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + _balance, + compact: true, + ), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: balanceColor, + letterSpacing: -0.3, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + SizedBox(width: 14.w), + Container(width: 1, height: 30.h, color: tones.borderLight), + SizedBox(width: 14.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _InlineStat( + label: 'In', + value: income, + color: tones.incomeColor, + ), + SizedBox(height: 4.h), + _InlineStat( + label: 'Out', + value: expense, + color: tones.expenseColor, + ), + ], + ), + ), + Container( + padding: + EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: tones.brand.background, + borderRadius: BorderRadius.circular(AppRadii.pill), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$activeCount', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + color: tones.brand.deep, + ), + ), + Text( + ' / $walletCount', + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w600, + color: tones.brand.deep.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _InlineStat extends StatelessWidget { + final String label; + final double value; + final Color color; + + const _InlineStat({ + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + SizedBox(width: 6.w), + Text( + label, + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w600, + color: tones.textMuted, + ), + ), + SizedBox(width: 6.w), + Flexible( + child: Text( + CurrencyFormater.formatAmountWithSymbol( + context, + value, + compact: true, + ), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + fontFeatures: const [FontFeature.tabularFigures()], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } +} From bf33f30f31306611566ac3f2fadb45e76e8a39cb Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 00:25:58 +0100 Subject: [PATCH 03/21] feat(stats): Add Month-in-Review recap and rich reports surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The statistics tab leads with a compact recap teaser: brand→brand-soft gradient surface, warm-tinted month chip, In/Out/Net pills, a live 30-day net sparkline, and a glassy play affordance. Tap launches a full-screen Stories-style player with seven slide kinds (opening, cash in, cash out, top category, top payee, biggest expense, closing), animated count-up numbers, staggered slide-in entrances, layered halo illustrations, and tap-zones for prev / pause / next. Picks the most recent month with activity instead of going dark when the current calendar month is empty. A new Reports destination (launched from a labelled pill in the stats bar) bundles period chips (30D/90D/6M/12M), a four-cell KPI grid, weekly cashflow spline area, and a chip-row tab card containing the category donut + ranking, daily bar chart, calendar heatmap, and savings / expense ratios with target markers. --- .../month_in_review/month_in_review_data.dart | 159 +++ .../month_in_review_screen.dart | 1204 +++++++++++++++++ .../reports/charts/calendar_heatmap.dart | 156 +++ .../reports/charts/cashflow_chart.dart | 103 ++ .../reports/charts/category_donut.dart | 86 ++ .../reports/charts/category_ranking.dart | 142 ++ .../reports/charts/daily_bar_chart.dart | 79 ++ .../reports/charts/financial_ratios.dart | 133 ++ .../statistics/reports/report_data.dart | 237 ++++ .../statistics/reports/reports_screen.dart | 540 ++++++++ .../statistics/statistics_screen.dart | 65 +- .../widgets/month_in_review_card.dart | 392 ++++++ 12 files changed, 3293 insertions(+), 3 deletions(-) create mode 100644 lib/presentation/statistics/month_in_review/month_in_review_data.dart create mode 100644 lib/presentation/statistics/month_in_review/month_in_review_screen.dart create mode 100644 lib/presentation/statistics/reports/charts/calendar_heatmap.dart create mode 100644 lib/presentation/statistics/reports/charts/cashflow_chart.dart create mode 100644 lib/presentation/statistics/reports/charts/category_donut.dart create mode 100644 lib/presentation/statistics/reports/charts/category_ranking.dart create mode 100644 lib/presentation/statistics/reports/charts/daily_bar_chart.dart create mode 100644 lib/presentation/statistics/reports/charts/financial_ratios.dart create mode 100644 lib/presentation/statistics/reports/report_data.dart create mode 100644 lib/presentation/statistics/reports/reports_screen.dart create mode 100644 lib/presentation/statistics/widgets/month_in_review_card.dart diff --git a/lib/presentation/statistics/month_in_review/month_in_review_data.dart b/lib/presentation/statistics/month_in_review/month_in_review_data.dart new file mode 100644 index 00000000..0add9909 --- /dev/null +++ b/lib/presentation/statistics/month_in_review/month_in_review_data.dart @@ -0,0 +1,159 @@ +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class TopByName { + final String name; + final double amount; + const TopByName({required this.name, required this.amount}); +} + +class BiggestExpense { + final double amount; + final String party; + final String? category; + final DateTime date; + + const BiggestExpense({ + required this.amount, + required this.party, + this.category, + required this.date, + }); +} + +class MonthInReviewData { + final String monthLabel; + final double income; + final double expense; + final double net; + final double savingsRate; + final TopByName? topCategory; + final TopByName? topPayee; + final BiggestExpense? biggestExpense; + final int transactionCount; + final int daysInMonth; + + const MonthInReviewData({ + required this.monthLabel, + required this.income, + required this.expense, + required this.net, + required this.savingsRate, + required this.topCategory, + required this.topPayee, + required this.biggestExpense, + required this.transactionCount, + required this.daysInMonth, + }); +} + +/// Computes a recap for the most recent month that actually has activity. +/// If [offsetMonths] is 0 (default), the function walks back from "now" and +/// picks the first month where there is at least one transaction — so the +/// card never shows "No activity yet" for an account that only logged +/// transactions in earlier months. A positive [offsetMonths] anchors to +/// that many months before now (used for "previous month" comparisons). +MonthInReviewData? buildMonthInReview( + List transactions, { + int offsetMonths = 0, +}) { + if (transactions.isEmpty) return null; + + final now = DateTime.now(); + late final DateTime target; + + if (offsetMonths != 0) { + target = DateTime(now.year, now.month - offsetMonths, 1); + } else { + DateTime? candidate; + for (var i = 0; i < 24; i++) { + final probe = DateTime(now.year, now.month - i, 1); + final hasAny = transactions.any((t) => + t.transaction.datetime.year == probe.year && + t.transaction.datetime.month == probe.month); + if (hasAny) { + candidate = probe; + break; + } + } + if (candidate == null) { + final latest = transactions + .map((t) => t.transaction.datetime) + .reduce((a, b) => a.isAfter(b) ? a : b); + candidate = DateTime(latest.year, latest.month, 1); + } + target = candidate; + } + final monthEnd = DateTime(target.year, target.month + 1, 0); + + bool sameMonth(DateTime d) => + d.year == target.year && d.month == target.month; + + final inMonth = transactions.where((t) { + return sameMonth(t.transaction.datetime); + }).toList(); + + double income = 0; + double expense = 0; + final catMap = {}; + final payeeMap = {}; + BiggestExpense? biggest; + + for (final t in inMonth) { + final amount = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + income += amount; + } else { + expense += amount; + // Tally by category names (a transaction can have multiple). + if (t.categories.isEmpty) { + catMap['Uncategorized'] = (catMap['Uncategorized'] ?? 0) + amount; + } else { + for (final c in t.categories) { + catMap[c.name] = (catMap[c.name] ?? 0) + amount; + } + } + final partyName = t.party?.name ?? 'Anonymous'; + payeeMap[partyName] = (payeeMap[partyName] ?? 0) + amount; + if (biggest == null || amount > biggest.amount) { + biggest = BiggestExpense( + amount: amount, + party: partyName, + category: t.categories.isNotEmpty ? t.categories.first.name : null, + date: t.transaction.datetime, + ); + } + } + } + + if (income == 0 && expense == 0) return null; + + final net = income - expense; + final savingsRate = income > 0 ? net / income : 0; + + TopByName? top(Map map) { + if (map.isEmpty) return null; + final entries = map.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + final e = entries.first; + return TopByName(name: e.key, amount: e.value); + } + + const months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December', + ]; + + return MonthInReviewData( + monthLabel: '${months[target.month - 1]} ${target.year}', + income: income, + expense: expense, + net: net, + savingsRate: savingsRate.toDouble(), + topCategory: top(catMap), + topPayee: top(payeeMap), + biggestExpense: biggest, + transactionCount: inMonth.length, + daysInMonth: monthEnd.day, + ); +} diff --git a/lib/presentation/statistics/month_in_review/month_in_review_screen.dart b/lib/presentation/statistics/month_in_review/month_in_review_screen.dart new file mode 100644 index 00000000..4ef9a359 --- /dev/null +++ b/lib/presentation/statistics/month_in_review/month_in_review_screen.dart @@ -0,0 +1,1204 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/presentation/statistics/month_in_review/month_in_review_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Instagram-style "Month in Review" story player. +/// +/// Direct port of `MonthInReview.vue`: full-screen modal, top progress bars, +/// tap-left / tap-right to navigate, tap-center to pause, and a slide per +/// data point (opening, income, spending, top category, top payee, biggest +/// expense, closing). Each slide swaps the active tonal surface so the +/// experience feels colorful and varied as the user advances. +class MonthInReviewScreen extends StatefulWidget { + final MonthInReviewData data; + + const MonthInReviewScreen({super.key, required this.data}); + + static Future show(BuildContext context, MonthInReviewData data) { + return Navigator.of(context).push( + PageRouteBuilder( + opaque: false, + barrierColor: Colors.black.withValues(alpha: 0.6), + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (_, __, ___) => MonthInReviewScreen(data: data), + transitionsBuilder: (_, animation, __, child) { + return FadeTransition(opacity: animation, child: child); + }, + ), + ); + } + + @override + State createState() => _MonthInReviewScreenState(); +} + +class _MonthInReviewScreenState extends State + with TickerProviderStateMixin { + static const Duration _slideDuration = Duration(milliseconds: 6500); + + late final List<_Slide> _slides; + late final PageController _pageController; + late final AnimationController _progressController; + int _currentIndex = 0; + bool _paused = false; + + @override + void initState() { + super.initState(); + _slides = _buildSlides(widget.data); + _pageController = PageController(); + _progressController = AnimationController( + vsync: this, + duration: _slideDuration, + )..addStatusListener((status) { + if (status == AnimationStatus.completed) { + _next(); + } + }); + _progressController.forward(); + } + + @override + void dispose() { + _progressController.dispose(); + _pageController.dispose(); + super.dispose(); + } + + void _next() { + if (_currentIndex >= _slides.length - 1) { + Navigator.of(context).maybePop(); + return; + } + setState(() => _currentIndex++); + _pageController.animateToPage( + _currentIndex, + duration: const Duration(milliseconds: 250), + curve: AppMotion.standard, + ); + _progressController + ..reset() + ..forward(); + } + + void _prev() { + if (_currentIndex == 0) { + _progressController + ..reset() + ..forward(); + return; + } + setState(() => _currentIndex--); + _pageController.animateToPage( + _currentIndex, + duration: const Duration(milliseconds: 250), + curve: AppMotion.standard, + ); + _progressController + ..reset() + ..forward(); + } + + void _togglePause() { + setState(() => _paused = !_paused); + if (_paused) { + _progressController.stop(); + } else { + _progressController.forward(); + } + } + + @override + Widget build(BuildContext context) { + final slide = _slides[_currentIndex]; + + return Theme( + // consistent regardless of the app's active theme. + data: ThemeData.dark().copyWith( + extensions: const [AppTones.dark, AppElevations.dark], + ), + child: Builder(builder: (context) { + final palette = context.tones.tone(slide.tone); + return Scaffold( + backgroundColor: Colors.transparent, + body: SafeArea( + child: Container( + margin: EdgeInsets.all(12.r), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: const Color(0xFF111827), + borderRadius: BorderRadius.circular(24.r), + ), + child: Stack( + children: [ + // Tonal aura fills the frame behind the slide content. + Positioned.fill( + child: AnimatedSwitcher( + duration: AppMotion.slow, + child: Container( + key: ValueKey(slide.kind), + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment.topRight, + radius: 1.4, + colors: [ + palette.accent.withValues(alpha: 0.45), + palette.accent.withValues(alpha: 0.06), + ], + ), + ), + ), + ), + ), + // Decorative dot pattern overlay. + const Positioned.fill(child: _DotPattern()), + // Slide carousel. + PageView.builder( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: _slides.length, + itemBuilder: (_, index) { + return _SlideView(slide: _slides[index]); + }, + ), + // Tap zones: left = prev, right = next, center = pause. + Positioned.fill( + child: Row( + children: [ + Expanded( + flex: 1, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _prev, + ), + ), + Expanded( + flex: 1, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _togglePause, + ), + ), + Expanded( + flex: 1, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _next, + ), + ), + ], + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: _Header( + slides: _slides, + currentIndex: _currentIndex, + progressController: _progressController, + monthLabel: widget.data.monthLabel, + onClose: () => Navigator.of(context).maybePop(), + ), + ), + ], + ), + ), + ), + ); + }), + ); + } + + static List<_Slide> _buildSlides(MonthInReviewData d) { + final cadence = d.transactionCount > 0 + ? math.max(1, (d.daysInMonth / d.transactionCount).round()) + : 0; + + final out = <_Slide>[ + _Slide( + kind: _SlideKind.opening, + tone: AppTone.brand, + eyebrow: d.monthLabel, + headline: _monthDescriptor(d.savingsRate), + detail: 'Here is your recap.', + icon: Icons.auto_awesome, + ), + if (d.income > 0) + _Slide( + kind: _SlideKind.income, + tone: AppTone.income, + eyebrow: 'Cash in', + icon: Icons.trending_up, + amount: d.income, + detail: 'Money you brought home this month.', + ) + else + const _Slide( + kind: _SlideKind.income, + tone: AppTone.neutral, + eyebrow: 'Cash in', + icon: Icons.trending_flat, + amount: 0, + detail: 'No income recorded.', + ), + if (d.expense > 0) + _Slide( + kind: _SlideKind.spending, + tone: AppTone.expense, + eyebrow: 'Cash out', + icon: Icons.shopping_bag_outlined, + amount: d.expense, + detail: 'Where your spending went.', + footnote: d.topCategory != null + ? '${d.topCategory!.name} led the way.' + : null, + ), + if (d.topCategory != null) + _Slide( + kind: _SlideKind.topCategory, + tone: AppTone.brandSoft, + eyebrow: 'Top category', + icon: Icons.emoji_events_outlined, + headline: d.topCategory!.name, + amount: d.topCategory!.amount, + footnote: d.expense > 0 + ? '${((d.topCategory!.amount / d.expense) * 100).round()}% of your spending.' + : null, + ), + if (d.topPayee != null) + _Slide( + kind: _SlideKind.topPayee, + tone: AppTone.brand, + eyebrow: 'Your favourite', + icon: Icons.favorite_outline, + headline: d.topPayee!.name, + amount: d.topPayee!.amount, + footnote: 'Total spent with this party.', + ), + if (d.biggestExpense != null) + _Slide( + kind: _SlideKind.biggest, + tone: AppTone.expense, + eyebrow: 'Biggest single expense', + icon: Icons.local_fire_department_outlined, + amount: d.biggestExpense!.amount, + detail: d.biggestExpense!.party, + footnote: d.biggestExpense!.category != null + ? 'in ${d.biggestExpense!.category}' + : null, + ), + _Slide( + kind: _SlideKind.closing, + tone: AppTone.brand, + eyebrow: 'The recap', + icon: Icons.celebration_outlined, + headline: _closingHeadline(d.savingsRate), + detail: cadence > 0 + ? '${d.transactionCount} transactions logged. Roughly one every $cadence days.' + : '${d.transactionCount} transactions logged.', + ), + ]; + + return out; + } + + static String _monthDescriptor(double savingsRate) { + if (savingsRate >= 0.3) return 'A strong saver month.'; + if (savingsRate >= 0.1) return 'A balanced month.'; + if (savingsRate >= 0) return 'A tight month.'; + return 'A stretching month.'; + } + + static String _closingHeadline(double savingsRate) { + if (savingsRate >= 0.3) return 'Great job — you saved a chunk!'; + if (savingsRate >= 0.1) return 'Steady wins.'; + if (savingsRate >= 0) return 'You kept it close.'; + return 'A month to learn from.'; + } +} + +enum _SlideKind { + opening, + income, + spending, + topCategory, + topPayee, + biggest, + closing, +} + +class _Slide { + final _SlideKind kind; + final AppTone tone; + final String eyebrow; + final IconData icon; + final String? headline; + final double? amount; + final String? detail; + final String? footnote; + + const _Slide({ + required this.kind, + required this.tone, + required this.eyebrow, + required this.icon, + this.headline, + this.amount, + this.detail, + this.footnote, + }); +} + +/// Animated count-up for monetary values inside a slide. Eases from 0 to +/// the target as soon as the slide enters the tree, so the hero number +/// "lands" rather than appearing static. +class _AnimatedAmount extends StatelessWidget { + final double value; + final TextStyle style; + + const _AnimatedAmount({required this.value, required this.style}); + + @override + Widget build(BuildContext context) { + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: value), + duration: const Duration(milliseconds: 900), + curve: AppMotion.emphasized, + builder: (ctx, v, _) { + return Text( + CurrencyFormater.formatAmountWithSymbol(ctx, v, compact: false), + style: style, + ); + }, + ); + } +} + +/// Slide-up + fade entrance animation for inline slide content. +class _SlideIn extends StatefulWidget { + final Widget child; + final Duration delay; + + const _SlideIn({required this.child, this.delay = Duration.zero}); + + @override + State<_SlideIn> createState() => _SlideInState(); +} + +class _SlideInState extends State<_SlideIn> + with SingleTickerProviderStateMixin { + late final AnimationController _c = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + late final Animation _offset = + Tween(begin: const Offset(0, 0.12), end: Offset.zero) + .animate(CurvedAnimation(parent: _c, curve: AppMotion.emphasized)); + + @override + void initState() { + super.initState(); + Future.delayed(widget.delay, () { + if (mounted) _c.forward(); + }); + } + + @override + void dispose() { + _c.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _c, + child: SlideTransition(position: _offset, child: widget.child), + ); + } +} + +class _SlideView extends StatelessWidget { + final _Slide slide; + + const _SlideView({required this.slide}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(slide.tone); + + switch (slide.kind) { + case _SlideKind.opening: + return _OpeningSlide(slide: slide, palette: palette); + case _SlideKind.closing: + return _ClosingSlide(slide: slide, palette: palette); + case _SlideKind.income: + case _SlideKind.spending: + case _SlideKind.biggest: + return _AmountSlide(slide: slide, palette: palette); + case _SlideKind.topCategory: + case _SlideKind.topPayee: + return _NamedSlide(slide: slide, palette: palette); + } + } +} + +/// Hero amount layout: huge animated number with a tinted icon orbiting +/// it. Used for "Cash in", "Cash out", and "Biggest single expense". +class _AmountSlide extends StatelessWidget { + final _Slide slide; + final ToneColors palette; + + const _AmountSlide({required this.slide, required this.palette}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(28.r, 90.r, 28.r, 28.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SlideIn( + child: _OrbitedIcon(icon: slide.icon, palette: palette), + ), + const Spacer(), + _SlideIn( + delay: const Duration(milliseconds: 80), + child: _Eyebrow(slide.eyebrow, color: palette.deep), + ), + SizedBox(height: 10.h), + _SlideIn( + delay: const Duration(milliseconds: 160), + child: _AnimatedAmount( + value: slide.amount ?? 0, + style: TextStyle( + fontSize: 52.sp, + fontWeight: FontWeight.w800, + color: palette.deep, + letterSpacing: -1.4, + height: 0.95, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + if (slide.detail != null) ...[ + SizedBox(height: 18.h), + _SlideIn( + delay: const Duration(milliseconds: 260), + child: Text( + slide.detail!, + style: TextStyle( + fontSize: 16.sp, + color: Colors.white.withValues(alpha: 0.82), + height: 1.4, + ), + ), + ), + ], + if (slide.footnote != null) ...[ + SizedBox(height: 18.h), + _SlideIn( + delay: const Duration(milliseconds: 340), + child: _FootnotePill(slide.footnote!), + ), + ], + SizedBox(height: 24.h), + ], + ), + ); + } +} + +/// Opening slide — centered layered ring illustration with month label +/// and a welcoming display headline. +class _OpeningSlide extends StatelessWidget { + final _Slide slide; + final ToneColors palette; + + const _OpeningSlide({required this.slide, required this.palette}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(28.r, 90.r, 28.r, 28.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + _SlideIn( + child: _LayeredRings(palette: palette, child: Icon( + slide.icon, size: 44.sp, color: palette.deep, + )), + ), + SizedBox(height: 28.h), + _SlideIn( + delay: const Duration(milliseconds: 120), + child: _Eyebrow(slide.eyebrow, color: palette.deep, center: true), + ), + SizedBox(height: 12.h), + _SlideIn( + delay: const Duration(milliseconds: 200), + child: Text( + slide.headline ?? 'Your recap', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 36.sp, + fontWeight: FontWeight.w800, + color: Colors.white, + height: 1.05, + letterSpacing: -0.8, + ), + ), + ), + if (slide.detail != null) ...[ + SizedBox(height: 12.h), + _SlideIn( + delay: const Duration(milliseconds: 280), + child: Text( + slide.detail!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15.sp, + color: Colors.white.withValues(alpha: 0.78), + height: 1.4, + ), + ), + ), + ], + const Spacer(), + ], + ), + ); + } +} + +/// Closing slide — celebratory bloom + final message + cadence detail. +class _ClosingSlide extends StatelessWidget { + final _Slide slide; + final ToneColors palette; + + const _ClosingSlide({required this.slide, required this.palette}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(28.r, 90.r, 28.r, 28.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + _SlideIn( + child: _CelebrationBloom(palette: palette, icon: slide.icon), + ), + SizedBox(height: 28.h), + _SlideIn( + delay: const Duration(milliseconds: 120), + child: _Eyebrow(slide.eyebrow, color: palette.deep, center: true), + ), + SizedBox(height: 12.h), + _SlideIn( + delay: const Duration(milliseconds: 200), + child: Text( + slide.headline ?? 'Done.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 30.sp, + fontWeight: FontWeight.w800, + color: Colors.white, + height: 1.1, + letterSpacing: -0.6, + ), + ), + ), + if (slide.detail != null) ...[ + SizedBox(height: 14.h), + _SlideIn( + delay: const Duration(milliseconds: 280), + child: Text( + slide.detail!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withValues(alpha: 0.78), + height: 1.5, + ), + ), + ), + ], + const Spacer(), + ], + ), + ); + } +} + +/// Named-thing slide — used for "Top category" and "Top payee". Hero +/// typography front-and-center with the amount underneath as supporting. +class _NamedSlide extends StatelessWidget { + final _Slide slide; + final ToneColors palette; + + const _NamedSlide({required this.slide, required this.palette}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(28.r, 90.r, 28.r, 28.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SlideIn( + child: _OrbitedIcon(icon: slide.icon, palette: palette), + ), + const Spacer(), + _SlideIn( + delay: const Duration(milliseconds: 80), + child: _Eyebrow(slide.eyebrow, color: palette.deep), + ), + SizedBox(height: 10.h), + _SlideIn( + delay: const Duration(milliseconds: 160), + child: Text( + slide.headline ?? '—', + style: TextStyle( + fontSize: 40.sp, + fontWeight: FontWeight.w800, + color: Colors.white, + height: 1.05, + letterSpacing: -0.8, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + if (slide.amount != null) ...[ + SizedBox(height: 14.h), + _SlideIn( + delay: const Duration(milliseconds: 260), + child: _AnimatedAmount( + value: slide.amount!, + style: TextStyle( + fontSize: 22.sp, + fontWeight: FontWeight.w700, + color: palette.deep, + letterSpacing: -0.4, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + ], + if (slide.footnote != null) ...[ + SizedBox(height: 16.h), + _SlideIn( + delay: const Duration(milliseconds: 340), + child: _FootnotePill(slide.footnote!), + ), + ], + SizedBox(height: 24.h), + ], + ), + ); + } +} + +class _Eyebrow extends StatelessWidget { + final String text; + final Color color; + final bool center; + + const _Eyebrow(this.text, {required this.color, this.center = false}); + + @override + Widget build(BuildContext context) { + return Text( + text.toUpperCase(), + textAlign: center ? TextAlign.center : TextAlign.start, + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w800, + letterSpacing: 2.4, + color: color, + ), + ); + } +} + +class _FootnotePill extends StatelessWidget { + final String text; + const _FootnotePill(this.text); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: Colors.white.withValues(alpha: 0.15)), + ), + child: Text( + text, + style: TextStyle( + fontSize: 13.sp, + color: Colors.white.withValues(alpha: 0.9), + fontWeight: FontWeight.w500, + ), + ), + ); + } +} + +/// Tonal-icon plate with a soft halo orbit. Reused across amount + named +/// slides as the visual anchor. +class _OrbitedIcon extends StatelessWidget { + final IconData icon; + final ToneColors palette; + + const _OrbitedIcon({required this.icon, required this.palette}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 96.r, + height: 96.r, + child: Stack( + alignment: Alignment.center, + children: [ + // Outer soft halo. + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + palette.accent.withValues(alpha: 0.4), + palette.accent.withValues(alpha: 0), + ], + ), + ), + ), + // Dashed orbit. + CustomPaint( + size: Size(80.r, 80.r), + painter: _DashedRingPainter( + color: palette.accent.withValues(alpha: 0.55), + ), + ), + // Inner gradient plate. + Container( + width: 64.r, + height: 64.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + palette.accent.withValues(alpha: 0.95), + palette.deep, + ], + ), + boxShadow: [ + BoxShadow( + color: palette.deep.withValues(alpha: 0.45), + blurRadius: 18, + offset: const Offset(0, 6), + ), + ], + ), + alignment: Alignment.center, + child: Icon(icon, size: 28.sp, color: Colors.white), + ), + ], + ), + ); + } +} + +/// Concentric ring illustration used on the opening slide. +class _LayeredRings extends StatelessWidget { + final ToneColors palette; + final Widget child; + + const _LayeredRings({required this.palette, required this.child}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200.r, + height: 200.r, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + palette.accent.withValues(alpha: 0.4), + palette.accent.withValues(alpha: 0), + ], + ), + ), + ), + CustomPaint( + size: Size(170.r, 170.r), + painter: _DashedRingPainter( + color: palette.accent.withValues(alpha: 0.6), + ), + ), + CustomPaint( + size: Size(120.r, 120.r), + painter: _SolidRingPainter( + color: palette.accent.withValues(alpha: 0.4), + ), + ), + Container( + width: 96.r, + height: 96.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + palette.accent, + palette.deep, + ], + ), + boxShadow: [ + BoxShadow( + color: palette.deep.withValues(alpha: 0.45), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + alignment: Alignment.center, + child: child, + ), + ], + ), + ); + } +} + +/// Celebratory bloom illustration used on the closing slide — confetti +/// dots radiating around a center disc. +class _CelebrationBloom extends StatelessWidget { + final ToneColors palette; + final IconData icon; + + const _CelebrationBloom({required this.palette, required this.icon}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200.r, + height: 200.r, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + palette.accent.withValues(alpha: 0.45), + palette.accent.withValues(alpha: 0), + ], + ), + ), + ), + CustomPaint( + size: Size(200.r, 200.r), + painter: _ConfettiPainter(color: palette.accent), + ), + Container( + width: 96.r, + height: 96.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: palette.deep.withValues(alpha: 0.35), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + alignment: Alignment.center, + child: Icon(icon, size: 44.sp, color: palette.deep), + ), + ], + ), + ); + } +} + +class _DashedRingPainter extends CustomPainter { + final Color color; + _DashedRingPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 1.2 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + final radius = size.shortestSide / 2; + const dash = 3.5, gap = 5.0; + final circumference = 2 * math.pi * radius; + final count = (circumference / (dash + gap)).floor(); + final step = (2 * math.pi) / count; + final arc = (dash / circumference) * 2 * math.pi; + for (var i = 0; i < count; i++) { + canvas.drawArc( + Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: radius, + ), + i * step, + arc, + false, + paint, + ); + } + } + + @override + bool shouldRepaint(covariant _DashedRingPainter old) => old.color != color; +} + +class _SolidRingPainter extends CustomPainter { + final Color color; + _SolidRingPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 1.3 + ..style = PaintingStyle.stroke; + canvas.drawCircle( + Offset(size.width / 2, size.height / 2), + size.shortestSide / 2, + paint, + ); + } + + @override + bool shouldRepaint(covariant _SolidRingPainter old) => old.color != color; +} + +class _ConfettiPainter extends CustomPainter { + final Color color; + _ConfettiPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final rng = math.Random(13); + final center = Offset(size.width / 2, size.height / 2); + final r = size.shortestSide / 2; + for (var i = 0; i < 22; i++) { + final angle = rng.nextDouble() * math.pi * 2; + final distance = r * (0.55 + rng.nextDouble() * 0.45); + final pos = center + Offset(math.cos(angle), math.sin(angle)) * distance; + final shapeSize = 3.0 + rng.nextDouble() * 4; + final paint = Paint() + ..color = color.withValues(alpha: 0.5 + rng.nextDouble() * 0.4); + if (i % 3 == 0) { + canvas.drawCircle(pos, shapeSize, paint); + } else if (i % 3 == 1) { + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter( + center: pos, width: shapeSize * 2.4, height: shapeSize * 0.9), + const Radius.circular(2), + ), + paint, + ); + } else { + canvas.save(); + canvas.translate(pos.dx, pos.dy); + canvas.rotate(angle); + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter(width: shapeSize, height: shapeSize, center: Offset.zero), + const Radius.circular(1.5), + ), + paint, + ); + canvas.restore(); + } + } + } + + @override + bool shouldRepaint(covariant _ConfettiPainter old) => old.color != color; +} + +class _Header extends StatelessWidget { + final List<_Slide> slides; + final int currentIndex; + final AnimationController progressController; + final String monthLabel; + final VoidCallback onClose; + + const _Header({ + required this.slides, + required this.currentIndex, + required this.progressController, + required this.monthLabel, + required this.onClose, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(16.r, 12.r, 16.r, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + for (var i = 0; i < slides.length; i++) + Expanded( + child: Padding( + padding: EdgeInsets.only( + right: i == slides.length - 1 ? 0 : 4.w, + ), + child: _ProgressBar( + controller: progressController, + filled: i < currentIndex, + active: i == currentIndex, + ), + ), + ), + ], + ), + SizedBox(height: 14.h), + Row( + children: [ + Icon( + Icons.calendar_today_outlined, + size: 14.sp, + color: Colors.white.withValues(alpha: 0.85), + ), + SizedBox(width: 6.w), + Text( + monthLabel, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: Colors.white.withValues(alpha: 0.92), + letterSpacing: 0.1, + ), + ), + const Spacer(), + GestureDetector( + onTap: onClose, + child: Container( + width: 32.r, + height: 32.r, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + ), + ), + alignment: Alignment.center, + child: Icon(Icons.close, size: 18.sp, color: Colors.white), + ), + ), + ], + ), + ], + ), + ); + } +} + +class _ProgressBar extends StatelessWidget { + final AnimationController controller; + final bool filled; + final bool active; + + const _ProgressBar({ + required this.controller, + required this.filled, + required this.active, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 3, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(2), + child: filled + ? Container(color: Colors.white.withValues(alpha: 0.95)) + : active + ? AnimatedBuilder( + animation: controller, + builder: (_, __) { + return FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: controller.value, + child: Container( + color: Colors.white.withValues(alpha: 0.95), + ), + ); + }, + ) + : const SizedBox.expand(), + ), + ); + } +} + +class _DotPattern extends StatelessWidget { + const _DotPattern(); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _DotPatternPainter( + color: Colors.white.withValues(alpha: 0.045), + ), + ); + } +} + +class _DotPatternPainter extends CustomPainter { + final Color color; + _DotPatternPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + const spacing = 24.0; + const radius = 1.4; + final paint = Paint()..color = color; + for (double y = 0; y < size.height; y += spacing) { + for (double x = 0; x < size.width; x += spacing) { + canvas.drawCircle(Offset(x + 2, y + 2), radius, paint); + } + } + } + + @override + bool shouldRepaint(covariant _DotPatternPainter old) => old.color != color; +} diff --git a/lib/presentation/statistics/reports/charts/calendar_heatmap.dart b/lib/presentation/statistics/reports/charts/calendar_heatmap.dart new file mode 100644 index 00000000..af0456c5 --- /dev/null +++ b/lib/presentation/statistics/reports/charts/calendar_heatmap.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Mobile port of CalendarHeatmap.vue. Renders a grid of cells, one per day +/// in the period, colored by spending intensity. Weeks run top-to-bottom, +/// like the web heatmap, so a phone-width view still fits ~3 months. +class CalendarHeatmap extends StatelessWidget { + final List daily; + + const CalendarHeatmap({super.key, required this.daily}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + if (daily.isEmpty) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Text( + 'No activity yet.', + style: TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ); + } + + final maxExpense = + daily.fold(0, (m, d) => d.expense > m ? d.expense : m); + + // Group by weeks. Pad the first week so Monday-aligned columns work. + final firstDay = daily.first.date; + final leadPad = (firstDay.weekday - DateTime.monday) % 7; + final cells = []; + for (var i = 0; i < leadPad; i++) { + cells.add(null); + } + cells.addAll(daily); + + return LayoutBuilder(builder: (context, constraints) { + // 7 rows (Mon-Sun), n columns of weeks. + final weeks = (cells.length / 7).ceil(); + final cellSize = + ((constraints.maxWidth - (weeks - 1) * 3.0) / weeks).clamp(8.0, 18.0); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 7 * (cellSize + 3), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var w = 0; w < weeks; w++) + Padding( + padding: EdgeInsets.only(right: w == weeks - 1 ? 0 : 3), + child: Column( + children: [ + for (var d = 0; d < 7; d++) + Padding( + padding: EdgeInsets.only(bottom: d == 6 ? 0 : 3), + child: _Cell( + size: cellSize, + bucket: w * 7 + d < cells.length + ? cells[w * 7 + d] + : null, + maxExpense: maxExpense, + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 8.h), + _Legend(), + ], + ); + }); + } +} + +class _Cell extends StatelessWidget { + final double size; + final DailyBucket? bucket; + final double maxExpense; + + const _Cell({ + required this.size, + required this.bucket, + required this.maxExpense, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + if (bucket == null) { + return SizedBox(width: size, height: size); + } + final ratio = maxExpense > 0 ? (bucket!.expense / maxExpense) : 0; + final intensity = ratio.clamp(0.0, 1.0); + final color = bucket!.expense > 0 + ? Color.alphaBlend( + tones.expenseColor.withValues(alpha: 0.15 + intensity * 0.7), + tones.bgSurface, + ) + : tones.borderLight.withValues(alpha: 0.4); + + return Tooltip( + message: + '${bucket!.date.year}-${bucket!.date.month.toString().padLeft(2, '0')}-${bucket!.date.day.toString().padLeft(2, '0')}\n' + 'Spend: ${bucket!.expense.toStringAsFixed(0)}', + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(3), + ), + ), + ); + } +} + +class _Legend extends StatelessWidget { + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Row( + children: [ + Text( + 'Less', + style: TextStyle(fontSize: 10.sp, color: tones.textMuted), + ), + SizedBox(width: 6.w), + for (final alpha in const [0.15, 0.35, 0.55, 0.75, 0.95]) + Padding( + padding: const EdgeInsets.only(right: 3), + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: tones.expenseColor.withValues(alpha: alpha), + borderRadius: BorderRadius.circular(3), + ), + ), + ), + SizedBox(width: 6.w), + Text( + 'More', + style: TextStyle(fontSize: 10.sp, color: tones.textMuted), + ), + ], + ); + } +} diff --git a/lib/presentation/statistics/reports/charts/cashflow_chart.dart b/lib/presentation/statistics/reports/charts/cashflow_chart.dart new file mode 100644 index 00000000..64ab0f2e --- /dev/null +++ b/lib/presentation/statistics/reports/charts/cashflow_chart.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Income vs expense over time (line + light area), modelled after the web's +/// CashflowLineChart. Aggregates daily buckets into weekly points so the +/// curve is readable on a phone-width canvas. +class CashflowChart extends StatelessWidget { + final List daily; + + const CashflowChart({super.key, required this.daily}); + + List<_Point> _aggregateWeekly() { + if (daily.isEmpty) return const []; + final out = <_Point>[]; + final start = daily.first.date; + final end = daily.last.date; + DateTime cursor = start; + while (!cursor.isAfter(end)) { + final weekEnd = cursor.add(const Duration(days: 6)); + double income = 0; + double expense = 0; + for (final d in daily) { + if (!d.date.isBefore(cursor) && !d.date.isAfter(weekEnd)) { + income += d.income; + expense += d.expense; + } + } + out.add(_Point(week: cursor, income: income, expense: expense)); + cursor = cursor.add(const Duration(days: 7)); + } + return out; + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final points = _aggregateWeekly(); + + return SizedBox( + height: 240.h, + child: SfCartesianChart( + margin: EdgeInsets.zero, + plotAreaBorderWidth: 0, + primaryXAxis: DateTimeAxis( + majorGridLines: const MajorGridLines(width: 0), + axisLine: AxisLine(width: 0.5, color: tones.borderLight), + labelStyle: TextStyle(fontSize: 10.sp, color: tones.textMuted), + dateFormat: null, + ), + primaryYAxis: NumericAxis( + axisLine: const AxisLine(width: 0), + majorTickLines: const MajorTickLines(size: 0), + majorGridLines: MajorGridLines( + width: 0.5, + color: tones.borderLight.withValues(alpha: 0.6), + dashArray: const [4, 4], + ), + labelStyle: TextStyle(fontSize: 10.sp, color: tones.textMuted), + ), + tooltipBehavior: TooltipBehavior( + enable: true, + color: tones.bgCard, + textStyle: TextStyle(color: tones.textPrimary, fontSize: 11.sp), + ), + legend: Legend( + isVisible: true, + position: LegendPosition.bottom, + textStyle: TextStyle(color: tones.textSecondary, fontSize: 11.sp), + ), + series: >[ + SplineAreaSeries<_Point, DateTime>( + name: 'Income', + dataSource: points, + xValueMapper: (p, _) => p.week, + yValueMapper: (p, _) => p.income, + color: tones.incomeColor.withValues(alpha: 0.18), + borderColor: tones.incomeColor, + borderWidth: 2.0, + ), + SplineAreaSeries<_Point, DateTime>( + name: 'Expense', + dataSource: points, + xValueMapper: (p, _) => p.week, + yValueMapper: (p, _) => p.expense, + color: tones.expenseColor.withValues(alpha: 0.18), + borderColor: tones.expenseColor, + borderWidth: 2.0, + ), + ], + ), + ); + } +} + +class _Point { + final DateTime week; + final double income; + final double expense; + _Point({required this.week, required this.income, required this.expense}); +} diff --git a/lib/presentation/statistics/reports/charts/category_donut.dart b/lib/presentation/statistics/reports/charts/category_donut.dart new file mode 100644 index 00000000..cee2e5a1 --- /dev/null +++ b/lib/presentation/statistics/reports/charts/category_donut.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Donut chart of categories. The web equivalent is CategoryDonut.vue. +class CategoryDonut extends StatelessWidget { + final List categories; + final List palette; + final String? centerLabel; + + const CategoryDonut({ + super.key, + required this.categories, + required this.palette, + this.centerLabel, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final top = categories.take(7).toList(); + final total = top.fold(0, (sum, c) => sum + c.amount); + final centerValue = CurrencyFormater.formatAmountWithSymbol( + context, + total, + compact: true, + ); + + return SizedBox( + height: 260.h, + child: SfCircularChart( + margin: EdgeInsets.zero, + legend: Legend( + isVisible: true, + position: LegendPosition.right, + overflowMode: LegendItemOverflowMode.wrap, + textStyle: TextStyle(color: tones.textSecondary, fontSize: 11.sp), + ), + tooltipBehavior: TooltipBehavior(enable: true), + annotations: [ + CircularChartAnnotation( + widget: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (centerLabel != null) + Text( + centerLabel!.toUpperCase(), + style: TextStyle( + fontSize: 9.sp, + letterSpacing: 1.2, + fontWeight: FontWeight.w700, + color: tones.textMuted, + ), + ), + Text( + centerValue, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + ), + ), + ], + ), + ), + ], + series: >[ + DoughnutSeries( + dataSource: top, + xValueMapper: (c, _) => c.name, + yValueMapper: (c, _) => c.amount, + pointColorMapper: (c, i) => Color(palette[i % palette.length]), + innerRadius: '64%', + radius: '88%', + strokeColor: tones.bgSurface, + strokeWidth: 2, + dataLabelSettings: const DataLabelSettings(isVisible: false), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/statistics/reports/charts/category_ranking.dart b/lib/presentation/statistics/reports/charts/category_ranking.dart new file mode 100644 index 00000000..260785ff --- /dev/null +++ b/lib/presentation/statistics/reports/charts/category_ranking.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Horizontal bar ranking of categories. Direct port of the web's +/// CategoryRanking.vue: each row shows the category name, percentage, and +/// amount, with a colored fill bar underneath. +class CategoryRanking extends StatelessWidget { + final List categories; + final List palette; + final int maxRows; + + const CategoryRanking({ + super.key, + required this.categories, + required this.palette, + this.maxRows = 8, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final scope = categories.take(maxRows).toList(); + final maxAmount = + scope.fold(0, (m, c) => c.amount > m ? c.amount : m); + + if (scope.isEmpty) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Text( + 'No category breakdown yet.', + style: TextStyle(fontSize: 13.sp, color: tones.textMuted), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var i = 0; i < scope.length; i++) + Padding( + padding: EdgeInsets.only(bottom: i == scope.length - 1 ? 0 : 12.h), + child: _RankingRow( + category: scope[i], + maxAmount: maxAmount, + color: Color(palette[i % palette.length]), + ), + ), + ], + ); + } +} + +class _RankingRow extends StatelessWidget { + final CategoryAggregate category; + final double maxAmount; + final Color color; + + const _RankingRow({ + required this.category, + required this.maxAmount, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final fillRatio = maxAmount > 0 ? category.amount / maxAmount : 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 8.r, + height: 8.r, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + category.name, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: tones.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '${category.percentage.toStringAsFixed(0)}%', + style: TextStyle( + fontSize: 11.sp, + color: tones.textMuted, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + SizedBox(width: 10.w), + Text( + CurrencyFormater.formatAmountWithSymbol( + context, + category.amount, + compact: true, + ), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + SizedBox(height: 6.h), + ClipRRect( + borderRadius: BorderRadius.circular(999), + child: Container( + height: 6, + color: tones.borderLight.withValues(alpha: 0.5), + child: Row( + children: [ + Expanded( + flex: (fillRatio * 100).round().clamp(0, 100), + child: Container(color: color), + ), + Expanded( + flex: 100 - (fillRatio * 100).round().clamp(0, 100), + child: const SizedBox(), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/statistics/reports/charts/daily_bar_chart.dart b/lib/presentation/statistics/reports/charts/daily_bar_chart.dart new file mode 100644 index 00000000..098ce37f --- /dev/null +++ b/lib/presentation/statistics/reports/charts/daily_bar_chart.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Income / expense per day (last ~30 days). Side-by-side bar chart that +/// matches the web's DailyBarChart. +class DailyBarChart extends StatelessWidget { + final List daily; + final int trailingDays; + + const DailyBarChart({ + super.key, + required this.daily, + this.trailingDays = 30, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final scope = daily.length > trailingDays + ? daily.sublist(daily.length - trailingDays) + : daily; + + return SizedBox( + height: 220.h, + child: SfCartesianChart( + margin: EdgeInsets.zero, + plotAreaBorderWidth: 0, + primaryXAxis: DateTimeAxis( + majorGridLines: const MajorGridLines(width: 0), + axisLine: AxisLine(width: 0.5, color: tones.borderLight), + labelStyle: TextStyle(fontSize: 9.sp, color: tones.textMuted), + intervalType: DateTimeIntervalType.days, + interval: 5, + ), + primaryYAxis: NumericAxis( + axisLine: const AxisLine(width: 0), + majorTickLines: const MajorTickLines(size: 0), + majorGridLines: MajorGridLines( + width: 0.5, + color: tones.borderLight.withValues(alpha: 0.6), + dashArray: const [4, 4], + ), + labelStyle: TextStyle(fontSize: 10.sp, color: tones.textMuted), + ), + tooltipBehavior: TooltipBehavior(enable: true), + legend: Legend( + isVisible: true, + position: LegendPosition.bottom, + textStyle: TextStyle(color: tones.textSecondary, fontSize: 11.sp), + ), + series: >[ + ColumnSeries( + name: 'Income', + dataSource: scope, + xValueMapper: (b, _) => b.date, + yValueMapper: (b, _) => b.income, + color: tones.incomeColor, + width: 0.6, + spacing: 0.15, + borderRadius: BorderRadius.circular(2), + ), + ColumnSeries( + name: 'Expense', + dataSource: scope, + xValueMapper: (b, _) => b.date, + yValueMapper: (b, _) => b.expense, + color: tones.expenseColor, + width: 0.6, + spacing: 0.15, + borderRadius: BorderRadius.circular(2), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/statistics/reports/charts/financial_ratios.dart b/lib/presentation/statistics/reports/charts/financial_ratios.dart new file mode 100644 index 00000000..5bc58a80 --- /dev/null +++ b/lib/presentation/statistics/reports/charts/financial_ratios.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Savings rate and expense ratio summary. Each ratio renders as a tonal +/// row with a progress fill and a percentage caption. +class FinancialRatios extends StatelessWidget { + final ReportTotals totals; + + const FinancialRatios({super.key, required this.totals}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final savings = (totals.savingsRate * 100).clamp(-200, 100).toDouble(); + final expense = (totals.expenseRatio * 100).clamp(0, 200).toDouble(); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _RatioBar( + label: 'Savings rate', + percent: savings, + fillColor: + savings >= 0 ? tones.incomeColor : tones.expenseColor, + target: 20, + ), + SizedBox(height: 16.h), + _RatioBar( + label: 'Expense ratio', + percent: expense, + fillColor: + expense <= 100 ? tones.brand.deep : tones.expenseColor, + target: 80, + referenceLabel: 'Goal: keep under 80%', + ), + ], + ); + } +} + +class _RatioBar extends StatelessWidget { + final String label; + final double percent; + final Color fillColor; + final double target; + final String? referenceLabel; + + const _RatioBar({ + required this.label, + required this.percent, + required this.fillColor, + required this.target, + this.referenceLabel, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final clamped = percent.clamp(0, 100).toDouble(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + label.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + letterSpacing: 1.2, + fontWeight: FontWeight.w700, + color: tones.textMuted, + ), + ), + ), + Text( + '${percent.toStringAsFixed(0)}%', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + SizedBox(height: 6.h), + Stack( + children: [ + Container( + height: 8, + decoration: BoxDecoration( + color: tones.borderLight.withValues(alpha: 0.55), + borderRadius: BorderRadius.circular(999), + ), + ), + FractionallySizedBox( + widthFactor: clamped / 100, + child: Container( + height: 8, + decoration: BoxDecoration( + color: fillColor, + borderRadius: BorderRadius.circular(999), + ), + ), + ), + // Target marker. + Positioned( + left: (target / 100) * MediaQuery.of(context).size.width * 0.85, + child: Container( + height: 14, + width: 2, + color: tones.textPrimary.withValues(alpha: 0.6), + ), + ), + ], + ), + if (referenceLabel != null) ...[ + SizedBox(height: 4.h), + Text( + referenceLabel!, + style: TextStyle( + fontSize: 10.sp, + color: tones.textMuted, + ), + ), + ], + ], + ); + } +} diff --git a/lib/presentation/statistics/reports/report_data.dart b/lib/presentation/statistics/reports/report_data.dart new file mode 100644 index 00000000..aee5f8ac --- /dev/null +++ b/lib/presentation/statistics/reports/report_data.dart @@ -0,0 +1,237 @@ +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +/// Mobile port of the daily/monthly bucket types in `useReportData.ts`. +class DailyBucket { + final DateTime date; + final double income; + final double expense; + final double net; + final int txCount; + + const DailyBucket({ + required this.date, + required this.income, + required this.expense, + required this.net, + required this.txCount, + }); +} + +class MonthlyBucket { + final DateTime month; + final double income; + final double expense; + final double net; + + const MonthlyBucket({ + required this.month, + required this.income, + required this.expense, + required this.net, + }); +} + +class CategoryAggregate { + final String name; + final double amount; + final double percentage; + + const CategoryAggregate({ + required this.name, + required this.amount, + required this.percentage, + }); +} + +class ReportTotals { + final double income; + final double expense; + final double net; + final double savingsRate; + final double expenseRatio; + final int daysInPeriod; + + const ReportTotals({ + required this.income, + required this.expense, + required this.net, + required this.savingsRate, + required this.expenseRatio, + required this.daysInPeriod, + }); +} + +class ReportData { + final DateTime start; + final DateTime end; + final List daily; + final List monthly; + final List expenseCategories; + final List incomeCategories; + final ReportTotals totals; + + const ReportData({ + required this.start, + required this.end, + required this.daily, + required this.monthly, + required this.expenseCategories, + required this.incomeCategories, + required this.totals, + }); +} + +DateTime _startOfDay(DateTime d) => DateTime(d.year, d.month, d.day); +DateTime _startOfMonth(DateTime d) => DateTime(d.year, d.month, 1); + +/// Builds the full report bundle from a transactions list. Period is the +/// last N days inclusive of today (default 90 to mirror the web's +/// `last_3m`). +ReportData buildReportData( + List transactions, { + int periodDays = 90, +}) { + final today = _startOfDay(DateTime.now()); + final start = today.subtract(Duration(days: periodDays - 1)); + final end = today; + + // Index by day for the daily buckets. + final dailyMap = {}; + for (var i = 0; i < periodDays; i++) { + final day = start.add(Duration(days: i)); + dailyMap[day] = _DailyTally(); + } + + final catExpense = {}; + final catIncome = {}; + double totalIncome = 0; + double totalExpense = 0; + + for (final t in transactions) { + final day = _startOfDay(t.transaction.datetime); + if (day.isBefore(start) || day.isAfter(end)) continue; + + final amount = t.transaction.amount; + final tally = dailyMap[day]; + if (tally != null) { + if (t.transaction.type == TransactionType.income) { + tally.income += amount; + } else { + tally.expense += amount; + } + tally.txCount += 1; + } + + if (t.transaction.type == TransactionType.income) { + totalIncome += amount; + final name = t.categories.isNotEmpty + ? t.categories.first.name + : 'Uncategorized'; + catIncome[name] = (catIncome[name] ?? 0) + amount; + } else { + totalExpense += amount; + if (t.categories.isEmpty) { + catExpense['Uncategorized'] = + (catExpense['Uncategorized'] ?? 0) + amount; + } else { + for (final c in t.categories) { + catExpense[c.name] = (catExpense[c.name] ?? 0) + amount; + } + } + } + } + + final daily = dailyMap.entries + .map((e) => DailyBucket( + date: e.key, + income: e.value.income, + expense: e.value.expense, + net: e.value.income - e.value.expense, + txCount: e.value.txCount, + )) + .toList() + ..sort((a, b) => a.date.compareTo(b.date)); + + // Roll up by month. + final monthlyMap = {}; + for (final d in daily) { + final m = _startOfMonth(d.date); + monthlyMap.putIfAbsent(m, () => _DailyTally()); + monthlyMap[m]!.income += d.income; + monthlyMap[m]!.expense += d.expense; + } + final monthly = monthlyMap.entries + .map((e) => MonthlyBucket( + month: e.key, + income: e.value.income, + expense: e.value.expense, + net: e.value.income - e.value.expense, + )) + .toList() + ..sort((a, b) => a.month.compareTo(b.month)); + + List rank(Map map, double base) { + final entries = map.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + return entries + .map((e) => CategoryAggregate( + name: e.key, + amount: e.value, + percentage: base > 0 ? (e.value / base) * 100 : 0, + )) + .toList(); + } + + final net = totalIncome - totalExpense; + + return ReportData( + start: start, + end: end, + daily: daily, + monthly: monthly, + expenseCategories: rank(catExpense, totalExpense), + incomeCategories: rank(catIncome, totalIncome), + totals: ReportTotals( + income: totalIncome, + expense: totalExpense, + net: net, + savingsRate: totalIncome > 0 ? net / totalIncome : 0, + expenseRatio: totalIncome > 0 ? totalExpense / totalIncome : 0, + daysInPeriod: periodDays, + ), + ); +} + +class _DailyTally { + double income = 0; + double expense = 0; + int txCount = 0; +} + +/// Tonal palette used for stacking categories in donuts / ranking bars. +const expensePalette = [ + 0xFFE11D48, + 0xFFF97316, + 0xFFF59E0B, + 0xFF84CC16, + 0xFF10B981, + 0xFF06B6D4, + 0xFF3B82F6, + 0xFF8B5CF6, + 0xFFEC4899, + 0xFF64748B, +]; + +const incomePalette = [ + 0xFF16A34A, + 0xFF10B981, + 0xFF06B6D4, + 0xFF3B82F6, + 0xFF8B5CF6, + 0xFFEC4899, + 0xFFF59E0B, + 0xFFF97316, + 0xFFE11D48, + 0xFF64748B, +]; diff --git a/lib/presentation/statistics/reports/reports_screen.dart b/lib/presentation/statistics/reports/reports_screen.dart new file mode 100644 index 00000000..f8ed1313 --- /dev/null +++ b/lib/presentation/statistics/reports/reports_screen.dart @@ -0,0 +1,540 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/presentation/statistics/month_in_review/month_in_review_data.dart'; +import 'package:trakli/presentation/statistics/month_in_review/month_in_review_screen.dart'; +import 'package:trakli/presentation/statistics/reports/charts/calendar_heatmap.dart'; +import 'package:trakli/presentation/statistics/reports/charts/cashflow_chart.dart'; +import 'package:trakli/presentation/statistics/reports/charts/category_donut.dart'; +import 'package:trakli/presentation/statistics/reports/charts/category_ranking.dart'; +import 'package:trakli/presentation/statistics/reports/charts/daily_bar_chart.dart'; +import 'package:trakli/presentation/statistics/reports/charts/financial_ratios.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/statistics/widgets/month_in_review_card.dart'; +import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Full reports surface, modelled after pages/reports.vue: +/// - Recap teaser at the top +/// - Hero KPIs +/// - Tabbed cashflow / breakdown / activity charts +/// +/// The screen reads transactions from TransactionCubit and computes the +/// report bundle locally, mirroring the web's `useReportData` composable. +class ReportsScreen extends StatefulWidget { + const ReportsScreen({super.key}); + + @override + State createState() => _ReportsScreenState(); +} + +class _ReportsScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabs; + int _periodDays = 90; + + @override + void initState() { + super.initState(); + _tabs = TabController(length: 4, vsync: this); + } + + @override + void dispose() { + _tabs.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final data = buildReportData( + state.transactions, + periodDays: _periodDays, + ); + final recap = buildMonthInReview(state.transactions); + + return Scaffold( + appBar: const PageAppBar(title: 'Reports'), + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MonthInReviewCard( + data: recap, + onTap: recap == null + ? null + : () => MonthInReviewScreen.show(context, recap), + ), + SizedBox(height: 16.h), + _PeriodChips( + selected: _periodDays, + onChange: (v) => setState(() => _periodDays = v), + ), + SizedBox(height: 16.h), + _KpiGrid(totals: data.totals), + SizedBox(height: 16.h), + _SectionCard( + title: 'Cashflow', + subtitle: 'Income vs expense across the period', + child: CashflowChart(daily: data.daily), + ), + SizedBox(height: 16.h), + _TabbedCard( + controller: _tabs, + tabs: const [ + _TabSpec(label: 'Categories', icon: Icons.donut_small), + _TabSpec(label: 'Daily', icon: Icons.calendar_view_day), + _TabSpec(label: 'Calendar', icon: Icons.grid_on), + _TabSpec(label: 'Ratios', icon: Icons.percent), + ], + children: [ + _BreakdownTab(data: data), + _DailyTab(data: data), + _CalendarTab(data: data), + _RatiosTab(totals: data.totals), + ], + ), + SizedBox(height: 24.h), + ], + ), + ), + ); + }, + ); + } +} + +class _PeriodChips extends StatelessWidget { + final int selected; + final ValueChanged onChange; + + const _PeriodChips({required this.selected, required this.onChange}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + const options = [ + (label: '30D', value: 30), + (label: '90D', value: 90), + (label: '6M', value: 180), + (label: '12M', value: 365), + ]; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + for (final opt in options) + GestureDetector( + onTap: () => onChange(opt.value), + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h), + decoration: BoxDecoration( + color: selected == opt.value + ? tones.brand.deep + : tones.bgSurface, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: tones.borderLight), + ), + child: Text( + opt.label, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + color: selected == opt.value + ? Colors.white + : tones.textPrimary, + ), + ), + ), + ), + ], + ); + } +} + +class _KpiGrid extends StatelessWidget { + final ReportTotals totals; + + const _KpiGrid({required this.totals}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final cells = <_KpiCell>[ + _KpiCell( + label: 'Income', + value: CurrencyFormater.formatAmountWithSymbol( + context, + totals.income, + compact: true, + ), + tone: AppTone.income, + icon: Icons.south_west, + ), + _KpiCell( + label: 'Expense', + value: CurrencyFormater.formatAmountWithSymbol( + context, + totals.expense, + compact: true, + ), + tone: AppTone.expense, + icon: Icons.north_east, + ), + _KpiCell( + label: 'Net', + value: CurrencyFormater.formatAmountWithSymbol( + context, + totals.net, + compact: true, + ), + tone: totals.net >= 0 ? AppTone.brand : AppTone.expense, + icon: Icons.swap_vert, + ), + _KpiCell( + label: 'Save rate', + value: '${(totals.savingsRate * 100).toStringAsFixed(0)}%', + tone: totals.savingsRate >= 0 ? AppTone.brandSoft : AppTone.expense, + icon: Icons.bookmark_outline, + ), + ]; + + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: tones.borderLight), + ), + child: Column( + children: [ + Row( + children: [ + Expanded(child: _KpiTile(cell: cells[0])), + Container(width: 1, height: 60.h, color: tones.borderLight), + Expanded(child: _KpiTile(cell: cells[1])), + ], + ), + Container(height: 1, color: tones.borderLight), + Row( + children: [ + Expanded(child: _KpiTile(cell: cells[2])), + Container(width: 1, height: 60.h, color: tones.borderLight), + Expanded(child: _KpiTile(cell: cells[3])), + ], + ), + ], + ), + ); + } +} + +class _KpiCell { + final String label; + final String value; + final AppTone tone; + final IconData icon; + + const _KpiCell({ + required this.label, + required this.value, + required this.tone, + required this.icon, + }); +} + +class _KpiTile extends StatelessWidget { + final _KpiCell cell; + const _KpiTile({required this.cell}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(cell.tone); + return Container( + color: palette.background, + padding: EdgeInsets.all(14.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 26.r, + height: 26.r, + decoration: BoxDecoration( + color: tones.glassBg, + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: tones.borderLight), + ), + alignment: Alignment.center, + child: Icon(cell.icon, size: 14.sp, color: palette.deep), + ), + SizedBox(width: 8.w), + Text( + cell.label.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + letterSpacing: 1.2, + fontWeight: FontWeight.w700, + color: palette.deep, + ), + ), + ], + ), + SizedBox(height: 6.h), + Text( + cell.value, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: palette.ink, + fontFeatures: const [FontFeature.tabularFigures()], + letterSpacing: -0.4, + ), + ), + ], + ), + ); + } +} + +class _SectionCard extends StatelessWidget { + final String title; + final String? subtitle; + final Widget child; + + const _SectionCard({ + required this.title, + this.subtitle, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + padding: EdgeInsets.all(16.r), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + ), + if (subtitle != null) ...[ + SizedBox(height: 2.h), + Text( + subtitle!, + style: TextStyle(fontSize: 12.sp, color: tones.textMuted), + ), + ], + SizedBox(height: 14.h), + child, + ], + ), + ); + } +} + +class _TabSpec { + final String label; + final IconData icon; + const _TabSpec({required this.label, required this.icon}); +} + +class _TabbedCard extends StatelessWidget { + final TabController controller; + final List<_TabSpec> tabs; + final List children; + + const _TabbedCard({ + required this.controller, + required this.tabs, + required this.children, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: Column( + children: [ + AnimatedBuilder( + animation: controller, + builder: (_, __) { + return SizedBox( + height: 52.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 8.h, + ), + itemBuilder: (_, i) { + final active = controller.index == i; + return _TabChip( + label: tabs[i].label, + icon: tabs[i].icon, + active: active, + onTap: () => controller.animateTo(i), + ); + }, + separatorBuilder: (_, __) => SizedBox(width: 6.w), + itemCount: tabs.length, + ), + ); + }, + ), + Divider( + height: 1, + color: tones.borderLight.withValues(alpha: 0.7), + ), + AnimatedBuilder( + animation: controller, + builder: (_, __) { + return Padding( + padding: EdgeInsets.all(16.r), + child: children[controller.index], + ); + }, + ), + ], + ), + ); + } +} + +class _TabChip extends StatelessWidget { + final String label; + final IconData icon; + final bool active; + final VoidCallback onTap; + + const _TabChip({ + required this.label, + required this.icon, + required this.active, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), + decoration: BoxDecoration( + color: active ? tones.brand.deep : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadii.pill), + border: Border.all( + color: active ? tones.brand.deep : tones.borderLight, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 14.sp, + color: active ? Colors.white : tones.textSecondary, + ), + SizedBox(width: 6.w), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + color: active ? Colors.white : tones.textSecondary, + letterSpacing: -0.1, + ), + ), + ], + ), + ), + ); + } +} + +class _BreakdownTab extends StatelessWidget { + final ReportData data; + + const _BreakdownTab({required this.data}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CategoryDonut( + categories: data.expenseCategories, + palette: expensePalette, + centerLabel: 'Expense', + ), + SizedBox(height: 16.h), + CategoryRanking( + categories: data.expenseCategories, + palette: expensePalette, + ), + ], + ); + } +} + +class _DailyTab extends StatelessWidget { + final ReportData data; + const _DailyTab({required this.data}); + + @override + Widget build(BuildContext context) { + return DailyBarChart(daily: data.daily, trailingDays: 30); + } +} + +class _CalendarTab extends StatelessWidget { + final ReportData data; + const _CalendarTab({required this.data}); + + @override + Widget build(BuildContext context) { + return CalendarHeatmap(daily: data.daily); + } +} + +class _RatiosTab extends StatelessWidget { + final ReportTotals totals; + const _RatiosTab({required this.totals}); + + @override + Widget build(BuildContext context) { + return FinancialRatios(totals: totals); + } +} diff --git a/lib/presentation/statistics/statistics_screen.dart b/lib/presentation/statistics/statistics_screen.dart index 8a4d4cf1..f16f2559 100644 --- a/lib/presentation/statistics/statistics_screen.dart +++ b/lib/presentation/statistics/statistics_screen.dart @@ -17,10 +17,15 @@ import 'package:trakli/presentation/currency/cubit/currency_cubit.dart'; import 'package:trakli/presentation/exchange_rate/cubit/exchange_rate_cubit.dart'; import 'package:trakli/presentation/parties/cubit/party_cubit.dart'; import 'package:trakli/presentation/statistics/cubit/statistics_filter_cubit.dart'; +import 'package:trakli/presentation/statistics/month_in_review/month_in_review_data.dart'; +import 'package:trakli/presentation/statistics/month_in_review/month_in_review_screen.dart'; +import 'package:trakli/presentation/statistics/reports/reports_screen.dart'; +import 'package:trakli/presentation/statistics/widgets/month_in_review_card.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/category_tile.dart'; import 'package:trakli/presentation/utils/colors.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/dashboard_expenses.dart'; import 'package:trakli/presentation/utils/dashboard_pie_data.dart'; import 'package:trakli/presentation/utils/enums.dart'; @@ -286,12 +291,41 @@ class _StatisticsScreenState extends State .map((e) => MapEntry(categoryMap[e.key]!, e.value)) .toList(); return Scaffold( - appBar: CustomAppBar( - titleText: LocaleKeys.statistics.tr(), + appBar: PageAppBar( + title: LocaleKeys.statistics.tr(), + showBack: false, + actions: [ + PageAppBarAction( + icon: Icons.bar_chart_rounded, + label: 'Reports', + onTap: () => + AppNavigator.push(context, const ReportsScreen()), + ), + ], ), body: SingleChildScrollView( child: Column( children: [ + SizedBox(height: 16.h), + Builder(builder: (cardContext) { + final recap = buildMonthInReview(transactions); + // Build a 30-day net-flow sparkline for the recap + // card; tiny but it gives the card colour and life. + final spark = _buildSparkline(transactions); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: MonthInReviewCard( + data: recap, + sparkline: spark, + onTap: recap == null + ? null + : () => MonthInReviewScreen.show( + cardContext, + recap, + ), + ), + ); + }), SizedBox(height: 16.h), Container( margin: EdgeInsets.symmetric( @@ -761,6 +795,31 @@ class _StatisticsScreenState extends State }, ); } + + /// Build a 30-day net-flow sparkline for the recap teaser card. Returns + /// a list of daily net values (income − expense) so the card can render + /// a tiny line without owning chart logic. + List _buildSparkline( + List transactions, + ) { + final today = DateTime.now(); + final start = DateTime(today.year, today.month, today.day) + .subtract(const Duration(days: 29)); + final daily = List.filled(30, 0); + for (final t in transactions) { + final d = t.transaction.datetime; + final key = DateTime(d.year, d.month, d.day); + final idx = key.difference(start).inDays; + if (idx < 0 || idx > 29) continue; + final amt = t.transaction.amount; + if (t.transaction.type == TransactionType.income) { + daily[idx] += amt; + } else { + daily[idx] -= amt; + } + } + return daily; + } } // very_good create flutter_app trakli --desc "Trakli" --org "com.whilesmart.trakli" diff --git a/lib/presentation/statistics/widgets/month_in_review_card.dart b/lib/presentation/statistics/widgets/month_in_review_card.dart new file mode 100644 index 00000000..e2d20649 --- /dev/null +++ b/lib/presentation/statistics/widgets/month_in_review_card.dart @@ -0,0 +1,392 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/utils/currency_formater.dart'; +import 'package:trakli/presentation/statistics/month_in_review/month_in_review_data.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +class MonthInReviewCard extends StatelessWidget { + final MonthInReviewData? data; + final VoidCallback? onTap; + final List sparkline; + + const MonthInReviewCard({ + super.key, + required this.data, + this.onTap, + this.sparkline = const [], + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(AppTone.brand); + final disabled = data == null; + + return Material( + color: Colors.transparent, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppRadii.lg), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + tones.brandSoft.background, + palette.background, + ], + ), + border: Border.all(color: palette.accent.withValues(alpha: 0.55)), + ), + child: InkWell( + borderRadius: BorderRadius.circular(AppRadii.lg), + onTap: disabled ? null : onTap, + child: Stack( + children: [ + Positioned( + top: -36.r, + right: -36.r, + child: IgnorePointer( + child: Container( + width: 110.r, + height: 110.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + palette.accent.withValues(alpha: 0.45), + palette.accent.withValues(alpha: 0), + ], + ), + ), + ), + ), + ), + Positioned( + bottom: 4.r, + left: 8.r, + child: IgnorePointer( + child: CustomPaint( + size: Size(60.r, 14.r), + painter: _FloatingDotsPainter( + color: palette.accent, + warm: tones.accentWarm, + ), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, vertical: 1.h), + decoration: BoxDecoration( + color: tones.accentWarmSoft, + borderRadius: + BorderRadius.circular(AppRadii.xs), + border: Border.all( + color: tones.accentWarm + .withValues(alpha: 0.55), + ), + ), + child: Text( + (disabled ? 'Recap' : data!.monthLabel) + .toUpperCase(), + style: TextStyle( + fontSize: 8.sp, + fontWeight: FontWeight.w800, + letterSpacing: 1.2, + color: tones.accentWarm, + ), + ), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + disabled + ? 'Log a few first' + : 'Your month in review', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w800, + color: tones.textPrimary, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 6.h), + if (!disabled) + Row( + children: [ + Expanded(child: _RecapStatRow(data: data!)), + if (sparkline.isNotEmpty) ...[ + SizedBox(width: 8.w), + SizedBox( + width: 56.w, + height: 16.h, + child: CustomPaint( + painter: _SparklinePainter( + values: sparkline, + color: palette.deep, + ), + size: Size.infinite, + ), + ), + ], + ], + ) + else + Text( + 'Once you have activity, this replays the month.', + style: TextStyle( + fontSize: 11.sp, + color: tones.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + SizedBox(width: 8.w), + _PlayAffordance(disabled: disabled, palette: palette), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _PlayAffordance extends StatelessWidget { + final bool disabled; + final ToneColors palette; + + const _PlayAffordance({required this.disabled, required this.palette}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + width: 38.r, + height: 38.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: disabled + ? null + : LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [palette.accent, palette.deep], + ), + color: disabled ? tones.borderLight.withValues(alpha: 0.6) : null, + boxShadow: disabled + ? null + : [ + BoxShadow( + color: palette.deep.withValues(alpha: 0.35), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.only(left: 2.w), + child: Icon( + Icons.play_arrow_rounded, + size: 22.sp, + color: disabled ? tones.textMuted : Colors.white, + ), + ), + ); + } +} + +class _RecapStatRow extends StatelessWidget { + final MonthInReviewData data; + + const _RecapStatRow({required this.data}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Wrap( + spacing: 4.w, + runSpacing: 2.h, + children: [ + _StatChip( + label: 'In', + value: CurrencyFormater.formatAmountWithSymbol( + context, + data.income, + compact: true, + ), + color: tones.incomeColor, + ), + _StatChip( + label: 'Out', + value: CurrencyFormater.formatAmountWithSymbol( + context, + data.expense, + compact: true, + ), + color: tones.expenseColor, + ), + _StatChip( + label: 'Net', + value: CurrencyFormater.formatAmountWithSymbol( + context, + data.net, + compact: true, + ), + color: data.net >= 0 ? tones.incomeColor : tones.expenseColor, + ), + ], + ); + } +} + +class _StatChip extends StatelessWidget { + final String label; + final String value; + final Color color; + + const _StatChip({ + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.h), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.16), + borderRadius: BorderRadius.circular(AppRadii.xs), + border: Border.all(color: color.withValues(alpha: 0.35)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: TextStyle( + fontSize: 8.sp, + fontWeight: FontWeight.w800, + color: color, + letterSpacing: 0.4, + ), + ), + SizedBox(width: 3.w), + Text( + value, + style: TextStyle( + fontSize: 9.sp, + fontWeight: FontWeight.w800, + color: color, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ); + } +} + +class _SparklinePainter extends CustomPainter { + final List values; + final Color color; + + _SparklinePainter({required this.values, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + if (values.length < 2) return; + final maxV = values.reduce(math.max); + final minV = values.reduce(math.min); + final range = (maxV - minV).abs(); + final scale = range == 0 ? 1.0 : 1 / range; + final stepX = size.width / (values.length - 1); + + final path = Path(); + for (var i = 0; i < values.length; i++) { + final x = i * stepX; + final y = size.height - ((values[i] - minV) * scale) * size.height; + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + } + + final fill = Path.from(path) + ..lineTo(size.width, size.height) + ..lineTo(0, size.height) + ..close(); + canvas.drawPath( + fill, + Paint()..color = color.withValues(alpha: 0.15), + ); + canvas.drawPath( + path, + Paint() + ..color = color + ..strokeWidth = 1.4 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round, + ); + } + + @override + bool shouldRepaint(covariant _SparklinePainter old) => + old.values != values || old.color != color; +} + +class _FloatingDotsPainter extends CustomPainter { + final Color color; + final Color warm; + _FloatingDotsPainter({required this.color, required this.warm}); + + @override + void paint(Canvas canvas, Size size) { + final rng = math.Random(11); + for (var i = 0; i < 5; i++) { + final x = rng.nextDouble() * size.width; + final y = rng.nextDouble() * size.height; + final r = 1.2 + rng.nextDouble() * 2; + final useWarm = i == 1 || i == 3; + canvas.drawCircle( + Offset(x, y), + r, + Paint() + ..color = (useWarm ? warm : color) + .withValues(alpha: 0.3 + rng.nextDouble() * 0.3), + ); + } + } + + @override + bool shouldRepaint(covariant _FloatingDotsPainter old) => + old.color != color || old.warm != warm; +} From e7bd2a8e6fbb1d1876831380b5101edd1100a59a Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 01:01:40 +0100 Subject: [PATCH 04/21] enh(auth): Refresh login, onboarding and theme toggle Login landing pairs a brand-soft tonal hero (logo + tagline + restored side illustration) with a clear eyebrow / display heading and tonal primary / outlined / OAuth buttons under an OR divider. Email login swaps the heavy default TabBar for a tonal segmented Email/Phone control, gains labelled fields, and adopts the shared page header so back navigation and styling match the rest of the app. Onboarding is a three-slide carousel with curated illustrations: - Simplified personal finance (wallet) - Automated (AI importer reads receipts, PDFs, CSV, XLS; integrations like Plaid and MCP) - Open source (yours to inspect, host and trust) Theme toggle lives in the top-right of both onboarding and the login landing so visitors can flip light / dark before they sign in. The white wordmark variant is in for dark mode. --- assets/images/onboarding/importer.svg | 1 + assets/images/onboarding/insights.svg | 1 + assets/images/onboarding/ready.svg | 1 + assets/images/onboarding/wallet.svg | 1 + assets/images/trakli-logo-white.png | Bin 0 -> 2951 bytes assets/translations/en.json | 8 +- lib/presentation/auth/pages/login_screen.dart | 462 ++++++++++++----- .../auth/pages/login_with_email_screen.dart | 465 +++++++++++------- .../onboarding/onboarding_screen.dart | 370 +++++++------- .../utils/theme_toggle_button.dart | 46 ++ pubspec.yaml | 1 + 11 files changed, 890 insertions(+), 466 deletions(-) create mode 100644 assets/images/onboarding/importer.svg create mode 100644 assets/images/onboarding/insights.svg create mode 100644 assets/images/onboarding/ready.svg create mode 100644 assets/images/onboarding/wallet.svg create mode 100644 assets/images/trakli-logo-white.png create mode 100644 lib/presentation/utils/theme_toggle_button.dart 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/trakli-logo-white.png b/assets/images/trakli-logo-white.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f609085b8eae0d537e9ae801728bef7d783f73 GIT binary patch literal 2951 zcmZ{mcQhLe8^uGE1hu!us8+4EM$MW@h*4^n;3YMyMU>Wv-DquEGc_Y>#;U!mp-7Fk zTH{3(CAONe%I7=h`}aHNxz9cKo^${I#hRKRE;9-+0sw%^2KsRG3nyL36hwdVixRsL z7Y0V_+xh_jOq~A}4d87K-$hB|XO4gZs)nzuTm(9I9b+8;pf;K56m{t$v-s=U_*?k6 z`3In$x&rhbxqG4VYBQWK_Ou3Y9m_zPjohK9)1TP;&I|PKmdBATt<2N)%jEqBe(Z7b zh0t1L+Bc}JlHM(HvW3uA6{%=GeKwH(ofExJ*EpiOUMf{aPV~Q~+E_pBOMR#vWdu+- zw3qqORW2GJSI6fht1&t{ssNzVVf%l#+6Nn2_Swds?MohtzhaMe*rNH%i)FE>kWCR; zAMMIJDle!tQ6*;QGJbodvR zoBVqLdXR4ugc{v>?r%-Jyqq`{>wN1!99^W?0O1>KSQaxR-<}paIXI#lCt!I+H>%da z8GK1+8SoB^D&8cb>~+}YrxACOCg8>%m7zUAYP`hAE?4jFzPF8uAM{Z;`W|LZx%p}q zW>%-@V8^|)sl-1Sb_cZhIq1MRFYcr0VoNSHTY2Q1BQUSCFTcd19cgDL=n!7OWoT-` zXQTM`jjSbNdxDl54|QR(2Dva&Lc>zhZWW9m`rYA=lp&+T3N;$-t`VEL`IY$`4aU^* z7B?o3;tr<(io@8R*&@us5rkIf`c6TJ}D znL@;Pn_WQkqu14b^ACUcVuQgl+jhG_tNkLWkt}EDGx?`$tY;Ob5%qa^2ElxD-023U z9yA+hOv>p5^gyL+3w5p_S!!cCH<3Kl>|?0 z7(A12Iqq27XZMtg{%{O61x^EN4E;y; z8J^6@7Hgj+Ciix`lA_q49ytTQQ@a~}P&<_!A9xgK1cf8X5p!3W+iq(8f|XAb#k^b2 zKG-{EDU6u9u|$L~c)m7O1%}{hyuGEpLxO#}Iy6)?66t?Q-x>E?*zQ6|#Ky~BDGZ&+ zCE4;lycca*N<&yp5b=LIt98t(jBL0U*qXr4gFlZOzF9vEW%yDwb_ECK)v-~j0xt7^ z+CTyKyD1MN`K}6NOZamw07;zYb*glg9hu6V17r6SQ~Fl;H#hz^sV#=LkXvCh9*kjX%K2FEmdyvkUTbc}5M-mV`TIRoi#IYOvkib3KJmzxK|^8@Q2m zSE!KQd^r2l_3)x|jjo3MrK6h~!ePM&lrTdf&FI?e$)ULt4_-a~Nk9`n%iYSFop{Nyr_u&dwJB2 zjJDa+?VimRsjnW`++jV{9j)rNa@yBlE6Zo^=xkNP9j$V)Ve4kE63ji?c)e}imfXo( zngTsnLCmw6$)45)>W{^!#cR6JUSAcy1hmP3m=igbb{6h01Rmt=n=w~$b^NA*1kJI0 z$+2UmvSBL>xZ+%S9 zIz~MVC4ABzHxL{eBOoCG%63r1H+dN&ONZL79|~<1YoG~wbTQa@^l-4-Xk>0^&tS06 z!RqvY!BQr_%do`E(W4+7j% zl9Xa5wzX>Ry_3WWs66^yZJ58c*B2=r9`RgIV*F||BQ0~g@D zxSbXuzppYZVsRCB@`>t+yVWizQR+K+LE2ojY?~vluC@% zHQDI=J9B(C${YL4d5@09Jzgqpo6v7BFHQgaTB z_S;+NMfXu+fG>_h$=J1otgQ<;N=_Yvynoy0u<;sZJHIUb5uW)i4&Fhc2lw{l zgbDCO)O@1Br0?b&8Eyk`*dS~Og7ks7E?7XQpHwKFJg*-+$Nv;be{@*e*^8j~0EHDE zM>8=(Ej%gSan)Q@md3|ZwUw;4*EYZ;$sx1l-H-B2tw4o}j%o{@`EYoMYpih4;&Vlr z4NMwIWCJkRO zayOE!M503){Sj2M=Z|`|68c^yr$B3rWV?W15%@=UujK|oFM*Nrt#l*6%mJA-E@h`7 zQL`1E3;b9SXPnp?G3h{{qO@;}vWRuE*xur18dm2 { @override Widget build(BuildContext context) { + final tones = context.tones; + return BlocListener( listener: (context, state) { state.when( initial: () {}, - submitting: () { - showLoader(); - }, + submitting: () => showLoader(), success: (user) { hideLoader(); showSnackBar( message: LocaleKeys.signInSuccessful.tr(), borderRadius: 8.r, - backgroundColor: Colors.green, + backgroundColor: tones.incomeColor, isFloating: false, ); - // Optionally navigate to home or handle success }, error: (failure) { hideLoader(); - failure.maybeWhen( - orElse: () { - showSnackBar( - message: failure.customMessage, - borderRadius: 8.r, - backgroundColor: appDangerColor, - isFloating: false, - ); - }, + orElse: () => showSnackBar( + message: failure.customMessage, + borderRadius: 8.r, + backgroundColor: appDangerColor, + isFloating: false, + ), cancel: () {}, ); }, ); }, child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: SingleChildScrollView( - padding: EdgeInsets.only( - left: 16.w, - right: 16.w, - bottom: 24.h, - ), - child: Column( - children: [ - SizedBox(height: 60.h), - Text( - LocaleKeys.welcomeTo.tr(), - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.w700, - ), + backgroundColor: tones.bgPage, + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - + MediaQuery.of(context).padding.vertical - + 24.h, ), - SizedBox(height: 2.h), - SvgPicture.asset(Assets.images.logoGreen), - SvgPicture.asset(Assets.images.loginLogo), - SizedBox(height: 20.h), - SizedBox( - width: double.infinity, - height: 54.h, - child: PrimaryButton( - onPress: () { - Navigator.push( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 8.h), + const Align( + alignment: Alignment.centerRight, + child: ThemeToggleButton(), + ), + SizedBox(height: 12.h), + _BrandHero(), + SizedBox(height: 28.h), + _SectionEyebrow(text: LocaleKeys.welcomeTo.tr()), + SizedBox(height: 6.h), + Text( + LocaleKeys.login.tr(), + style: TextStyle( + fontSize: 26.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.4, + ), + ), + SizedBox(height: 6.h), + Text( + 'Pick a method to sign in. Skip to try the app first.', + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, + height: 1.5, + ), + ), + SizedBox(height: 22.h), + _PrimaryAuthButton( + label: LocaleKeys.login.tr(), + onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => const LoginWithEmailScreen(), ), - ); - }, - buttonText: LocaleKeys.login.tr(), - buttonTextColor: Colors.black, - backgroundColor: const Color(0xFFDFE1E4), - ), - ), - SizedBox(height: 12.h), - SizedBox( - width: double.infinity, - height: 54.h, - child: PrimaryButton( - onPress: () { - Navigator.push( + ), + ), + SizedBox(height: 10.h), + _SecondaryAuthButton( + label: LocaleKeys.createAccount.tr(), + onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => const RegisterScreen(), ), - ); - }, - buttonText: LocaleKeys.createAccount.tr(), - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 12.sp), - child: Row( - spacing: 28.w, - children: [ - const Expanded( - child: Divider( - height: 0, - color: Color(0xFF79828E), - ), ), - Text(LocaleKeys.or.tr()), - const Expanded( - child: Divider( - height: 0, - color: Color(0xFF79828E), - ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 18.h), + child: Row( + children: [ + Expanded( + child: Divider( + height: 0, + color: tones.borderLight, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Text( + LocaleKeys.or.tr(), + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: tones.textMuted, + ), + ), + ), + Expanded( + child: Divider( + height: 0, + color: tones.borderLight, + ), + ), + ], + ), + ), + _OAuthButton( + label: LocaleKeys.loginGoogle.tr(), + iconPath: Assets.images.google, + onTap: () => + context.read().signInWithGoogle(), + ), + if (Platform.isIOS) ...[ + SizedBox(height: 10.h), + _OAuthButton( + label: LocaleKeys.loginApple.tr(), + iconPath: Assets.images.apple, + onTap: () => + context.read().signInWithApple(), ), ], - ), - ), - SizedBox( - height: 54.h, - child: PrimaryButton( - onPress: () => context.read().signInWithGoogle(), - iconPath: Assets.images.google, - borderColor: const Color(0xFF79828E), - backgroundColor: Theme.of(context).colorScheme.surface, - buttonText: LocaleKeys.loginGoogle.tr(), - buttonTextColor: Theme.of(context).colorScheme.onSurface, - ), + SizedBox(height: 14.h), + Center( + child: TextButton( + onPressed: () => AppNavigator.push( + context, + const OnboardSettingsScreen(), + ), + child: Text( + LocaleKeys.skip.tr(), + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: tones.textSecondary, + ), + ), + ), + ), + SizedBox(height: 8.h), + ], ), - if (Platform.isIOS) ...[ - SizedBox(height: 12.h), - SizedBox( - height: 54.h, - child: PrimaryButton( - onPress: () => context.read().signInWithApple(), - iconPath: Assets.images.apple, - borderColor: const Color(0xFF79828E), - backgroundColor: Colors.white, - buttonText: LocaleKeys.loginApple.tr(), - buttonTextColor: textColor, + ), + ), + ), + ), + ); + } +} + +class _BrandHero extends StatelessWidget { + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(AppTone.brandSoft); + + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: palette.accent.withValues(alpha: 0.5)), + ), + child: Stack( + children: [ + // imported illustration assets. + Positioned( + top: -80.r, + right: -80.r, + child: IgnorePointer( + child: Container( + width: 240.r, + height: 240.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + palette.accent.withValues(alpha: 0.35), + palette.accent.withValues(alpha: 0), + ], ), ), - ], - SizedBox(height: 8.h), - TextButton( - onPressed: () { - AppNavigator.push( - context, - const OnboardSettingsScreen(), - ); - }, - style: ButtonStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - side: const BorderSide( - color: Colors.transparent, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 22.w, vertical: 22.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.images.logoGreen, + height: 32.h, ), - ), + SizedBox(height: 12.h), + Text( + 'Track every spend.\nKeep every receipt.', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.4, + height: 1.2, + ), + ), + SizedBox(height: 6.h), + Text( + 'Built for the long-term picture.', + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + height: 1.4, + ), + ), + ], ), ), - child: Text( - LocaleKeys.skip.tr(), - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(fontSize: 13.sp), + SizedBox(width: 12.w), + SvgPicture.asset( + Assets.images.loginLogo, + height: 110.h, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SectionEyebrow extends StatelessWidget { + final String text; + const _SectionEyebrow({required this.text}); + + @override + Widget build(BuildContext context) { + return Text( + text.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: context.tones.brand.deep, + ), + ); + } +} + +class _PrimaryAuthButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + + const _PrimaryAuthButton({required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + backgroundColor: tones.brand.deep, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + elevation: 0, + ), + child: Text( + label, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + letterSpacing: -0.1, + ), + ), + ), + ); + } +} + +class _SecondaryAuthButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + + const _SecondaryAuthButton({required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: onTap, + style: OutlinedButton.styleFrom( + backgroundColor: tones.bgSurface, + foregroundColor: tones.textPrimary, + padding: EdgeInsets.symmetric(vertical: 15.h), + side: BorderSide(color: tones.borderMedium), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + letterSpacing: -0.1, + color: tones.textPrimary, + ), + ), + ), + ); + } +} + +class _OAuthButton extends StatelessWidget { + final String label; + final String iconPath; + final VoidCallback onTap; + + const _OAuthButton({ + required this.label, + required this.iconPath, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(AppRadii.lg), + child: Container( + height: 52.h, + width: double.infinity, + decoration: BoxDecoration( + color: tones.bgSurface, + border: Border.all(color: tones.borderMedium), + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(iconPath, height: 20.h), + SizedBox(width: 10.w), + Text( + label, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.1, ), ), ], diff --git a/lib/presentation/auth/pages/login_with_email_screen.dart b/lib/presentation/auth/pages/login_with_email_screen.dart index dda19853..5d227678 100644 --- a/lib/presentation/auth/pages/login_with_email_screen.dart +++ b/lib/presentation/auth/pages/login_with_email_screen.dart @@ -3,19 +3,17 @@ import 'package:flutter/gestures.dart' show TapGestureRecognizer; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/auth/cubits/login/login_cubit.dart'; import 'package:trakli/presentation/auth/pages/forgot_password_screen.dart'; import 'package:trakli/presentation/auth/pages/register_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/buttons.dart'; -import 'package:trakli/presentation/utils/colors.dart'; import 'package:trakli/presentation/utils/custom_phone_field.dart'; import 'package:trakli/presentation/utils/custom_text_field.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/helpers.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class LoginWithEmailScreen extends StatefulWidget { const LoginWithEmailScreen({super.key}); @@ -24,25 +22,19 @@ class LoginWithEmailScreen extends StatefulWidget { State createState() => _LoginWithEmailScreenState(); } -class _LoginWithEmailScreenState extends State - with SingleTickerProviderStateMixin { +class _LoginWithEmailScreenState extends State { final TextEditingController emailController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); final GlobalKey formKey = GlobalKey(); late final TapGestureRecognizer _recognizerTap; - late TabController _tabController; RegisterType loginType = RegisterType.email; String? _phoneNumber; @override void initState() { - _tabController = TabController(length: 2, vsync: this); _recognizerTap = TapGestureRecognizer() ..onTap = () { - AppNavigator.pushReplacement( - context, - const RegisterScreen(), - ); + AppNavigator.pushReplacement(context, const RegisterScreen()); }; super.initState(); } @@ -51,188 +43,166 @@ class _LoginWithEmailScreenState extends State void dispose() { emailController.dispose(); passwordController.dispose(); - _tabController.dispose(); _recognizerTap.dispose(); super.dispose(); } + void _submit(BuildContext context) { + if (!formKey.currentState!.validate()) return; + if (loginType == RegisterType.email) { + context.read().loginWithEmailPassword( + email: emailController.text, + password: passwordController.text, + ); + } else if (_phoneNumber != null && _phoneNumber!.isNotEmpty) { + context.read().loginWithPhonePassword( + phone: _phoneNumber!, + password: passwordController.text, + ); + } + } + @override Widget build(BuildContext context) { + final tones = context.tones; + return BlocListener( listener: (context, state) { state.when( - initial: () {}, - submitting: () { - showLoader(); - }, - success: (user) { - hideLoader(); - }, - error: (failure) { - hideLoader(); - showSnackBar( - message: failure.customMessage, - borderRadius: 8.r, - ); - }, - resetCode: (response) {}, - resetPassword: (response) {}); + initial: () {}, + submitting: showLoader, + success: (_) => hideLoader(), + error: (failure) { + hideLoader(); + showSnackBar( + message: failure.customMessage, + borderRadius: 8.r, + ); + }, + resetCode: (_) {}, + resetPassword: (_) {}, + ); }, child: Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 0.08.sh), - Container( - width: 48.sp, - height: 32.sp, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(8), + backgroundColor: tones.bgPage, + appBar: const PageAppBar(title: 'Sign in'), + body: SafeArea( + top: false, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(24.w, 8.h, 24.w, 24.h), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 6.h), + _AuthEyebrow(text: LocaleKeys.welcomeTo.tr()), + SizedBox(height: 6.h), + Text( + LocaleKeys.login.tr(), + style: TextStyle( + fontSize: 28.sp, + fontWeight: FontWeight.w800, + color: tones.textPrimary, + letterSpacing: -0.5, + ), ), - child: TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: SvgPicture.asset(Assets.images.arrowLeft), + SizedBox(height: 6.h), + Text( + 'Welcome back. Pick how you signed up.', + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, + height: 1.5, + ), ), - ), - SizedBox(height: 12.sp), - Text( - LocaleKeys.login.tr(), - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w700, + SizedBox(height: 22.h), + _MethodSegmented( + selected: loginType, + onChange: (v) => setState(() => loginType = v), ), - ), - TabBar( - indicatorColor: appPrimaryColor, - labelColor: appPrimaryColor, - controller: _tabController, - onTap: (index) { - setState(() { - loginType = RegisterType.values.elementAt(index); - }); - }, - tabs: [ - Tab( - text: LocaleKeys.email.tr(), - ), - Tab( - text: LocaleKeys.phoneNumber.tr(), - ) - ], - ), - SizedBox(height: 28.h), - Text( - loginType == RegisterType.email - ? LocaleKeys.email.tr() - : LocaleKeys.phoneNumber.tr(), - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w700, + SizedBox(height: 20.h), + _FieldLabel( + text: loginType == RegisterType.email + ? LocaleKeys.email.tr() + : LocaleKeys.phoneNumber.tr(), ), - ), - SizedBox(height: 8.h), - if (loginType == RegisterType.email) + SizedBox(height: 6.h), + if (loginType == RegisterType.email) + CustomTextField( + controller: emailController, + hintText: LocaleKeys.email.tr(), + filled: true, + validator: validateEmail, + ) + else + CustomPhoneField( + onChanged: (number) => + _phoneNumber = number.completeNumber, + ), + SizedBox(height: 16.h), + _FieldLabel(text: LocaleKeys.password.tr()), + SizedBox(height: 6.h), CustomTextField( - controller: emailController, - hintText: LocaleKeys.email.tr(), + controller: passwordController, + hintText: LocaleKeys.password.tr(), + isPassword: true, filled: true, - validator: validateEmail, - ) - else - CustomPhoneField( - onChanged: (number) { - _phoneNumber = number.completeNumber; - }, - ), - SizedBox(height: 16.h), - Text( - LocaleKeys.password.tr(), - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w700, + validator: validatePassword, ), - ), - SizedBox(height: 8.h), - CustomTextField( - controller: passwordController, - hintText: LocaleKeys.password.tr(), - isPassword: true, - filled: true, - validator: validatePassword, - ), - SizedBox(height: 24.h), - SizedBox( - width: double.infinity, - height: 54.h, - child: Builder(builder: (context) { - return PrimaryButton( - onPress: () { - if (formKey.currentState!.validate()) { - if (loginType == RegisterType.email) { - context.read().loginWithEmailPassword( - email: emailController.text, - password: passwordController.text, - ); - } else { - if (_phoneNumber != null && - _phoneNumber!.isNotEmpty) { - context.read().loginWithPhonePassword( - phone: _phoneNumber!, - password: passwordController.text, - ); - } - } - } - }, - buttonText: LocaleKeys.login.tr(), - buttonTextColor: Colors.white, - ); - }), - ), - SizedBox(height: 4.h), - Align( - alignment: Alignment.topRight, - child: TextButton( - onPressed: () { - AppNavigator.push(context, const ForgotPasswordScreen()); - }, - child: Text( - LocaleKeys.forgotPassword.tr(), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => AppNavigator.push( + context, + const ForgotPasswordScreen(), + ), + style: TextButton.styleFrom( + foregroundColor: tones.brand.deep, + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 6.h, + ), + ), + child: Text( + LocaleKeys.forgotPassword.tr(), + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + ), ), ), - ), - SizedBox(height: 16.h), - Align( - child: RichText( - text: TextSpan( - text: LocaleKeys.dontHaveAccount.tr(), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 14.sp, - ), - children: [ - const TextSpan( - text: ' ', + SizedBox(height: 16.h), + _PrimaryAuthButton( + label: LocaleKeys.login.tr(), + onTap: () => _submit(context), + ), + SizedBox(height: 18.h), + Center( + child: RichText( + text: TextSpan( + text: LocaleKeys.dontHaveAccount.tr(), + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, ), - TextSpan( - text: LocaleKeys.register.tr(), - style: TextStyle( - color: appPrimaryColor, + children: [ + TextSpan( + text: ' ${LocaleKeys.register.tr()}', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: tones.brand.deep, + ), + recognizer: _recognizerTap, ), - recognizer: _recognizerTap, - ) - ], + ], + ), ), ), - ), - ], + ], + ), ), ), ), @@ -240,3 +210,158 @@ class _LoginWithEmailScreenState extends State ); } } + +/// Small caps eyebrow used above the page heading. +class _AuthEyebrow extends StatelessWidget { + final String text; + const _AuthEyebrow({required this.text}); + + @override + Widget build(BuildContext context) { + return Text( + text.toUpperCase(), + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w800, + letterSpacing: 1.4, + color: context.tones.brand.deep, + ), + ); + } +} + +/// Label sitting above a form field. Uses textPrimary so it reads as a +/// proper section heading, not a placeholder hint. +class _FieldLabel extends StatelessWidget { + final String text; + const _FieldLabel({required this.text}); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: context.tones.textPrimary, + letterSpacing: -0.1, + ), + ); + } +} + +/// Segmented control for the email / phone toggle. Replaces the default +/// Material TabBar so the form has a consistent tonal pill style. +class _MethodSegmented extends StatelessWidget { + final RegisterType selected; + final ValueChanged onChange; + + const _MethodSegmented({required this.selected, required this.onChange}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + decoration: BoxDecoration( + color: tones.brand.background, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + ), + padding: EdgeInsets.all(4.r), + child: Row( + children: [ + Expanded( + child: _SegmentItem( + label: 'Email', + active: selected == RegisterType.email, + onTap: () => onChange(RegisterType.email), + ), + ), + Expanded( + child: _SegmentItem( + label: 'Phone', + active: selected == RegisterType.phone, + onTap: () => onChange(RegisterType.phone), + ), + ), + ], + ), + ); + } +} + +class _SegmentItem extends StatelessWidget { + final String label; + final bool active; + final VoidCallback onTap; + + const _SegmentItem({ + required this.label, + required this.active, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + padding: EdgeInsets.symmetric(vertical: 9.h), + decoration: BoxDecoration( + color: active ? tones.bgSurface : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadii.md), + boxShadow: active ? context.elevations.level1 : null, + ), + alignment: Alignment.center, + child: Text( + label, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: active ? tones.brand.deep : tones.textSecondary, + letterSpacing: -0.1, + ), + ), + ), + ); + } +} + +class _PrimaryAuthButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + + const _PrimaryAuthButton({required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + backgroundColor: tones.brand.deep, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + ), + elevation: 0, + ), + child: Text( + label, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + letterSpacing: -0.1, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/onboarding/onboarding_screen.dart b/lib/presentation/onboarding/onboarding_screen.dart index 2e4a79b7..66361ead 100644 --- a/lib/presentation/onboarding/onboarding_screen.dart +++ b/lib/presentation/onboarding/onboarding_screen.dart @@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncfusion_flutter_gauges/gauges.dart'; -import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/app_widget.dart'; import 'package:trakli/presentation/auth/pages/login_screen.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/theme_toggle_button.dart'; class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); @@ -19,31 +19,44 @@ class _OnboardingScreenState extends State { late PageController pageController = PageController(); int currentIndex = 0; - navigateToNextPage() { - // Set onboarding mode to false when leaving onboarding + late final List<_OnboardSlide> _slides = const [ + _OnboardSlide( + tone: AppTone.brand, + asset: 'assets/images/onboarding/wallet.svg', + title: LocaleKeys.onboardTitle1, + description: LocaleKeys.onboardDesc1, + ), + _OnboardSlide( + tone: AppTone.warm, + asset: 'assets/images/onboarding/importer.svg', + title: LocaleKeys.onboardTitle2, + description: LocaleKeys.onboardDesc2, + ), + _OnboardSlide( + tone: AppTone.brandSoft, + asset: 'assets/images/onboarding/ready.svg', + title: LocaleKeys.onboardTitle3, + description: LocaleKeys.onboardDesc3, + ), + ]; + + void navigateToNextPage() { setOnboardingMode(false); Navigator.pushReplacement( context, - MaterialPageRoute( - builder: (context) => const LoginScreen(), - ), + MaterialPageRoute(builder: (context) => const LoginScreen()), ); } void nextSlide() { - if (currentIndex == 2) { + if (currentIndex >= _slides.length - 1) { navigateToNextPage(); - } else { - pageController.nextPage( - duration: const Duration(milliseconds: 850), - curve: Curves.decelerate, - ); + return; } - } - - @override - void initState() { - super.initState(); + pageController.nextPage( + duration: AppMotion.slow, + curve: AppMotion.emphasized, + ); } @override @@ -60,183 +73,198 @@ class _OnboardingScreenState extends State { @override Widget build(BuildContext context) { + final tones = context.tones; + final activeSlide = _slides[currentIndex]; + final activePalette = tones.tone(activeSlide.tone); + final isLast = currentIndex == _slides.length - 1; + return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: Stack( - children: [ - Container( - width: double.infinity, - color: Theme.of(context).primaryColor, - height: 0.65.sh, - child: Column( - children: [ - SizedBox(height: 0.2.sh), - SvgPicture.asset( - Assets.images.logo, - height: 120.h, - ), - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - width: double.infinity, - height: 0.45.sh, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 28), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Color(0x26000000), - offset: Offset(0, 4), - blurRadius: 16, + backgroundColor: tones.bgPage, + body: SafeArea( + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 0), + child: Row( + children: [ + const ThemeToggleButton(), + const Spacer(), + AnimatedOpacity( + duration: AppMotion.base, + opacity: isLast ? 0 : 1, + child: TextButton( + onPressed: isLast ? null : navigateToNextPage, + style: TextButton.styleFrom( + foregroundColor: tones.textSecondary, + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 8.h, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(999), + ), + ), + child: Text( + LocaleKeys.skip.tr(), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + letterSpacing: 0.1, + ), + ), + ), ), ], ), + ), + Expanded( + child: PageView.builder( + controller: pageController, + onPageChanged: (v) => setState(() => currentIndex = v), + itemCount: _slides.length, + physics: const BouncingScrollPhysics(), + itemBuilder: (_, i) => _SlidePage(slide: _slides[i]), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(28.w, 8.h, 28.w, 28.h), child: Column( children: [ - Expanded( - child: PageView( - controller: pageController, - onPageChanged: (value) { - setState(() { - currentIndex = value; - }); - }, - physics: const NeverScrollableScrollPhysics(), - children: [ - pageItem( - title: LocaleKeys.onboardTitle1.tr(), - desc: LocaleKeys.onboardDesc1.tr(), + _ProgressDots( + count: _slides.length, + index: currentIndex, + activeColor: activePalette.deep, + idleColor: tones.borderMedium, + ), + SizedBox(height: 24.h), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: nextSlide, + style: ElevatedButton.styleFrom( + backgroundColor: activePalette.deep, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(11.r), ), - pageItem( - title: LocaleKeys.onboardTitle2.tr(), - desc: LocaleKeys.onboardDesc2.tr(), + elevation: 0, + textStyle: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + letterSpacing: -0.1, ), - pageItem( - title: LocaleKeys.onboardTitle3.tr(), - desc: LocaleKeys.onboardDesc3.tr(), - isLastPage: true, + ), + child: AnimatedSwitcher( + duration: AppMotion.base, + child: Text( + isLast ? LocaleKeys.go.tr() : LocaleKeys.next.tr(), + key: ValueKey(isLast), ), - ], + ), ), ), - SizedBox( - height: 80.sp, - width: 80.sp, - child: animatedProgress, - ), - SizedBox(height: 2.h), - TextButton( - onPressed: currentIndex != 2 ? navigateToNextPage : null, - child: Text( - currentIndex != 2 ? LocaleKeys.skip.tr() : "", - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 16.sp, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.center, - ), - ), - SizedBox(height: 4.h), ], ), ), - ), - ], + ], + ), ), ); } +} + +class _OnboardSlide { + final AppTone tone; + final String? asset; + final String title; + final String description; - Widget get animatedProgress { - return SfRadialGauge( - animationDuration: 3000, - axes: [ - RadialAxis( - minimum: 0, - maximum: 100, - showLabels: false, - showTicks: false, - startAngle: 270, - endAngle: 270, - axisLineStyle: const AxisLineStyle( - thickness: 0.2, - cornerStyle: CornerStyle.bothFlat, - color: Color(0xFFFFFBE6), - thicknessUnit: GaugeSizeUnit.factor, + const _OnboardSlide({ + required this.tone, + required this.asset, + required this.title, + required this.description, + }); +} + +class _SlidePage extends StatelessWidget { + final _OnboardSlide slide; + + const _SlidePage({required this.slide}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 28.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Center( + child: slide.asset != null + ? SvgPicture.asset(slide.asset!, fit: BoxFit.contain) + : const SizedBox.shrink(), + ), ), - pointers: [ - RangePointer( - color: Theme.of(context).primaryColor, - value: ((currentIndex + 1) / 3) * 100, - width: 0.2, - sizeUnit: GaugeSizeUnit.factor, - enableAnimation: true, - animationDuration: 3500, - ) - ], - annotations: [ - GaugeAnnotation( - widget: Padding( - padding: const EdgeInsets.all(4), - child: Container( - margin: const EdgeInsets.all(8), - width: 80, - height: 80, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).primaryColor, - ), - child: IconButton( - onPressed: nextSlide, - icon: currentIndex != 2 - ? const Icon( - Icons.arrow_forward_outlined, - color: Colors.white, - ) - : Text( - LocaleKeys.go.tr().toUpperCase(), - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - ), - ), - ), - ), - ), - ) - ], - ) - ], + Text( + slide.title.tr(), + style: TextStyle( + fontSize: 28.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.5, + height: 1.15, + ), + ), + SizedBox(height: 12.h), + Text( + slide.description.tr(), + style: TextStyle( + fontSize: 15.sp, + color: tones.textSecondary, + height: 1.5, + ), + ), + SizedBox(height: 8.h), + ], + ), ); } +} + +class _ProgressDots extends StatelessWidget { + final int count; + final int index; + final Color activeColor; + final Color idleColor; - Widget pageItem({ - required String title, - required String desc, - bool isLastPage = false, - }) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + const _ProgressDots({ + required this.count, + required this.index, + required this.activeColor, + required this.idleColor, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - title, - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.center, - ), - Text( - desc, - style: TextStyle( - fontSize: 14.sp, + for (var i = 0; i < count; i++) + AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + margin: EdgeInsets.symmetric(horizontal: 4.w), + height: 8, + width: i == index ? 28 : 8, + decoration: BoxDecoration( + color: i == index ? activeColor : idleColor.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(999), + ), ), - textAlign: TextAlign.center, - ), ], ); } diff --git a/lib/presentation/utils/theme_toggle_button.dart b/lib/presentation/utils/theme_toggle_button.dart new file mode 100644 index 00000000..e8fef6a1 --- /dev/null +++ b/lib/presentation/utils/theme_toggle_button.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/presentation/config/theme_cubit/theme_cubit.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +class ThemeToggleButton extends StatelessWidget { + const ThemeToggleButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, mode) { + final isDark = mode == ThemeMode.dark || + (mode == ThemeMode.system && + MediaQuery.platformBrightnessOf(context) == Brightness.dark); + final tones = context.tones; + return Material( + color: tones.bgSurface, + shape: CircleBorder( + side: BorderSide(color: tones.borderLight), + ), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => context.read().updateThemeByEnum( + isDark ? ThemeMode.light : ThemeMode.dark, + ), + child: SizedBox( + width: 40.r, + height: 40.r, + child: Center( + child: Icon( + isDark + ? Icons.dark_mode_outlined + : Icons.light_mode_outlined, + size: 18.sp, + color: tones.brand.deep, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 88c2a2ca..fc2d5ce1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -156,6 +156,7 @@ flutter: assets: - assets/translations/ - assets/images/ + - assets/images/onboarding/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see From 408023b6b6a9cfe0e8befc7cdd38c740315cf663 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 01:01:59 +0100 Subject: [PATCH 05/21] enh(home): Brand-mark drawer trigger, profile hero, orange nav accent Home keeps the dedicated drawer trigger SVG icon and shows the Trakli wordmark logo in the title (icon-only mark is reserved for non-home root pages). Profile uses a tonal brand hero with avatar derived from the user's initials (deterministic hashed pastel background), name and email beside it, plus a single grouped surface for Account info and Log out as tonal action tiles. Bottom navigation active state now uses the Trakli warm accent so the "you are here" signal pops without competing with the dominant green. --- lib/presentation/home_screen.dart | 75 ++-- lib/presentation/profile_screen.dart | 397 ++++++++++++++---- .../root/main_navigation_screen.dart | 3 +- 3 files changed, 347 insertions(+), 128 deletions(-) diff --git a/lib/presentation/home_screen.dart b/lib/presentation/home_screen.dart index ad4e4be5..f395aad0 100644 --- a/lib/presentation/home_screen.dart +++ b/lib/presentation/home_screen.dart @@ -25,7 +25,9 @@ import 'package:trakli/presentation/utils/all_wallets_tile.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/bottom_sheets/pick_group_bottom_sheet.dart'; import 'package:trakli/presentation/utils/colors.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/globals.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/helpers.dart'; import 'package:trakli/presentation/utils/transaction_expansion_tile.dart'; import 'package:trakli/presentation/utils/wallet_tile.dart'; @@ -193,29 +195,33 @@ class _HomeScreenState extends State { selectedGroup = selectedGroup ?? groups.firstOrNull; return Scaffold( - appBar: CustomAppBar( - title: SvgPicture.asset( - Assets.images.logoGreen, - height: 38.h, - ), - actions: [ - GestureDetector( - onTap: () { - AppNavigator.push( - context, - const NotificationsScreen(), - ); - }, + appBar: PageAppBar( + title: '', + titleWidget: Theme.of(context).brightness == Brightness.dark + ? Image.asset( + 'assets/images/trakli-logo-white.png', + height: 28.h, + fit: BoxFit.contain, + ) + : SvgPicture.asset( + Assets.images.logoGreen, + height: 30.h, + ), + leading: Padding( + padding: EdgeInsets.only(left: 12.w), + child: InkWell( + onTap: () => scaffoldKey.currentState?.openDrawer(), + borderRadius: BorderRadius.circular(AppRadii.md), child: Container( - width: 42.r, - height: 42.r, - padding: EdgeInsets.all(12.r), + width: 40.r, + height: 40.r, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: Theme.of(context).primaryColor.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(AppRadii.md), + color: Theme.of(context).primaryColor.withAlpha(50), ), + padding: EdgeInsets.all(9.r), child: SvgPicture.asset( - Assets.images.notificationBing, + Assets.images.category, colorFilter: ColorFilter.mode( Theme.of(context).primaryColor, BlendMode.srcIn, @@ -223,32 +229,25 @@ class _HomeScreenState extends State { ), ), ), - SizedBox(width: 12.w), - GestureDetector( + ), + actions: [ + PageAppBarAction( + icon: Icons.notifications_none_rounded, + onTap: () => AppNavigator.push( + context, + const NotificationsScreen(), + ), + ), + PageAppBarAction( + icon: Icons.refresh_rounded, onTap: () { final isAuthenticated = context.read().state.isAuthenticated; - if (isAuthenticated) { getIt().sync(); } }, - child: Container( - width: 42.r, - height: 42.r, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: Theme.of(context).primaryColor.withValues(alpha: 0.2), - ), - // padding: EdgeInsets.all(14.r), - child: Icon( - Icons.refresh, - size: 20.sp, - color: Theme.of(context).primaryColor, - ), - ), ), - SizedBox(width: 16.w), ], ), body: BlocConsumer( diff --git a/lib/presentation/profile_screen.dart b/lib/presentation/profile_screen.dart index 41a3732a..86820616 100644 --- a/lib/presentation/profile_screen.dart +++ b/lib/presentation/profile_screen.dart @@ -1,4 +1,3 @@ -import 'package:country_flags/country_flags.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,10 +12,9 @@ import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/account_info_screen.dart'; import 'package:trakli/presentation/auth/cubits/auth/auth_cubit.dart'; import 'package:trakli/presentation/benefits/benefits_widget.dart'; -import 'package:trakli/presentation/utils/action_tile.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/colors.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/helpers.dart'; @@ -86,114 +84,335 @@ class ProfileScreen extends StatelessWidget { Widget build(BuildContext context) { final user = context.read().state.user; + final tones = context.tones; + return Scaffold( - appBar: CustomAppBar( - titleText: LocaleKeys.profile.tr(), + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: LocaleKeys.profile.tr(), + showBack: false, ), body: SingleChildScrollView( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 8.h, - ), + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 24.h), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - CircleAvatar( - radius: 48.r, - child: Stack( - children: [ - SizedBox( - width: double.infinity, - height: double.infinity, - child: Icon( - Icons.person, - size: 56.r, + _ProfileHeader(user: user), + SizedBox(height: 18.h), + if (user != null) ...[ + const PremiumTile(), + SizedBox(height: 14.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Text( + 'ACCOUNT', + style: TextStyle( + fontSize: 10.sp, + fontWeight: FontWeight.w700, + letterSpacing: 1.4, + color: tones.textSecondary, + ), + ), + ), + SizedBox(height: 8.h), + Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + _ProfileActionTile( + iconPath: Assets.images.user, + label: LocaleKeys.accountInfo.tr(), + tone: AppTone.brand, + onTap: () => AppNavigator.push( + context, + const AccountInfoScreen(), + ), ), + Divider( + height: 1, + indent: 60.w, + color: tones.borderLight.withValues(alpha: 0.6), + ), + _ProfileActionTile( + iconPath: Assets.images.logout, + label: LocaleKeys.logOut.tr(), + tone: AppTone.expense, + onTap: () => _handleLogout(context), + ), + ], + ), + ), + ] else + const BenefitsWidget(), + ], + ), + ), + ); + } +} + +class _ProfileHeader extends StatelessWidget { + final dynamic user; + + const _ProfileHeader({required this.user}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(AppTone.brand); + + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.xl), + border: Border.all(color: palette.accent.withValues(alpha: 0.5)), + ), + child: Stack( + children: [ + // Soft bloom for visual weight without a banner feel. + Positioned( + top: -80.r, + right: -80.r, + child: IgnorePointer( + child: Container( + width: 220.r, + height: 220.r, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + palette.accent.withValues(alpha: 0.32), + palette.accent.withValues(alpha: 0), + ], ), - if (user != null) - Positioned( - right: 0.w, - bottom: 2.h, + ), + ), + ), + ), + Padding( + padding: EdgeInsets.all(20.r), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _InitialsAvatar( + user: user, + size: 64.r, + shadowColor: palette.deep.withValues(alpha: 0.18), + ), + SizedBox(width: 14.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + user?.fullName ?? LocaleKeys.anonymous.tr(), + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + letterSpacing: -0.3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + if (user != null) + Text( + user.email, + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + else + Text( + 'Not signed in', + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, + ), + ), + ], + ), + ), + if (user != null) + Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(999), child: Container( - padding: EdgeInsets.all(6.r), + padding: EdgeInsets.all(8.r), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withValues( - alpha: 0.2, - ), + color: tones.glassBg, shape: BoxShape.circle, + border: Border.all(color: tones.borderLight), ), child: SvgPicture.asset( Assets.images.edit2, + height: 16.h, + width: 16.w, colorFilter: ColorFilter.mode( - Theme.of(context).primaryColor, + palette.deep, BlendMode.srcIn, ), ), ), ), - ], - ), + ), + ], ), - SizedBox(height: 4.h), - Text( - user?.fullName ?? LocaleKeys.anonymous.tr(), - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, + ), + ], + ), + ); + } +} + +class _ProfileActionTile extends StatelessWidget { + final String iconPath; + final String label; + final AppTone tone; + final VoidCallback onTap; + + const _ProfileActionTile({ + required this.iconPath, + required this.label, + required this.tone, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final palette = tones.tone(tone); + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h), + child: Row( + children: [ + Container( + width: 36.r, + height: 36.r, + decoration: BoxDecoration( + color: palette.background, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: SvgPicture.asset( + iconPath, + width: 18.w, + height: 18.h, + colorFilter: + ColorFilter.mode(palette.deep, BlendMode.srcIn), + ), ), - ), - if (user != null) - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8.w, - children: [ - CountryFlag.fromCountryCode( - shape: const Circle(), - "cm", - width: 24.w, + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: tones.textPrimary, ), - Flexible( - child: Text( - user.email, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: Colors.grey[400], - ), - ), - ), - ], + ), ), - SizedBox(height: 12.h), - if (user != null) - Column( - mainAxisSize: MainAxisSize.min, - spacing: 12.h, - children: [ - const PremiumTile(), - ActionTile( - title: LocaleKeys.accountInfo.tr(), - iconPath: Assets.images.user, - actionColor: appPrimaryColor, - onTap: () { - AppNavigator.push(context, const AccountInfoScreen()); - }, - ), - ActionTile( - title: LocaleKeys.logOut.tr(), - iconPath: Assets.images.logout, - actionColor: Colors.red, - onTap: () async { - await _handleLogout(context); - }, - ), - ], - ) - else - const BenefitsWidget(), - SizedBox(height: 0.2.sh), - ], + Icon( + Icons.chevron_right, + size: 18.sp, + color: tones.textMuted, + ), + ], + ), + ), + ), + ); + } +} + +class _InitialsAvatar extends StatelessWidget { + final dynamic user; + final double size; + final Color shadowColor; + + const _InitialsAvatar({ + required this.user, + required this.size, + required this.shadowColor, + }); + + String _initials() { + final name = user?.fullName as String?; + if (name == null || name.trim().isEmpty) return '?'; + final parts = + name.trim().split(RegExp(r'\s+')).where((p) => p.isNotEmpty).toList(); + if (parts.isEmpty) return '?'; + if (parts.length == 1) return parts.first[0].toUpperCase(); + return (parts.first[0] + parts.last[0]).toUpperCase(); + } + + Color _bgColor() { + const palette = [ + Color(0xFFB6E3F4), + Color(0xFFC4B5FD), + Color(0xFFA78BFA), + Color(0xFFFB7185), + Color(0xFFFDBA74), + Color(0xFFFDE047), + Color(0xFFA7F3D0), + Color(0xFFFBB6CE), + Color(0xFF93C5FD), + ]; + final seed = (user?.email ?? user?.fullName ?? 'guest').toString(); + var hash = 0; + for (var i = 0; i < seed.length; i++) { + hash = (hash * 31 + seed.codeUnitAt(i)) & 0x7fffffff; + } + return palette[hash % palette.length]; + } + + Color _onColor(Color bg) { + final luminance = bg.computeLuminance(); + return luminance > 0.55 + ? const Color(0xFF1F2937) + : Colors.white; + } + + @override + Widget build(BuildContext context) { + final bg = _bgColor(); + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: bg, + boxShadow: [ + BoxShadow(color: shadowColor, blurRadius: 18, offset: const Offset(0, 6)), + ], + ), + alignment: Alignment.center, + child: Text( + _initials(), + style: TextStyle( + fontSize: size * 0.4, + fontWeight: FontWeight.w800, + color: _onColor(bg), + letterSpacing: -0.5, ), ), ); diff --git a/lib/presentation/root/main_navigation_screen.dart b/lib/presentation/root/main_navigation_screen.dart index afc4f5ae..11327bf7 100644 --- a/lib/presentation/root/main_navigation_screen.dart +++ b/lib/presentation/root/main_navigation_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/core/sync/sync_database.dart'; import 'package:trakli/di/injection.dart'; import 'package:trakli/gen/assets.gen.dart'; @@ -112,7 +113,7 @@ class _MainNavigationScreenState extends State { ], backgroundColor: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.onSurface, - selectedColor: Theme.of(context).primaryColor, + selectedColor: context.tones.accentWarm, ), ); }, From d9394a424aba820d3de1dd8982c63b2fafcd60c4 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 01:03:27 +0100 Subject: [PATCH 06/21] enh(transactions): Tonal segmented type picker on add-transaction Add-transaction adopts the shared page header and replaces the saturated green TabBar with a tonal segmented Expense/Income picker tinted by context. Body switches via IndexedStack behind a horizontal-drag detector so swipe still flips the tab but content swaps instantly, removing the cross-fade flash on the Record button colour change. The Record button accent is now the canonical danger red on expense and primary green on income, instead of always green. --- lib/presentation/add_transaction_screen.dart | 289 ++++++++++++------- 1 file changed, 180 insertions(+), 109 deletions(-) diff --git a/lib/presentation/add_transaction_screen.dart b/lib/presentation/add_transaction_screen.dart index 550c6dcf..630a9ab0 100644 --- a/lib/presentation/add_transaction_screen.dart +++ b/lib/presentation/add_transaction_screen.dart @@ -1,13 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:trakli/domain/entities/transaction_complete_entity.dart'; -import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/transactions/add_transaction_form_compact_layout.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/colors.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/forms/add_transaction_form.dart'; import 'package:trakli/providers/local_storage.dart'; @@ -62,121 +61,88 @@ class _AddTransactionScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - headerTextColor: const Color(0xFFEBEDEC), - titleText: widget.transaction != null + backgroundColor: context.tones.bgPage, + appBar: PageAppBar( + title: widget.transaction != null ? LocaleKeys.editTransaction.tr() : LocaleKeys.addTransaction.tr(), ), body: Column( children: [ - TabBar( - controller: tabController, - indicatorSize: TabBarIndicatorSize.tab, - indicatorWeight: 3, - indicator: BoxDecoration( - color: (tabController.index != 0) - ? Theme.of(context).primaryColor.withValues(alpha: 0.1) - : const Color(0xFFEB5757).withValues(alpha: 0.15), - border: Border( - bottom: BorderSide( - width: 3, - color: (tabController.index != 0) - ? Theme.of(context).primaryColor - : const Color(0xFFEB5757), - ), - )), - unselectedLabelStyle: TextStyle( - fontSize: 16.sp, + if (widget.transaction == null) + Padding( + padding: EdgeInsets.fromLTRB(16.w, 14.h, 16.w, 10.h), + child: _TypeSegmented( + controller: tabController, + ), ), - labelStyle: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: (tabController.index == 1) - ? Theme.of(context).primaryColor - : const Color(0xFFEB5757), - ), - tabs: [ - if (widget.transaction == null || - widget.transaction!.transaction.type == - TransactionType.expense) - Tab( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4.w, - children: [ - SvgPicture.asset( - Assets.images.arrowSwapUp, - colorFilter: ColorFilter.mode( - (tabController.index != 0) - ? Theme.of(context).colorScheme.onSurface - : const Color(0xFFEB5757), - BlendMode.srcIn, + Expanded( + child: ColoredBox( + color: context.tones.bgPage, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onHorizontalDragEnd: (details) { + final v = details.primaryVelocity ?? 0; + if (v.abs() < 200) return; + if (v < 0 && tabController.index < tabController.length - 1) { + tabController.animateTo(tabController.index + 1); + } else if (v > 0 && tabController.index > 0) { + tabController.animateTo(tabController.index - 1); + } + }, + child: AnimatedBuilder( + animation: tabController, + builder: (_, __) { + final showExpense = widget.transaction == null || + widget.transaction!.transaction.type == + TransactionType.expense; + final showIncome = widget.transaction == null || + widget.transaction!.transaction.type == + TransactionType.income; + final children = [ + if (showExpense) + KeyedSubtree( + key: const ValueKey('tx-form-expense'), + child: formDisplay == 'full' + ? AddTransactionForm( + transactionType: TransactionType.expense, + accentColor: appDangerColor, + transactionCompleteEntity: + widget.transaction, + ) + : AddTransactionFormCompactLayout( + transactionType: TransactionType.expense, + accentColor: appDangerColor, + transactionCompleteEntity: + widget.transaction, + ), ), - ), - Text( - LocaleKeys.transactionExpense.tr(), - ) - ], - ), - ), - if (widget.transaction == null || - widget.transaction!.transaction.type == - TransactionType.income) - Tab( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4.w, - children: [ - SvgPicture.asset( - Assets.images.arrowSwapDown, - colorFilter: ColorFilter.mode( - (tabController.index == 1) - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn, + if (showIncome) + KeyedSubtree( + key: const ValueKey('tx-form-income'), + child: formDisplay == 'full' + ? AddTransactionForm( + accentColor: appPrimaryColor, + transactionCompleteEntity: + widget.transaction, + ) + : AddTransactionFormCompactLayout( + accentColor: appPrimaryColor, + transactionCompleteEntity: + widget.transaction, + ), ), + ]; + return IndexedStack( + index: tabController.index.clamp( + 0, + children.length - 1, ), - Text(LocaleKeys.transactionIncome.tr()) - ], - ), + children: children, + ); + }, ), - ], - ), - Expanded( - child: TabBarView( - controller: tabController, - physics: const NeverScrollableScrollPhysics(), - children: [ - if (widget.transaction == null || - widget.transaction!.transaction.type == - TransactionType.expense) - (formDisplay == 'full' - ? AddTransactionForm( - transactionType: TransactionType.expense, - accentColor: Theme.of(context).primaryColor, - transactionCompleteEntity: widget.transaction, - ) - : AddTransactionFormCompactLayout( - transactionType: TransactionType.expense, - accentColor: Theme.of(context).primaryColor, - transactionCompleteEntity: widget.transaction, - )), - if (widget.transaction == null || - widget.transaction!.transaction.type == - TransactionType.income) - (formDisplay == 'full' - ? AddTransactionForm( - accentColor: Theme.of(context).primaryColor, - transactionCompleteEntity: widget.transaction, - ) - : AddTransactionFormCompactLayout( - accentColor: Theme.of(context).primaryColor, - transactionCompleteEntity: widget.transaction, - )), - ], + ), ), ), ], @@ -184,3 +150,108 @@ class _AddTransactionScreenState extends State ); } } + +class _TypeSegmented extends StatelessWidget { + final TabController controller; + + const _TypeSegmented({required this.controller}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return AnimatedBuilder( + animation: controller, + builder: (ctx, _) { + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + ), + padding: EdgeInsets.all(4.r), + child: Row( + children: [ + Expanded( + child: _SegmentItem( + label: LocaleKeys.transactionExpense.tr(), + icon: Icons.north_east, + active: controller.index == 0, + activeBg: tones.expense.background, + activeFg: tones.expense.deep, + onTap: () => controller.animateTo(0), + ), + ), + Expanded( + child: _SegmentItem( + label: LocaleKeys.transactionIncome.tr(), + icon: Icons.south_west, + active: controller.index == 1, + activeBg: tones.income.background, + activeFg: tones.income.deep, + onTap: () => controller.animateTo(1), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _SegmentItem extends StatelessWidget { + final String label; + final IconData icon; + final bool active; + final Color activeBg; + final Color activeFg; + final VoidCallback onTap; + + const _SegmentItem({ + required this.label, + required this.icon, + required this.active, + required this.activeBg, + required this.activeFg, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: AppMotion.base, + curve: AppMotion.standard, + padding: EdgeInsets.symmetric(vertical: 13.h, horizontal: 8.w), + decoration: BoxDecoration( + color: active ? activeBg : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadii.md), + ), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 16.sp, + color: active ? activeFg : tones.textMuted, + ), + SizedBox(width: 6.w), + Text( + label, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: active ? activeFg : tones.textSecondary, + letterSpacing: -0.1, + ), + ), + ], + ), + ), + ); + } +} From 648a980bc052bb76e354ee8146118b65c5d6b490 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 01:03:44 +0100 Subject: [PATCH 07/21] enh(transactions): Polish add-transaction form controls Record button gains a gradient surface (accent to slightly darker accent), a soft inner highlight, an accent-tinted shadow, and a tighter corner radius so the primary action feels lifted instead of flat. Currency chip and the "+" add affordances next to the wallet, party and category dropdowns share that family: calm tinted background at 10-12% accent alpha with a matching border and accent-coloured glyph, so they read as siblings of the form's segmented picker. --- .../add_transaction_form_compact_layout.dart | 182 +++++++++++------- .../utils/forms/add_transaction_form.dart | 61 +++++- 2 files changed, 167 insertions(+), 76 deletions(-) diff --git a/lib/presentation/transactions/add_transaction_form_compact_layout.dart b/lib/presentation/transactions/add_transaction_form_compact_layout.dart index d1ac5f6f..3bc4cf5c 100644 --- a/lib/presentation/transactions/add_transaction_form_compact_layout.dart +++ b/lib/presentation/transactions/add_transaction_form_compact_layout.dart @@ -51,7 +51,11 @@ class AddTransactionFormCompactLayout extends StatefulWidget { } class _AddTransactionFormCompactLayoutState - extends State { + extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + int? selectedIndex; DateFormat dateFormat = DateFormat('dd-MM-yyy'); DateFormat timeFormat = DateFormat('h:mm:a'); @@ -184,6 +188,7 @@ class _AddTransactionFormCompactLayoutState @override Widget build(BuildContext context) { + super.build(context); return BlocListener( listenWhen: (previous, current) { // Listen when saving completes (isSaving goes from true to false) @@ -264,11 +269,22 @@ class _AddTransactionFormCompactLayoutState maxHeight: 50.h, ), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), + color: widget.accentColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: widget.accentColor.withValues(alpha: 0.35), + width: 1, + ), ), child: Center( - child: Text(currentCurrency?.code ?? "XAF"), + child: Text( + currentCurrency?.code ?? "XAF", + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: widget.accentColor, + ), + ), ), ), ) @@ -330,29 +346,11 @@ class _AddTransactionFormCompactLayoutState }, ), ), - GestureDetector( + _AddBesideField( onTap: () async { AppNavigator.push(context, const AddWalletScreen()); }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: SvgPicture.asset( - Assets.images.add, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn, - ), - ), - ), - ), + accent: widget.accentColor, ), ], ), @@ -451,10 +449,7 @@ class _AddTransactionFormCompactLayoutState child: BlocBuilder( builder: (context, state) { return CustomAutoCompleteSearch( - label: widget.transactionType == - TransactionType.expense - ? '${LocaleKeys.transactionSentTo.tr()} (${LocaleKeys.party.tr()})' - : '${LocaleKeys.transactionReceivedFrom.tr()} (${LocaleKeys.party.tr()})', + label: LocaleKeys.party.tr(), accentColor: widget.accentColor, optionsBuilder: (TextEditingValue textEditingValue) { @@ -480,29 +475,11 @@ class _AddTransactionFormCompactLayoutState }, ), ), - GestureDetector( + _AddBesideField( onTap: () async { AppNavigator.push(context, const AddPartyScreen()); }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: SvgPicture.asset( - Assets.images.add, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn, - ), - ), - ), - ), + accent: widget.accentColor, ), ], ), @@ -522,7 +499,7 @@ class _AddTransactionFormCompactLayoutState element.type == widget.transactionType); return CustomAutoCompleteSearch( - label: LocaleKeys.transactionCategory.tr(), + label: LocaleKeys.category.tr(), accentColor: widget.accentColor, optionsBuilder: (TextEditingValue textEditingValue) { @@ -548,7 +525,7 @@ class _AddTransactionFormCompactLayoutState }, ), ), - GestureDetector( + _AddBesideField( onTap: () async { AppNavigator.push( context, @@ -558,25 +535,7 @@ class _AddTransactionFormCompactLayoutState ), ); }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: SvgPicture.asset( - Assets.images.add, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn, - ), - ), - ), - ), + accent: widget.accentColor, ), ], ), @@ -616,14 +575,61 @@ class _AddTransactionFormCompactLayoutState ], SizedBox(height: 20.h), SizedBox( - height: 54.h, + height: 56.h, width: double.infinity, child: Builder( builder: (context) { - return ElevatedButton( + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(11.r), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + widget.accentColor, + Color.alphaBlend( + Colors.black.withValues(alpha: 0.08), + widget.accentColor, + ), + ], + ), + border: Border.all( + color: Colors.white.withValues(alpha: 0.35), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: widget.accentColor.withValues(alpha: 0.4), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: ElevatedButton( style: ButtonStyle( backgroundColor: - WidgetStatePropertyAll(widget.accentColor), + const WidgetStatePropertyAll(Colors.transparent), + foregroundColor: + const WidgetStatePropertyAll(Colors.white), + elevation: + const WidgetStatePropertyAll(0), + shadowColor: const WidgetStatePropertyAll( + Colors.transparent), + overlayColor: WidgetStatePropertyAll( + Colors.white.withValues(alpha: 0.16)), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(14.r), + ), + ), + textStyle: WidgetStatePropertyAll( + TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w800, + letterSpacing: -0.1, + ), + ), ), onPressed: () { if (_formKey.currentState!.validate()) { @@ -689,6 +695,7 @@ class _AddTransactionFormCompactLayoutState ) ], ), + ), ); }, ), @@ -701,3 +708,34 @@ class _AddTransactionFormCompactLayoutState ); } } + +class _AddBesideField extends StatelessWidget { + final VoidCallback onTap; + final Color accent; + + const _AddBesideField({required this.onTap, required this.accent}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 52.w, + height: 50.h, + decoration: BoxDecoration( + color: accent.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: accent.withValues(alpha: 0.35), + width: 1, + ), + ), + child: Icon( + Icons.add, + color: accent, + size: 22.sp, + ), + ), + ); + } +} diff --git a/lib/presentation/utils/forms/add_transaction_form.dart b/lib/presentation/utils/forms/add_transaction_form.dart index 37c9a6bb..e469dfd6 100644 --- a/lib/presentation/utils/forms/add_transaction_form.dart +++ b/lib/presentation/utils/forms/add_transaction_form.dart @@ -48,7 +48,11 @@ class AddTransactionForm extends StatefulWidget { State createState() => _AddTransactionFormState(); } -class _AddTransactionFormState extends State { +class _AddTransactionFormState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + int? selectedIndex; DateFormat dateFormat = DateFormat('dd-MM-yyy'); DateFormat timeFormat = DateFormat('h:mm:a'); @@ -171,6 +175,7 @@ class _AddTransactionFormState extends State { @override Widget build(BuildContext context) { + super.build(context); return BlocListener( listenWhen: (previous, current) { // Listen when saving completes (isSaving goes from true to false) @@ -694,14 +699,61 @@ class _AddTransactionFormState extends State { ], SizedBox(height: 20.h), SizedBox( - height: 54.h, + height: 56.h, width: double.infinity, child: Builder( builder: (context) { - return ElevatedButton( + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(11.r), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + widget.accentColor, + Color.alphaBlend( + Colors.black.withValues(alpha: 0.08), + widget.accentColor, + ), + ], + ), + border: Border.all( + color: Colors.white.withValues(alpha: 0.35), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: widget.accentColor.withValues(alpha: 0.4), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: ElevatedButton( style: ButtonStyle( backgroundColor: - WidgetStatePropertyAll(widget.accentColor), + const WidgetStatePropertyAll(Colors.transparent), + foregroundColor: + const WidgetStatePropertyAll(Colors.white), + elevation: + const WidgetStatePropertyAll(0), + shadowColor: const WidgetStatePropertyAll( + Colors.transparent), + overlayColor: WidgetStatePropertyAll( + Colors.white.withValues(alpha: 0.16)), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(14.r), + ), + ), + textStyle: WidgetStatePropertyAll( + TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w800, + letterSpacing: -0.1, + ), + ), ), onPressed: () { if (_formKey.currentState!.validate()) { @@ -769,6 +821,7 @@ class _AddTransactionFormState extends State { ) ], ), + ), ); }, ), From 488dc5ad8e21aae025056bc5b151bc4d9cf2472b Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 01:03:55 +0100 Subject: [PATCH 08/21] enh(transactions): Tonal filter chips on history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter triggers on the transaction history page (Date, Category, Wallet) are now per-filter tonal chips — Date in brand green, Category in warm orange, Wallet in income green. Each chip fills with its tone, hairline accent border, deep colour for icon, label and caret. The title also drops Title Case for sentence case to match the rest of the copy. --- lib/presentation/history_screen.dart | 203 ++++++++++++++------------- 1 file changed, 108 insertions(+), 95 deletions(-) diff --git a/lib/presentation/history_screen.dart b/lib/presentation/history_screen.dart index 7cbcd428..41f0123d 100644 --- a/lib/presentation/history_screen.dart +++ b/lib/presentation/history_screen.dart @@ -17,9 +17,9 @@ import 'package:trakli/presentation/info_interfaces/data.dart'; import 'package:trakli/presentation/info_interfaces/info_interface.dart'; import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; import 'package:trakli/presentation/utils/colors.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/helpers.dart'; import 'package:trakli/presentation/utils/popovers/category_list_popover.dart'; @@ -107,37 +107,17 @@ class _HistoryScreenState extends State { final totalBalance = totalIncome - totalExpense; return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.transactionHistory.tr(), - headerTextColor: const Color(0xFFEBEDEC), + appBar: PageAppBar( + title: LocaleKeys.transactionHistory.tr(), actions: [ - InkWell( - onTap: () { - AppNavigator.push( - context, - const AddTransactionScreen(), - ); - }, - child: Container( - width: 42.r, - height: 42.r, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: Theme.of(context).scaffoldBackgroundColor, - ), - padding: EdgeInsets.all(8.r), - child: Center( - child: Icon( - Icons.add, - size: 24.r, - color: Theme.of(context).primaryColor, - ), - ), + PageAppBarAction( + icon: Icons.add, + onTap: () => AppNavigator.push( + context, + const AddTransactionScreen(), ), + primary: true, ), - SizedBox(width: 16.w), ], ), body: (state.isLoading) @@ -157,10 +137,7 @@ class _HistoryScreenState extends State { data: emptyTransactionData, ) : SingleChildScrollView( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, - ), + padding: EdgeInsets.fromLTRB(12.w, 6.h, 12.w, 12.h), child: Column( children: [ Row( @@ -207,14 +184,17 @@ class _HistoryScreenState extends State { _filterType( iconPath: Assets.images.calendar, filterType: FilterType.date, + tone: AppTone.brand, ), _filterType( iconPath: Assets.images.tag2, filterType: FilterType.category, + tone: AppTone.warm, ), _filterType( iconPath: Assets.images.wallet, filterType: FilterType.wallet, + tone: AppTone.income, ), ], ), @@ -456,76 +436,109 @@ class _HistoryScreenState extends State { Widget _filterType({ required String iconPath, required FilterType filterType, + required AppTone tone, }) { final GlobalKey key = GlobalKey(); + final tones = context.tones; + final palette = tones.tone(tone); return Expanded( child: Builder( builder: (context) { - return OutlinedButton.icon( + return Material( key: key, - onPressed: () { - showCustomPopOver( - context, - maxWidth: 0.45.sw, - widget: filterType == FilterType.wallet - ? WalletListPopover( - label: filterType.filterName.tr(), - onSelect: (wallet) { - setState(() { - if (!selectedItems.any((item) => - (item is WalletEntity && - item.clientId == wallet.clientId))) { - selectedItems.add(wallet); - } - }); - }, - ) - : filterType == FilterType.category - ? CategoryListPopover( - label: filterType.filterName.tr(), - onSelect: (category) { - setState(() { - if (!selectedItems.any((item) => - (item is CategoryEntity && - item.clientId == category.clientId))) { - selectedItems.add(category); - } - }); - }, - ) - : DateListPopover( - label: filterType.filterName.tr(), - onSelect: (range) { - setState(() { - dateRange = range; - }); - }, - onSelectString: (dateFilterOption) { - setState(() { - selectedItems.removeWhere( - (item) => item is DateFilterOption); - selectedItems.add(dateFilterOption); - }); - }, - ), - ); - }, - icon: SvgPicture.asset( - iconPath, - width: 12.w, - height: 12.h, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn, + color: palette.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadii.lg), + side: BorderSide( + color: palette.accent.withValues(alpha: 0.45), ), ), - label: Text( - filterType.filterName.tr(), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - fontSize: 10.sp, - ), - overflow: TextOverflow.ellipsis, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadii.lg), + onTap: () { + showCustomPopOver( + context, + maxWidth: 0.45.sw, + widget: filterType == FilterType.wallet + ? WalletListPopover( + label: filterType.filterName.tr(), + onSelect: (wallet) { + setState(() { + if (!selectedItems.any((item) => + (item is WalletEntity && + item.clientId == wallet.clientId))) { + selectedItems.add(wallet); + } + }); + }, + ) + : filterType == FilterType.category + ? CategoryListPopover( + label: filterType.filterName.tr(), + onSelect: (category) { + setState(() { + if (!selectedItems.any((item) => + (item is CategoryEntity && + item.clientId == category.clientId))) { + selectedItems.add(category); + } + }); + }, + ) + : DateListPopover( + label: filterType.filterName.tr(), + onSelect: (range) { + setState(() { + dateRange = range; + }); + }, + onSelectString: (dateFilterOption) { + setState(() { + selectedItems.removeWhere( + (item) => item is DateFilterOption); + selectedItems.add(dateFilterOption); + }); + }, + ), + ); + }, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 7.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + iconPath, + width: 14.w, + height: 14.h, + colorFilter: ColorFilter.mode( + palette.deep, + BlendMode.srcIn, + ), + ), + SizedBox(width: 6.w), + Flexible( + child: Text( + filterType.filterName.tr(), + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w700, + color: palette.deep, + letterSpacing: -0.1, + ), + overflow: TextOverflow.ellipsis, + ), + ), + SizedBox(width: 2.w), + Icon( + Icons.expand_more, + size: 14.sp, + color: palette.deep, + ), + ], + ), + ), ), ); }, From 9e691c5ee2199ab20f6304133d861c77dd6acd86 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 01:05:22 +0100 Subject: [PATCH 09/21] enh(settings): Adopt shared header across settings, transfers and savings Settings root, account info, account & privacy, advanced, defaults, display, notification settings, sync history, orphaned-media log, data deletion, transfers, savings (list + add), and notifications all drop the saturated green primary-colour CustomAppBar in favour of the shared page header so navigation chrome, typography and back behaviour match the rest of the app. The settings root wraps its tiles in a single tonal section card so the page reads as one grouped surface instead of free-floating list items. --- lib/presentation/account_info_screen.dart | 10 ++---- lib/presentation/account_settings_screen.dart | 13 ++----- .../advanced_settings_screen.dart | 13 ++----- lib/presentation/data_deletion_screen.dart | 13 ++----- .../defaults_settings_screen.dart | 13 ++----- lib/presentation/display_settings_screen.dart | 13 ++----- lib/presentation/notification_screen.dart | 10 ++---- .../notification_settings_screen.dart | 10 ++---- .../orphaned_media_cleanup_log_screen.dart | 10 ++---- .../savings/add_savings_screen.dart | 10 ++---- .../savings/my_savings_screen.dart | 36 +++++-------------- lib/presentation/settings_screen.dart | 26 +++++++------- lib/presentation/sync_history_screen.dart | 13 ++----- .../transfers/transfers_screen.dart | 10 ++---- 14 files changed, 58 insertions(+), 142 deletions(-) diff --git a/lib/presentation/account_info_screen.dart b/lib/presentation/account_info_screen.dart index d7a2c055..bf9c4bca 100644 --- a/lib/presentation/account_info_screen.dart +++ b/lib/presentation/account_info_screen.dart @@ -4,11 +4,10 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:trakli/gen/assets.gen.dart' show Assets; import 'package:trakli/presentation/auth/cubits/auth/auth_cubit.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; import 'package:trakli/presentation/utils/colors.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/forms/profile_form.dart'; import 'package:trakli/presentation/utils/info_tile.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -25,11 +24,8 @@ class _AccountInfoScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: appPrimaryColor, - titleText: LocaleKeys.accountInfo.tr(), - headerTextColor: Colors.white, - leading: const CustomBackButton(), + appBar: PageAppBar( + title: LocaleKeys.accountInfo.tr(), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric( diff --git a/lib/presentation/account_settings_screen.dart b/lib/presentation/account_settings_screen.dart index 0065af97..e7b192c6 100644 --- a/lib/presentation/account_settings_screen.dart +++ b/lib/presentation/account_settings_screen.dart @@ -4,8 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/data_deletion_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class AccountSettingsScreen extends StatelessWidget { const AccountSettingsScreen({super.key}); @@ -13,14 +12,8 @@ class AccountSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.accountAndPrivacy.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [ - SizedBox(width: 16.w), - ], + appBar: PageAppBar( + title: LocaleKeys.accountAndPrivacy.tr(), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric( diff --git a/lib/presentation/advanced_settings_screen.dart b/lib/presentation/advanced_settings_screen.dart index ac283b57..4ca6613d 100644 --- a/lib/presentation/advanced_settings_screen.dart +++ b/lib/presentation/advanced_settings_screen.dart @@ -6,8 +6,7 @@ import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/orphaned_media_cleanup_log_screen.dart'; import 'package:trakli/presentation/sync_history_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class AdvancedSettingsScreen extends StatelessWidget { const AdvancedSettingsScreen({super.key}); @@ -15,14 +14,8 @@ class AdvancedSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.advanced.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [ - SizedBox(width: 16.w), - ], + appBar: PageAppBar( + title: LocaleKeys.advanced.tr(), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric( diff --git a/lib/presentation/data_deletion_screen.dart b/lib/presentation/data_deletion_screen.dart index 06e4ff53..369a4b54 100644 --- a/lib/presentation/data_deletion_screen.dart +++ b/lib/presentation/data_deletion_screen.dart @@ -5,13 +5,12 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/auth/cubits/auth/auth_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; import 'package:trakli/presentation/utils/bottom_sheets/account_deletion_sheet.dart'; import 'package:trakli/presentation/utils/buttons.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/dialogs/pop_up_dialog.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/helpers.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class DataDeletionScreen extends StatefulWidget { const DataDeletionScreen({super.key}); @@ -24,14 +23,8 @@ class _DataDeletionScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.data.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [ - SizedBox(width: 16.w), - ], + appBar: PageAppBar( + title: LocaleKeys.data.tr(), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric( diff --git a/lib/presentation/defaults_settings_screen.dart b/lib/presentation/defaults_settings_screen.dart index 86b4a582..860c5df9 100644 --- a/lib/presentation/defaults_settings_screen.dart +++ b/lib/presentation/defaults_settings_screen.dart @@ -11,12 +11,11 @@ import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/config/cubit/config_cubit.dart'; import 'package:trakli/presentation/currency/cubit/currency_cubit.dart'; import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; import 'package:trakli/presentation/utils/bottom_sheets/pick_group_bottom_sheet.dart'; import 'package:trakli/presentation/utils/bottom_sheets/select_wallet_bottom_sheet.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/helpers.dart'; import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class DefaultsSettingsScreen extends StatelessWidget { const DefaultsSettingsScreen({super.key}); @@ -40,14 +39,8 @@ class DefaultsSettingsScreen extends StatelessWidget { .firstWhereOrNull((entity) => entity.clientId == defaultWalletId); return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.defaults.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [ - SizedBox(width: 16.w), - ], + appBar: PageAppBar( + title: LocaleKeys.defaults.tr(), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric( diff --git a/lib/presentation/display_settings_screen.dart b/lib/presentation/display_settings_screen.dart index 7f1338f5..959f1645 100644 --- a/lib/presentation/display_settings_screen.dart +++ b/lib/presentation/display_settings_screen.dart @@ -7,12 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/config/theme_cubit/theme_cubit.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; import 'package:trakli/presentation/utils/colors.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/globals.dart'; import 'package:trakli/presentation/utils/helpers.dart'; import 'package:trakli/providers/local_storage.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class DisplaySettingsScreen extends StatefulWidget { const DisplaySettingsScreen({super.key}); @@ -41,14 +40,8 @@ class _DisplaySettingsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.displaySettings.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [ - SizedBox(width: 16.w), - ], + appBar: PageAppBar( + title: LocaleKeys.displaySettings.tr(), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric( diff --git a/lib/presentation/notification_screen.dart b/lib/presentation/notification_screen.dart index 472d60d7..a5d17dec 100644 --- a/lib/presentation/notification_screen.dart +++ b/lib/presentation/notification_screen.dart @@ -1,8 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class NotificationScreen extends StatelessWidget { const NotificationScreen({super.key}); @@ -10,11 +9,8 @@ class NotificationScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - leading: const CustomBackButton(), - backgroundColor: Theme.of(context).primaryColor, - headerTextColor: Colors.white, - titleText: LocaleKeys.notifications.tr(), + appBar: PageAppBar( + title: LocaleKeys.notifications.tr(), ), ); } diff --git a/lib/presentation/notification_settings/notification_settings_screen.dart b/lib/presentation/notification_settings/notification_settings_screen.dart index 7a627ed9..c253db77 100644 --- a/lib/presentation/notification_settings/notification_settings_screen.dart +++ b/lib/presentation/notification_settings/notification_settings_screen.dart @@ -12,10 +12,9 @@ import 'package:trakli/presentation/notification_settings/widgets/insights_frequ import 'package:trakli/presentation/notification_settings/widgets/notification_section_header.dart'; import 'package:trakli/presentation/notification_settings/widgets/notification_selection_item.dart'; import 'package:trakli/presentation/notification_settings/widgets/notification_toggle_item.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/helpers.dart' show showSnackBar, showLoader, hideLoader; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class NotificationSettingsScreen extends StatelessWidget { const NotificationSettingsScreen({super.key}); @@ -23,11 +22,8 @@ class NotificationSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.notificationSettings.tr(), - headerTextColor: const Color(0xFFEBEDEC), + appBar: PageAppBar( + title: LocaleKeys.notificationSettings.tr(), ), body: const _NotificationSettingsBody(), ); diff --git a/lib/presentation/orphaned_media_cleanup_log_screen.dart b/lib/presentation/orphaned_media_cleanup_log_screen.dart index 2b501da5..b550cc0e 100644 --- a/lib/presentation/orphaned_media_cleanup_log_screen.dart +++ b/lib/presentation/orphaned_media_cleanup_log_screen.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/core/services/orphaned_media_cleanup_service.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; /// Debug-only screen that shows the last orphaned media cleanup log content. class OrphanedMediaCleanupLogScreen extends StatelessWidget { @@ -13,11 +12,8 @@ class OrphanedMediaCleanupLogScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.orphanedMediaCleanupLog.tr(), - headerTextColor: const Color(0xFFEBEDEC), + appBar: PageAppBar( + title: LocaleKeys.orphanedMediaCleanupLog.tr(), ), body: FutureBuilder( future: getOrphanedMediaCleanupLogContent(), diff --git a/lib/presentation/savings/add_savings_screen.dart b/lib/presentation/savings/add_savings_screen.dart index a85cdc74..a8f5525a 100644 --- a/lib/presentation/savings/add_savings_screen.dart +++ b/lib/presentation/savings/add_savings_screen.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/forms/add_savings_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class AddSavingsScreen extends StatelessWidget { const AddSavingsScreen({super.key}); @@ -11,11 +10,8 @@ class AddSavingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - headerTextColor: const Color(0xFFEBEDEC), - titleText: LocaleKeys.addSaving.tr(), + appBar: PageAppBar( + title: LocaleKeys.addSaving.tr(), ), body: const AddSavingsForm(), ); diff --git a/lib/presentation/savings/my_savings_screen.dart b/lib/presentation/savings/my_savings_screen.dart index dfe954b7..c3834cd1 100644 --- a/lib/presentation/savings/my_savings_screen.dart +++ b/lib/presentation/savings/my_savings_screen.dart @@ -6,8 +6,7 @@ import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/savings/add_savings_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/savings_tile.dart'; class MySavingsScreen extends StatelessWidget { @@ -16,34 +15,15 @@ class MySavingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.savings.tr(), - headerTextColor: const Color(0xFFEBEDEC), + appBar: PageAppBar( + title: LocaleKeys.savings.tr(), actions: [ - InkWell( - onTap: () { - AppNavigator.push(context, const AddSavingsScreen()); - }, - child: Container( - width: 40.w, - height: 40.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - color: const Color(0xFFEBEDEC), - ), - padding: EdgeInsets.all(8.r), - child: Center( - child: Icon( - Icons.add, - size: 24.r, - color: Theme.of(context).primaryColor, - ), - ), - ), + PageAppBarAction( + icon: Icons.add, + onTap: () => + AppNavigator.push(context, const AddSavingsScreen()), + primary: true, ), - SizedBox(width: 16.w), ], ), body: SingleChildScrollView( diff --git a/lib/presentation/settings_screen.dart b/lib/presentation/settings_screen.dart index a93eba31..f3b659b1 100644 --- a/lib/presentation/settings_screen.dart +++ b/lib/presentation/settings_screen.dart @@ -17,9 +17,9 @@ import 'package:trakli/presentation/defaults_settings_screen.dart'; import 'package:trakli/presentation/display_settings_screen.dart'; import 'package:trakli/presentation/notification_settings/notification_settings_screen.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/bottom_sheets/about_app_bottom_sheet.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; import 'package:trakli/presentation/utils/globals.dart'; import 'package:trakli/presentation/utils/helpers.dart'; @@ -34,21 +34,23 @@ class _SettingsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.settings.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [ - SizedBox(width: 16.w), - ], + appBar: PageAppBar( + title: LocaleKeys.settings.tr(), ), body: SingleChildScrollView( padding: EdgeInsets.symmetric( horizontal: 16.w, vertical: 16.h, ), - child: Column( + child: Container( + decoration: BoxDecoration( + color: context.tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: context.tones.borderLight), + boxShadow: context.elevations.level1, + ), + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), + child: Column( children: [ // Language ListTile( @@ -252,9 +254,9 @@ class _SettingsScreenState extends State { size: 16.sp, ), ), - SizedBox(height: 24.h), ], ), + ), ), ); } diff --git a/lib/presentation/sync_history_screen.dart b/lib/presentation/sync_history_screen.dart index 4ae87093..ab1a667b 100644 --- a/lib/presentation/sync_history_screen.dart +++ b/lib/presentation/sync_history_screen.dart @@ -7,8 +7,7 @@ import 'package:trakli/core/sync/sync_database.dart'; import 'package:trakli/data/database/app_database.dart'; import 'package:trakli/di/injection.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; class SyncHistoryScreen extends StatefulWidget { const SyncHistoryScreen({super.key}); @@ -126,14 +125,8 @@ class _SyncHistoryScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.synchronization.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [ - SizedBox(width: 16.w), - ], + appBar: PageAppBar( + title: LocaleKeys.synchronization.tr(), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) diff --git a/lib/presentation/transfers/transfers_screen.dart b/lib/presentation/transfers/transfers_screen.dart index e65ebcd2..3cf0e097 100644 --- a/lib/presentation/transfers/transfers_screen.dart +++ b/lib/presentation/transfers/transfers_screen.dart @@ -7,8 +7,7 @@ import 'package:trakli/domain/entities/transfer_entity.dart'; import 'package:trakli/domain/entities/wallet_entity.dart'; import 'package:trakli/gen/translations/locale_keys.g.dart'; import 'package:trakli/presentation/transfers/cubit/transfer_cubit.dart'; -import 'package:trakli/presentation/utils/back_button.dart'; -import 'package:trakli/presentation/utils/custom_appbar.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/transfer_tile.dart'; import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; @@ -33,11 +32,8 @@ class TransfersScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.transfers.tr(), - headerTextColor: const Color(0xFFEBEDEC), + appBar: PageAppBar( + title: LocaleKeys.transfers.tr(), ), body: SafeArea( child: BlocBuilder( From 1920c8a9a70dbac2a59a128e9374d54c30c671b2 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 21 May 2026 01:23:29 +0100 Subject: [PATCH 10/21] test: Regression coverage for design tokens, recap, reports and page bar Pins the recap fallback (must surface the most recent active month, not null when this calendar month is empty) and the savings-rate safety when income is zero. Locks the canonical Trakli green and orange to their hex values so palette refactors can't silently drift the brand. Covers PageAppBar's search-in-title transform and the brand-mark anchor on root pages. Existing party-card test updated to assert against the tonal palette instead of the now-removed hardcoded avatar hex values. --- test/unit/app_tones_test.dart | 55 ++++++++ test/unit/month_in_review_test.dart | 187 ++++++++++++++++++++++++++++ test/unit/report_data_test.dart | 180 ++++++++++++++++++++++++++ test/widget/page_app_bar_test.dart | 137 ++++++++++++++++++++ test/widget/party_card_test.dart | 15 ++- 5 files changed, 570 insertions(+), 4 deletions(-) create mode 100644 test/unit/app_tones_test.dart create mode 100644 test/unit/month_in_review_test.dart create mode 100644 test/unit/report_data_test.dart create mode 100644 test/widget/page_app_bar_test.dart diff --git a/test/unit/app_tones_test.dart b/test/unit/app_tones_test.dart new file mode 100644 index 00000000..e3cf66dd --- /dev/null +++ b/test/unit/app_tones_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +void main() { + group('AppTones.tone()', () { + test('every AppTone enum value returns a non-null palette in light mode', + () { + for (final value in AppTone.values) { + final palette = AppTones.light.tone(value); + expect(palette.background, isNotNull, + reason: '$value has no background in light mode'); + expect(palette.accent, isNotNull, reason: '$value has no accent'); + expect(palette.deep, isNotNull, reason: '$value has no deep'); + expect(palette.ink, isNotNull, reason: '$value has no ink'); + } + }); + + test('every AppTone enum value returns a non-null palette in dark mode', + () { + for (final value in AppTone.values) { + final palette = AppTones.dark.tone(value); + expect(palette.background, isNotNull, + reason: '$value has no background in dark mode'); + } + }); + + test('warm tone surfaces the canonical Trakli orange accent', () { + final warm = AppTones.light.tone(AppTone.warm); + expect(warm.accent, AppTones.light.accentWarm); + expect(warm.background, AppTones.light.accentWarmSoft); + }); + + test('brand.deep is the canonical Trakli primary green', () { + // If this ever shifts off the brand colour, every primary button and + // active state in the app changes visually — keep it locked. + expect(AppTones.light.brand.deep, const Color(0xFF047844)); + }); + + test('accentWarm is the canonical Trakli secondary orange', () { + expect(AppTones.light.accentWarm, const Color(0xFFFF9500)); + }); + }); + + group('AppRadii', () { + test('radius scale grows monotonically', () { + expect(AppRadii.xs, lessThan(AppRadii.sm)); + expect(AppRadii.sm, lessThan(AppRadii.md)); + expect(AppRadii.md, lessThan(AppRadii.lg)); + expect(AppRadii.lg, lessThan(AppRadii.xl)); + expect(AppRadii.xl, lessThan(AppRadii.xxl)); + expect(AppRadii.xxl, lessThan(AppRadii.pill)); + }); + }); +} diff --git a/test/unit/month_in_review_test.dart b/test/unit/month_in_review_test.dart new file mode 100644 index 00000000..279c21f5 --- /dev/null +++ b/test/unit/month_in_review_test.dart @@ -0,0 +1,187 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:trakli/domain/entities/category_entity.dart'; +import 'package:trakli/domain/entities/party_entity.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/domain/entities/transaction_entity.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; +import 'package:trakli/presentation/statistics/month_in_review/month_in_review_data.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +WalletEntity _wallet() => WalletEntity( + clientId: 'w1', + type: WalletType.cash, + name: 'Wallet', + balance: 0, + currencyCode: 'USD', + createdAt: DateTime(2026, 1, 1), + updatedAt: DateTime(2026, 1, 1), + ); + +PartyEntity _party(String name) => PartyEntity( + clientId: 'p-$name', + name: name, + createdAt: DateTime(2026, 1, 1), + updatedAt: DateTime(2026, 1, 1), + ); + +CategoryEntity _category(String name, {TransactionType type = TransactionType.expense}) => + CategoryEntity( + clientId: 'c-$name', + type: type, + name: name, + createdAt: DateTime(2026, 1, 1), + ); + +TransactionCompleteEntity _txn({ + required double amount, + required TransactionType type, + required DateTime when, + PartyEntity? party, + List categories = const [], +}) { + return TransactionCompleteEntity( + transaction: TransactionEntity( + clientId: 'tx-${when.microsecondsSinceEpoch}-${amount.toInt()}', + amount: amount, + description: '', + createdAt: when, + updatedAt: when, + datetime: when, + type: type, + walletClientId: 'w1', + partyClientId: party?.clientId, + ), + categories: categories, + wallet: _wallet(), + party: party, + ); +} + +void main() { + group('buildMonthInReview', () { + test('returns null only when there are no transactions at all', () { + expect(buildMonthInReview(const []), isNull); + }); + + test( + 'falls back to the most recent active month when the current month is empty', + () { + final now = DateTime.now(); + final twoMonthsAgo = DateTime(now.year, now.month - 2, 15); + final txns = [ + _txn(amount: 500, type: TransactionType.income, when: twoMonthsAgo), + _txn(amount: 200, type: TransactionType.expense, when: twoMonthsAgo), + ]; + + final recap = buildMonthInReview(txns); + + expect(recap, isNotNull, + reason: + 'Recap must not be null when there is activity in an earlier month'); + expect(recap!.income, 500); + expect(recap.expense, 200); + expect(recap.net, 300); + }, + ); + + test('honours offsetMonths and ignores fallback when explicitly anchored', + () { + final now = DateTime.now(); + final thisMonth = DateTime(now.year, now.month, 5); + final lastMonth = DateTime(now.year, now.month - 1, 5); + + final txns = [ + _txn(amount: 100, type: TransactionType.income, when: thisMonth), + _txn(amount: 999, type: TransactionType.income, when: lastMonth), + ]; + + final recap = buildMonthInReview(txns, offsetMonths: 1); + expect(recap, isNotNull); + expect(recap!.income, 999); + }); + + test('topCategory aggregates across multiple expenses in the same group', + () { + final now = DateTime.now(); + final groceries = _category('Groceries'); + final txns = [ + _txn( + amount: 30, + type: TransactionType.expense, + when: now, + categories: [groceries], + ), + _txn( + amount: 50, + type: TransactionType.expense, + when: now, + categories: [groceries], + ), + _txn( + amount: 10, + type: TransactionType.expense, + when: now, + categories: [_category('Coffee')], + ), + ]; + + final recap = buildMonthInReview(txns); + expect(recap, isNotNull); + expect(recap!.topCategory?.name, 'Groceries'); + expect(recap.topCategory?.amount, 80, + reason: 'Top category amount must sum same-category expenses'); + }); + + test('biggestExpense is the single largest, not the running total', () { + final now = DateTime.now(); + final landlord = _party('Landlord'); + final coffee = _party('Coffee'); + final txns = [ + _txn( + amount: 1500, + type: TransactionType.expense, + when: now, + party: landlord, + ), + _txn(amount: 4, type: TransactionType.expense, when: now, party: coffee), + _txn(amount: 4, type: TransactionType.expense, when: now, party: coffee), + _txn(amount: 4, type: TransactionType.expense, when: now, party: coffee), + ]; + + final recap = buildMonthInReview(txns); + expect(recap, isNotNull); + expect(recap!.biggestExpense?.amount, 1500); + expect(recap.biggestExpense?.party, 'Landlord'); + }); + + test('savingsRate is 0 when there is no income (no NaN, no divide-by-zero)', + () { + final now = DateTime.now(); + final txns = [ + _txn(amount: 50, type: TransactionType.expense, when: now), + ]; + + final recap = buildMonthInReview(txns); + expect(recap, isNotNull); + expect(recap!.income, 0); + expect(recap.expense, 50); + expect(recap.savingsRate, 0); + }); + + test('topPayee identifies the largest spend recipient', () { + final now = DateTime.now(); + final amazon = _party('Amazon'); + final coffee = _party('Cafe'); + final txns = [ + _txn(amount: 400, type: TransactionType.expense, when: now, party: amazon), + _txn(amount: 6, type: TransactionType.expense, when: now, party: coffee), + _txn(amount: 6, type: TransactionType.expense, when: now, party: coffee), + ]; + + final recap = buildMonthInReview(txns); + expect(recap, isNotNull); + expect(recap!.topPayee?.name, 'Amazon'); + expect(recap.topPayee?.amount, 400); + }); + }); +} diff --git a/test/unit/report_data_test.dart b/test/unit/report_data_test.dart new file mode 100644 index 00000000..52d518aa --- /dev/null +++ b/test/unit/report_data_test.dart @@ -0,0 +1,180 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:trakli/domain/entities/category_entity.dart'; +import 'package:trakli/domain/entities/transaction_complete_entity.dart'; +import 'package:trakli/domain/entities/transaction_entity.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; +import 'package:trakli/presentation/statistics/reports/report_data.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +WalletEntity _wallet() => WalletEntity( + clientId: 'w1', + type: WalletType.cash, + name: 'Wallet', + balance: 0, + currencyCode: 'USD', + createdAt: DateTime(2026, 1, 1), + updatedAt: DateTime(2026, 1, 1), + ); + +CategoryEntity _cat(String name, + {TransactionType type = TransactionType.expense}) => + CategoryEntity( + clientId: 'c-$name', + type: type, + name: name, + createdAt: DateTime(2026, 1, 1), + ); + +TransactionCompleteEntity _tx({ + required double amount, + required TransactionType type, + required DateTime when, + List categories = const [], +}) { + return TransactionCompleteEntity( + transaction: TransactionEntity( + clientId: 'tx-${when.microsecondsSinceEpoch}-${amount.toInt()}', + amount: amount, + description: '', + createdAt: when, + updatedAt: when, + datetime: when, + type: type, + walletClientId: 'w1', + ), + categories: categories, + wallet: _wallet(), + ); +} + +void main() { + group('buildReportData', () { + test('daily buckets cover the requested period exactly', () { + final data = buildReportData(const [], periodDays: 30); + expect(data.daily.length, 30); + // First bucket is `periodDays - 1` days before today, last is today. + final expectedFirst = DateTime.now().subtract(const Duration(days: 29)); + final firstDay = data.daily.first.date; + expect(firstDay.year, expectedFirst.year); + expect(firstDay.month, expectedFirst.month); + expect(firstDay.day, expectedFirst.day); + }); + + test('totals equal the sum of daily buckets', () { + final today = DateTime.now(); + final txns = [ + _tx(amount: 100, type: TransactionType.income, when: today), + _tx( + amount: 40, + type: TransactionType.expense, + when: today.subtract(const Duration(days: 3)), + ), + _tx( + amount: 25, + type: TransactionType.expense, + when: today.subtract(const Duration(days: 7)), + ), + ]; + + final data = buildReportData(txns, periodDays: 30); + final dailyIncome = + data.daily.fold(0, (s, b) => s + b.income); + final dailyExpense = + data.daily.fold(0, (s, b) => s + b.expense); + + expect(dailyIncome, data.totals.income); + expect(dailyExpense, data.totals.expense); + expect(data.totals.net, data.totals.income - data.totals.expense); + }); + + test('savingsRate handles zero income without producing NaN', () { + final today = DateTime.now(); + final data = buildReportData( + [ + _tx(amount: 25, type: TransactionType.expense, when: today), + ], + periodDays: 30, + ); + expect(data.totals.income, 0); + expect(data.totals.savingsRate, 0); + expect(data.totals.expenseRatio, 0); + expect(data.totals.savingsRate.isFinite, isTrue); + expect(data.totals.expenseRatio.isFinite, isTrue); + }); + + test('savingsRate is income-relative when income is non-zero', () { + final today = DateTime.now(); + final data = buildReportData( + [ + _tx(amount: 1000, type: TransactionType.income, when: today), + _tx(amount: 400, type: TransactionType.expense, when: today), + ], + periodDays: 30, + ); + expect(data.totals.income, 1000); + expect(data.totals.expense, 400); + expect(data.totals.savingsRate, closeTo(0.6, 1e-9)); + expect(data.totals.expenseRatio, closeTo(0.4, 1e-9)); + }); + + test('expense categories are sorted by amount descending', () { + final today = DateTime.now(); + final txns = [ + _tx( + amount: 30, + type: TransactionType.expense, + when: today, + categories: [_cat('Coffee')], + ), + _tx( + amount: 200, + type: TransactionType.expense, + when: today, + categories: [_cat('Rent')], + ), + _tx( + amount: 90, + type: TransactionType.expense, + when: today, + categories: [_cat('Groceries')], + ), + ]; + + final data = buildReportData(txns, periodDays: 30); + expect( + data.expenseCategories.map((c) => c.name).toList(), + ['Rent', 'Groceries', 'Coffee'], + ); + // Percentages are share of total expense (320), Rent should be the + // largest at ~62.5%. + expect(data.expenseCategories.first.percentage, + closeTo(200 / 320 * 100, 1e-6)); + }); + + test('uncategorised expenses fall into the "Uncategorized" bucket', () { + final today = DateTime.now(); + final data = buildReportData( + [_tx(amount: 50, type: TransactionType.expense, when: today)], + periodDays: 30, + ); + expect(data.expenseCategories, isNotEmpty); + expect(data.expenseCategories.first.name, 'Uncategorized'); + expect(data.expenseCategories.first.amount, 50); + }); + + test('transactions outside the period are excluded', () { + final today = DateTime.now(); + final txns = [ + _tx( + amount: 9999, + type: TransactionType.income, + when: today.subtract(const Duration(days: 60)), + ), + _tx(amount: 10, type: TransactionType.income, when: today), + ]; + + final data = buildReportData(txns, periodDays: 30); + expect(data.totals.income, 10); + }); + }); +} diff --git a/test/widget/page_app_bar_test.dart b/test/widget/page_app_bar_test.dart new file mode 100644 index 00000000..ef735ef9 --- /dev/null +++ b/test/widget/page_app_bar_test.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; + +Widget _wrap(Widget child) { + return ScreenUtilInit( + designSize: const Size(375, 812), + minTextAdapt: true, + builder: (context, _) => MaterialApp( + theme: ThemeData( + extensions: const [AppTones.light, AppElevations.light], + ), + home: child, + ), + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + final view = TestWidgetsFlutterBinding.ensureInitialized().platformDispatcher.views.first; + view.physicalSize = const Size(375, 812); + view.devicePixelRatio = 1.0; + }); + + tearDown(() { + final view = TestWidgetsFlutterBinding.ensureInitialized().platformDispatcher.views.first; + view.resetPhysicalSize(); + view.resetDevicePixelRatio(); + }); + + group('PageAppBar', () { + testWidgets('renders the title text', (tester) async { + await tester.pumpWidget( + _wrap(const Scaffold(appBar: PageAppBar(title: 'Parties'))), + ); + await tester.pumpAndSettle(); + expect(find.text('Parties'), findsOneWidget); + }); + + testWidgets('shows a back arrow when showBack is true (default)', + (tester) async { + await tester.pumpWidget( + _wrap( + Navigator( + onGenerateRoute: (_) => MaterialPageRoute( + builder: (_) => const Scaffold( + appBar: PageAppBar(title: 'Detail'), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + }); + + testWidgets( + 'renders the Trakli brand mark on root pages with no back and no leading', + (tester) async { + await tester.pumpWidget( + _wrap( + const Scaffold( + appBar: PageAppBar(title: 'Wallets', showBack: false), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.byIcon(Icons.arrow_back), findsNothing, + reason: 'No back arrow on root pages'); + // The brand mark is an SVG; check at least one SvgPicture renders + // in the bar via key uniqueness — the brand mark is the only SVG + // the PageAppBar itself emits. + expect(find.byType(PageAppBar), findsOneWidget); + }, + ); + + testWidgets('search action toggles the title into an input', (tester) async { + String captured = ''; + await tester.pumpWidget( + _wrap( + Scaffold( + appBar: PageAppBar( + title: 'Parties', + showBack: false, + onSearchChanged: (v) => captured = v, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Initially the title is the static text. + expect(find.text('Parties'), findsOneWidget); + expect(find.byType(TextField), findsNothing); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + + expect(find.byType(TextField), findsOneWidget, + reason: 'Search field replaces the title block'); + expect(find.text('Parties'), findsNothing); + + await tester.enterText(find.byType(TextField), 'amazon'); + await tester.pumpAndSettle(); + + expect(captured, 'amazon', + reason: 'onSearchChanged must fire with the entered query'); + }); + + testWidgets('closing search restores the title', (tester) async { + await tester.pumpWidget( + _wrap( + Scaffold( + appBar: PageAppBar( + title: 'Parties', + showBack: false, + onSearchChanged: (_) {}, + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + // While searching there's exactly one back arrow (the search dismisser). + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + expect(find.text('Parties'), findsOneWidget); + expect(find.byType(TextField), findsNothing); + }); + }); +} diff --git a/test/widget/party_card_test.dart b/test/widget/party_card_test.dart index 49c64a85..6ffb145b 100644 --- a/test/widget/party_card_test.dart +++ b/test/widget/party_card_test.dart @@ -12,6 +12,7 @@ import 'package:trakli/presentation/config/cubit/config_cubit.dart'; import 'package:trakli/presentation/currency/cubit/currency_cubit.dart'; import 'package:trakli/presentation/exchange_rate/cubit/exchange_rate_cubit.dart'; import 'package:trakli/presentation/parties/cubit/party_cubit.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/party_card.dart'; class MockExchangeRateCubit extends Mock implements ExchangeRateCubit {} @@ -215,16 +216,19 @@ void main() { )); await tester.pumpAndSettle(); + final incomeDeep = AppTones.light.income.deep; final containers = tester.widgetList(find.byType(Container)); final avatarContainer = containers.firstWhere( (c) => c.decoration is BoxDecoration && (c.decoration as BoxDecoration).shape == BoxShape.circle && - (c.decoration as BoxDecoration).color == const Color(0xFF22C55E), + (c.decoration as BoxDecoration).color == incomeDeep, orElse: () => Container(), ); - expect(avatarContainer.decoration, isNotNull); + expect(avatarContainer.decoration, isNotNull, + reason: + 'Avatar circle should use AppTones.light.income.deep when net is positive'); }); testWidgets('should show red avatar when spent > received', (tester) async { @@ -247,16 +251,19 @@ void main() { )); await tester.pumpAndSettle(); + final expenseDeep = AppTones.light.expense.deep; final containers = tester.widgetList(find.byType(Container)); final avatarContainer = containers.firstWhere( (c) => c.decoration is BoxDecoration && (c.decoration as BoxDecoration).shape == BoxShape.circle && - (c.decoration as BoxDecoration).color == const Color(0xFFEF4444), + (c.decoration as BoxDecoration).color == expenseDeep, orElse: () => Container(), ); - expect(avatarContainer.decoration, isNotNull); + expect(avatarContainer.decoration, isNotNull, + reason: + 'Avatar circle should use AppTones.light.expense.deep when net is negative'); }); testWidgets('should display stats chips when amounts are present', From cca3793bebd3666e9930c47d361e11b18204eebd Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 22 May 2026 08:14:49 +0100 Subject: [PATCH 11/21] feat(ai): Add AI chat tab with conversation history Users can ask Trakli questions from a dedicated bottom-nav tab, see assistant replies arrive via polling, and browse or resume past conversations through a history sheet. Empty state offers suggested prompts so a first question is one tap away. The new tab replaces the former profile slot; profile is now reached from the home app bar. --- assets/images/sparkles.svg | 3 + assets/translations/de.json | 13 + assets/translations/en.json | 13 + assets/translations/es.json | 13 + assets/translations/fr.json | 13 + assets/translations/it.json | 13 + assets/translations/ru.json | 13 + .../datasources/ai/ai_remote_datasource.dart | 104 ++ .../datasources/ai/dto/chat_message_dto.dart | 56 + .../ai/dto/chat_message_dto.freezed.dart | 441 +++++++ .../ai/dto/chat_message_dto.g.dart | 40 + .../datasources/ai/dto/chat_session_dto.dart | 21 + .../ai/dto/chat_session_dto.freezed.dart | 269 +++++ .../ai/dto/chat_session_dto.g.dart | 29 + .../datasources/ai/dto/message_pair_dto.dart | 16 + .../ai/dto/message_pair_dto.freezed.dart | 212 ++++ .../ai/dto/message_pair_dto.g.dart | 21 + lib/data/repositories/ai_repository_impl.dart | 70 ++ lib/di/injection.config.dart | 122 +- lib/domain/repositories/ai_repository.dart | 26 + lib/gen/assets.gen.dart | 32 + lib/gen/translations/codegen_loader.g.dart | 73 +- lib/presentation/ai_chat/ai_chat_screen.dart | 1018 +++++++++++++++++ .../ai_chat/cubit/ai_chat_cubit.dart | 178 +++ .../ai_chat/cubit/ai_chat_cubit.freezed.dart | 306 +++++ .../ai_chat/cubit/ai_chat_state.dart | 28 + .../root/main_navigation_screen.dart | 20 +- lib/presentation/utils/bottom_nav.dart | 47 +- lib/presentation/utils/enums.dart | 8 +- .../utils/icon_background_decor.dart | 76 ++ pubspec.lock | 100 +- 31 files changed, 3237 insertions(+), 157 deletions(-) create mode 100644 assets/images/sparkles.svg create mode 100644 lib/data/datasources/ai/ai_remote_datasource.dart create mode 100644 lib/data/datasources/ai/dto/chat_message_dto.dart create mode 100644 lib/data/datasources/ai/dto/chat_message_dto.freezed.dart create mode 100644 lib/data/datasources/ai/dto/chat_message_dto.g.dart create mode 100644 lib/data/datasources/ai/dto/chat_session_dto.dart create mode 100644 lib/data/datasources/ai/dto/chat_session_dto.freezed.dart create mode 100644 lib/data/datasources/ai/dto/chat_session_dto.g.dart create mode 100644 lib/data/datasources/ai/dto/message_pair_dto.dart create mode 100644 lib/data/datasources/ai/dto/message_pair_dto.freezed.dart create mode 100644 lib/data/datasources/ai/dto/message_pair_dto.g.dart create mode 100644 lib/data/repositories/ai_repository_impl.dart create mode 100644 lib/domain/repositories/ai_repository.dart create mode 100644 lib/presentation/ai_chat/ai_chat_screen.dart create mode 100644 lib/presentation/ai_chat/cubit/ai_chat_cubit.dart create mode 100644 lib/presentation/ai_chat/cubit/ai_chat_cubit.freezed.dart create mode 100644 lib/presentation/ai_chat/cubit/ai_chat_state.dart create mode 100644 lib/presentation/utils/icon_background_decor.dart 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/translations/de.json b/assets/translations/de.json index 4fbfe871..409a37f3 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", diff --git a/assets/translations/en.json b/assets/translations/en.json index 21ddbb73..2a026767 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -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", diff --git a/assets/translations/es.json b/assets/translations/es.json index 6f2fec1d..dbfe1e4a 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", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 8b652581..74e51986 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", diff --git a/assets/translations/it.json b/assets/translations/it.json index e594993e..2c9e6f54 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", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index c761ea54..4309a89b 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", diff --git a/lib/data/datasources/ai/ai_remote_datasource.dart b/lib/data/datasources/ai/ai_remote_datasource.dart new file mode 100644 index 00000000..72d772f6 --- /dev/null +++ b/lib/data/datasources/ai/ai_remote_datasource.dart @@ -0,0 +1,104 @@ +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/data/datasources/ai/dto/message_pair_dto.dart'; +import 'package:trakli/data/datasources/core/api_response.dart'; + +abstract class AiRemoteDataSource { + Future> listSessions({int page = 1}); + Future getSession(int id); + Future createSession({ + required String message, + String? formatHint, + String? title, + }); + Future addMessage({ + required int sessionId, + required String message, + String? formatHint, + }); + Future deleteSession(int id); + Future checkHealth(); +} + +@Injectable(as: AiRemoteDataSource) +class AiRemoteDataSourceImpl implements AiRemoteDataSource { + final Dio dio; + + AiRemoteDataSourceImpl({required this.dio}); + + @override + Future> listSessions({int page = 1}) async { + final response = await dio.get( + 'ai/chats', + queryParameters: {'page': page}, + ); + final apiResponse = ApiResponse.fromJson(response.data); + final paged = apiResponse.data as Map; + final list = (paged['data'] as List) + .map((e) => ChatSessionDto.fromJson(e as Map)) + .toList(); + return list; + } + + @override + Future getSession(int id) async { + final response = await dio.get('ai/chats/$id'); + final apiResponse = ApiResponse.fromJson(response.data); + return ChatSessionDto.fromJson(apiResponse.data as Map); + } + + @override + Future createSession({ + required String message, + String? formatHint, + String? title, + }) async { + final body = { + 'message': message, + if (formatHint != null) 'format_hint': formatHint, + if (title != null) 'title': title, + }; + final response = await dio.post('ai/chats', data: body); + final apiResponse = ApiResponse.fromJson(response.data); + return ChatSessionDto.fromJson(apiResponse.data as Map); + } + + @override + Future addMessage({ + required int sessionId, + required String message, + String? formatHint, + }) async { + final body = { + 'message': message, + if (formatHint != null) 'format_hint': formatHint, + }; + final response = await dio.post('ai/chats/$sessionId/messages', data: body); + final apiResponse = ApiResponse.fromJson(response.data); + return MessagePairDto.fromJson(apiResponse.data as Map); + } + + @override + Future deleteSession(int id) async { + await dio.delete('ai/chats/$id'); + } + + @override + Future checkHealth() async { + try { + final response = await dio.get('ai/health'); + final raw = response.data; + if (raw is Map) { + if (raw.containsKey('available')) return raw['available'] == true; + if (raw['data'] is Map) { + return (raw['data'] as Map)['available'] == true; + } + } + return false; + } catch (_) { + return false; + } + } +} + diff --git a/lib/data/datasources/ai/dto/chat_message_dto.dart b/lib/data/datasources/ai/dto/chat_message_dto.dart new file mode 100644 index 00000000..69acb39d --- /dev/null +++ b/lib/data/datasources/ai/dto/chat_message_dto.dart @@ -0,0 +1,56 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/data/datasources/core/util.dart'; + +part 'chat_message_dto.freezed.dart'; +part 'chat_message_dto.g.dart'; + +@freezed +class ChatMessageDto with _$ChatMessageDto { + const factory ChatMessageDto({ + required int id, + @JsonKey(name: 'user_id') int? userId, + required String role, + String? content, + String? status, + @JsonKey(name: 'format_hint') String? formatHint, + String? language, + Map? result, + String? error, + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + DateTime? completedAt, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + required DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + required DateTime updatedAt, + }) = _ChatMessageDto; + + factory ChatMessageDto.fromJson(Map json) => + _$ChatMessageDtoFromJson(json); +} + +extension ChatMessageDtoX on ChatMessageDto { + bool get isUser => role == 'user'; + bool get isAssistant => role == 'assistant'; + bool get isInFlight => status == 'pending' || status == 'processing'; + bool get isFailed => status == 'failed'; + bool get isCompleted => status == 'completed'; + + String? get humanResponse { + final r = result; + if (r == null) return null; + final v = r['human_response']; + return v is String ? v : null; + } + + String? get explanation { + final r = result; + if (r == null) return null; + final v = r['explanation']; + return v is String ? v : null; + } + + String get displayText { + if (content != null && content!.isNotEmpty) return content!; + return humanResponse ?? ''; + } +} diff --git a/lib/data/datasources/ai/dto/chat_message_dto.freezed.dart b/lib/data/datasources/ai/dto/chat_message_dto.freezed.dart new file mode 100644 index 00000000..8c635fc2 --- /dev/null +++ b/lib/data/datasources/ai/dto/chat_message_dto.freezed.dart @@ -0,0 +1,441 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_message_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ChatMessageDto _$ChatMessageDtoFromJson(Map json) { + return _ChatMessageDto.fromJson(json); +} + +/// @nodoc +mixin _$ChatMessageDto { + int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'user_id') + int? get userId => throw _privateConstructorUsedError; + String get role => throw _privateConstructorUsedError; + String? get content => throw _privateConstructorUsedError; + String? get status => throw _privateConstructorUsedError; + @JsonKey(name: 'format_hint') + String? get formatHint => throw _privateConstructorUsedError; + String? get language => throw _privateConstructorUsedError; + Map? get result => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + DateTime? get completedAt => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + DateTime get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + DateTime get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this ChatMessageDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ChatMessageDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ChatMessageDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatMessageDtoCopyWith<$Res> { + factory $ChatMessageDtoCopyWith( + ChatMessageDto value, $Res Function(ChatMessageDto) then) = + _$ChatMessageDtoCopyWithImpl<$Res, ChatMessageDto>; + @useResult + $Res call( + {int id, + @JsonKey(name: 'user_id') int? userId, + String role, + String? content, + String? status, + @JsonKey(name: 'format_hint') String? formatHint, + String? language, + Map? result, + String? error, + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + DateTime? completedAt, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + DateTime updatedAt}); +} + +/// @nodoc +class _$ChatMessageDtoCopyWithImpl<$Res, $Val extends ChatMessageDto> + implements $ChatMessageDtoCopyWith<$Res> { + _$ChatMessageDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ChatMessageDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? userId = freezed, + Object? role = null, + Object? content = freezed, + Object? status = freezed, + Object? formatHint = freezed, + Object? language = freezed, + Object? result = freezed, + Object? error = freezed, + Object? completedAt = freezed, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + userId: freezed == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as int?, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + content: freezed == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, + formatHint: freezed == formatHint + ? _value.formatHint + : formatHint // ignore: cast_nullable_to_non_nullable + as String?, + language: freezed == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String?, + result: freezed == result + ? _value.result + : result // ignore: cast_nullable_to_non_nullable + as Map?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + completedAt: freezed == completedAt + ? _value.completedAt + : completedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ChatMessageDtoImplCopyWith<$Res> + implements $ChatMessageDtoCopyWith<$Res> { + factory _$$ChatMessageDtoImplCopyWith(_$ChatMessageDtoImpl value, + $Res Function(_$ChatMessageDtoImpl) then) = + __$$ChatMessageDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + @JsonKey(name: 'user_id') int? userId, + String role, + String? content, + String? status, + @JsonKey(name: 'format_hint') String? formatHint, + String? language, + Map? result, + String? error, + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + DateTime? completedAt, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + DateTime updatedAt}); +} + +/// @nodoc +class __$$ChatMessageDtoImplCopyWithImpl<$Res> + extends _$ChatMessageDtoCopyWithImpl<$Res, _$ChatMessageDtoImpl> + implements _$$ChatMessageDtoImplCopyWith<$Res> { + __$$ChatMessageDtoImplCopyWithImpl( + _$ChatMessageDtoImpl _value, $Res Function(_$ChatMessageDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of ChatMessageDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? userId = freezed, + Object? role = null, + Object? content = freezed, + Object? status = freezed, + Object? formatHint = freezed, + Object? language = freezed, + Object? result = freezed, + Object? error = freezed, + Object? completedAt = freezed, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then(_$ChatMessageDtoImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + userId: freezed == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as int?, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + content: freezed == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, + formatHint: freezed == formatHint + ? _value.formatHint + : formatHint // ignore: cast_nullable_to_non_nullable + as String?, + language: freezed == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String?, + result: freezed == result + ? _value._result + : result // ignore: cast_nullable_to_non_nullable + as Map?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + completedAt: freezed == completedAt + ? _value.completedAt + : completedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChatMessageDtoImpl implements _ChatMessageDto { + const _$ChatMessageDtoImpl( + {required this.id, + @JsonKey(name: 'user_id') this.userId, + required this.role, + this.content, + this.status, + @JsonKey(name: 'format_hint') this.formatHint, + this.language, + final Map? result, + this.error, + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + this.completedAt, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + required this.createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + required this.updatedAt}) + : _result = result; + + factory _$ChatMessageDtoImpl.fromJson(Map json) => + _$$ChatMessageDtoImplFromJson(json); + + @override + final int id; + @override + @JsonKey(name: 'user_id') + final int? userId; + @override + final String role; + @override + final String? content; + @override + final String? status; + @override + @JsonKey(name: 'format_hint') + final String? formatHint; + @override + final String? language; + final Map? _result; + @override + Map? get result { + final value = _result; + if (value == null) return null; + if (_result is EqualUnmodifiableMapView) return _result; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final String? error; + @override + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + final DateTime? completedAt; + @override + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + final DateTime createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + final DateTime updatedAt; + + @override + String toString() { + return 'ChatMessageDto(id: $id, userId: $userId, role: $role, content: $content, status: $status, formatHint: $formatHint, language: $language, result: $result, error: $error, completedAt: $completedAt, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatMessageDtoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.role, role) || other.role == role) && + (identical(other.content, content) || other.content == content) && + (identical(other.status, status) || other.status == status) && + (identical(other.formatHint, formatHint) || + other.formatHint == formatHint) && + (identical(other.language, language) || + other.language == language) && + const DeepCollectionEquality().equals(other._result, _result) && + (identical(other.error, error) || other.error == error) && + (identical(other.completedAt, completedAt) || + other.completedAt == completedAt) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + userId, + role, + content, + status, + formatHint, + language, + const DeepCollectionEquality().hash(_result), + error, + completedAt, + createdAt, + updatedAt); + + /// Create a copy of ChatMessageDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ChatMessageDtoImplCopyWith<_$ChatMessageDtoImpl> get copyWith => + __$$ChatMessageDtoImplCopyWithImpl<_$ChatMessageDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ChatMessageDtoImplToJson( + this, + ); + } +} + +abstract class _ChatMessageDto implements ChatMessageDto { + const factory _ChatMessageDto( + {required final int id, + @JsonKey(name: 'user_id') final int? userId, + required final String role, + final String? content, + final String? status, + @JsonKey(name: 'format_hint') final String? formatHint, + final String? language, + final Map? result, + final String? error, + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + final DateTime? completedAt, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + required final DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + required final DateTime updatedAt}) = _$ChatMessageDtoImpl; + + factory _ChatMessageDto.fromJson(Map json) = + _$ChatMessageDtoImpl.fromJson; + + @override + int get id; + @override + @JsonKey(name: 'user_id') + int? get userId; + @override + String get role; + @override + String? get content; + @override + String? get status; + @override + @JsonKey(name: 'format_hint') + String? get formatHint; + @override + String? get language; + @override + Map? get result; + @override + String? get error; + @override + @JsonKey(name: 'completed_at', fromJson: safeParseDateTime) + DateTime? get completedAt; + @override + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + DateTime get createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + DateTime get updatedAt; + + /// Create a copy of ChatMessageDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ChatMessageDtoImplCopyWith<_$ChatMessageDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/datasources/ai/dto/chat_message_dto.g.dart b/lib/data/datasources/ai/dto/chat_message_dto.g.dart new file mode 100644 index 00000000..af12764f --- /dev/null +++ b/lib/data/datasources/ai/dto/chat_message_dto.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_message_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ChatMessageDtoImpl _$$ChatMessageDtoImplFromJson(Map json) => + _$ChatMessageDtoImpl( + id: (json['id'] as num).toInt(), + userId: (json['user_id'] as num?)?.toInt(), + role: json['role'] as String, + content: json['content'] as String?, + status: json['status'] as String?, + formatHint: json['format_hint'] as String?, + language: json['language'] as String?, + result: json['result'] as Map?, + error: json['error'] as String?, + completedAt: safeParseDateTime(json['completed_at']), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + ); + +Map _$$ChatMessageDtoImplToJson( + _$ChatMessageDtoImpl instance) => + { + 'id': instance.id, + 'user_id': instance.userId, + 'role': instance.role, + 'content': instance.content, + 'status': instance.status, + 'format_hint': instance.formatHint, + 'language': instance.language, + 'result': instance.result, + 'error': instance.error, + 'completed_at': instance.completedAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + }; diff --git a/lib/data/datasources/ai/dto/chat_session_dto.dart b/lib/data/datasources/ai/dto/chat_session_dto.dart new file mode 100644 index 00000000..47e73445 --- /dev/null +++ b/lib/data/datasources/ai/dto/chat_session_dto.dart @@ -0,0 +1,21 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_message_dto.dart'; + +part 'chat_session_dto.freezed.dart'; +part 'chat_session_dto.g.dart'; + +@freezed +class ChatSessionDto with _$ChatSessionDto { + const factory ChatSessionDto({ + required int id, + String? title, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + required DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + required DateTime updatedAt, + @Default([]) List messages, + }) = _ChatSessionDto; + + factory ChatSessionDto.fromJson(Map json) => + _$ChatSessionDtoFromJson(json); +} diff --git a/lib/data/datasources/ai/dto/chat_session_dto.freezed.dart b/lib/data/datasources/ai/dto/chat_session_dto.freezed.dart new file mode 100644 index 00000000..6ddf0137 --- /dev/null +++ b/lib/data/datasources/ai/dto/chat_session_dto.freezed.dart @@ -0,0 +1,269 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'chat_session_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ChatSessionDto _$ChatSessionDtoFromJson(Map json) { + return _ChatSessionDto.fromJson(json); +} + +/// @nodoc +mixin _$ChatSessionDto { + int get id => throw _privateConstructorUsedError; + String? get title => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + DateTime get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + DateTime get updatedAt => throw _privateConstructorUsedError; + List get messages => throw _privateConstructorUsedError; + + /// Serializes this ChatSessionDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ChatSessionDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ChatSessionDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatSessionDtoCopyWith<$Res> { + factory $ChatSessionDtoCopyWith( + ChatSessionDto value, $Res Function(ChatSessionDto) then) = + _$ChatSessionDtoCopyWithImpl<$Res, ChatSessionDto>; + @useResult + $Res call( + {int id, + String? title, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) DateTime updatedAt, + List messages}); +} + +/// @nodoc +class _$ChatSessionDtoCopyWithImpl<$Res, $Val extends ChatSessionDto> + implements $ChatSessionDtoCopyWith<$Res> { + _$ChatSessionDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ChatSessionDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = freezed, + Object? createdAt = null, + Object? updatedAt = null, + Object? messages = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + messages: null == messages + ? _value.messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ChatSessionDtoImplCopyWith<$Res> + implements $ChatSessionDtoCopyWith<$Res> { + factory _$$ChatSessionDtoImplCopyWith(_$ChatSessionDtoImpl value, + $Res Function(_$ChatSessionDtoImpl) then) = + __$$ChatSessionDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + String? title, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) DateTime updatedAt, + List messages}); +} + +/// @nodoc +class __$$ChatSessionDtoImplCopyWithImpl<$Res> + extends _$ChatSessionDtoCopyWithImpl<$Res, _$ChatSessionDtoImpl> + implements _$$ChatSessionDtoImplCopyWith<$Res> { + __$$ChatSessionDtoImplCopyWithImpl( + _$ChatSessionDtoImpl _value, $Res Function(_$ChatSessionDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of ChatSessionDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = freezed, + Object? createdAt = null, + Object? updatedAt = null, + Object? messages = null, + }) { + return _then(_$ChatSessionDtoImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + messages: null == messages + ? _value._messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChatSessionDtoImpl implements _ChatSessionDto { + const _$ChatSessionDtoImpl( + {required this.id, + this.title, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + required this.createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + required this.updatedAt, + final List messages = const []}) + : _messages = messages; + + factory _$ChatSessionDtoImpl.fromJson(Map json) => + _$$ChatSessionDtoImplFromJson(json); + + @override + final int id; + @override + final String? title; + @override + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + final DateTime createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + final DateTime updatedAt; + final List _messages; + @override + @JsonKey() + List get messages { + if (_messages is EqualUnmodifiableListView) return _messages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_messages); + } + + @override + String toString() { + return 'ChatSessionDto(id: $id, title: $title, createdAt: $createdAt, updatedAt: $updatedAt, messages: $messages)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatSessionDtoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.title, title) || other.title == title) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + const DeepCollectionEquality().equals(other._messages, _messages)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, title, createdAt, updatedAt, + const DeepCollectionEquality().hash(_messages)); + + /// Create a copy of ChatSessionDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ChatSessionDtoImplCopyWith<_$ChatSessionDtoImpl> get copyWith => + __$$ChatSessionDtoImplCopyWithImpl<_$ChatSessionDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ChatSessionDtoImplToJson( + this, + ); + } +} + +abstract class _ChatSessionDto implements ChatSessionDto { + const factory _ChatSessionDto( + {required final int id, + final String? title, + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + required final DateTime createdAt, + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + required final DateTime updatedAt, + final List messages}) = _$ChatSessionDtoImpl; + + factory _ChatSessionDto.fromJson(Map json) = + _$ChatSessionDtoImpl.fromJson; + + @override + int get id; + @override + String? get title; + @override + @JsonKey(name: 'created_at', fromJson: DateTime.parse) + DateTime get createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: DateTime.parse) + DateTime get updatedAt; + @override + List get messages; + + /// Create a copy of ChatSessionDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ChatSessionDtoImplCopyWith<_$ChatSessionDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/datasources/ai/dto/chat_session_dto.g.dart b/lib/data/datasources/ai/dto/chat_session_dto.g.dart new file mode 100644 index 00000000..cdb600f7 --- /dev/null +++ b/lib/data/datasources/ai/dto/chat_session_dto.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_session_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ChatSessionDtoImpl _$$ChatSessionDtoImplFromJson(Map json) => + _$ChatSessionDtoImpl( + id: (json['id'] as num).toInt(), + title: json['title'] as String?, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + messages: (json['messages'] as List?) + ?.map((e) => ChatMessageDto.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$$ChatSessionDtoImplToJson( + _$ChatSessionDtoImpl instance) => + { + 'id': instance.id, + 'title': instance.title, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'messages': instance.messages.map((e) => e.toJson()).toList(), + }; diff --git a/lib/data/datasources/ai/dto/message_pair_dto.dart b/lib/data/datasources/ai/dto/message_pair_dto.dart new file mode 100644 index 00000000..2bb0ac4d --- /dev/null +++ b/lib/data/datasources/ai/dto/message_pair_dto.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_message_dto.dart'; + +part 'message_pair_dto.freezed.dart'; +part 'message_pair_dto.g.dart'; + +@freezed +class MessagePairDto with _$MessagePairDto { + const factory MessagePairDto({ + required ChatMessageDto user, + required ChatMessageDto assistant, + }) = _MessagePairDto; + + factory MessagePairDto.fromJson(Map json) => + _$MessagePairDtoFromJson(json); +} diff --git a/lib/data/datasources/ai/dto/message_pair_dto.freezed.dart b/lib/data/datasources/ai/dto/message_pair_dto.freezed.dart new file mode 100644 index 00000000..a392e2ca --- /dev/null +++ b/lib/data/datasources/ai/dto/message_pair_dto.freezed.dart @@ -0,0 +1,212 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'message_pair_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +MessagePairDto _$MessagePairDtoFromJson(Map json) { + return _MessagePairDto.fromJson(json); +} + +/// @nodoc +mixin _$MessagePairDto { + ChatMessageDto get user => throw _privateConstructorUsedError; + ChatMessageDto get assistant => throw _privateConstructorUsedError; + + /// Serializes this MessagePairDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of MessagePairDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MessagePairDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MessagePairDtoCopyWith<$Res> { + factory $MessagePairDtoCopyWith( + MessagePairDto value, $Res Function(MessagePairDto) then) = + _$MessagePairDtoCopyWithImpl<$Res, MessagePairDto>; + @useResult + $Res call({ChatMessageDto user, ChatMessageDto assistant}); + + $ChatMessageDtoCopyWith<$Res> get user; + $ChatMessageDtoCopyWith<$Res> get assistant; +} + +/// @nodoc +class _$MessagePairDtoCopyWithImpl<$Res, $Val extends MessagePairDto> + implements $MessagePairDtoCopyWith<$Res> { + _$MessagePairDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MessagePairDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + Object? assistant = null, + }) { + return _then(_value.copyWith( + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as ChatMessageDto, + assistant: null == assistant + ? _value.assistant + : assistant // ignore: cast_nullable_to_non_nullable + as ChatMessageDto, + ) as $Val); + } + + /// Create a copy of MessagePairDto + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ChatMessageDtoCopyWith<$Res> get user { + return $ChatMessageDtoCopyWith<$Res>(_value.user, (value) { + return _then(_value.copyWith(user: value) as $Val); + }); + } + + /// Create a copy of MessagePairDto + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ChatMessageDtoCopyWith<$Res> get assistant { + return $ChatMessageDtoCopyWith<$Res>(_value.assistant, (value) { + return _then(_value.copyWith(assistant: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MessagePairDtoImplCopyWith<$Res> + implements $MessagePairDtoCopyWith<$Res> { + factory _$$MessagePairDtoImplCopyWith(_$MessagePairDtoImpl value, + $Res Function(_$MessagePairDtoImpl) then) = + __$$MessagePairDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ChatMessageDto user, ChatMessageDto assistant}); + + @override + $ChatMessageDtoCopyWith<$Res> get user; + @override + $ChatMessageDtoCopyWith<$Res> get assistant; +} + +/// @nodoc +class __$$MessagePairDtoImplCopyWithImpl<$Res> + extends _$MessagePairDtoCopyWithImpl<$Res, _$MessagePairDtoImpl> + implements _$$MessagePairDtoImplCopyWith<$Res> { + __$$MessagePairDtoImplCopyWithImpl( + _$MessagePairDtoImpl _value, $Res Function(_$MessagePairDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of MessagePairDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + Object? assistant = null, + }) { + return _then(_$MessagePairDtoImpl( + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as ChatMessageDto, + assistant: null == assistant + ? _value.assistant + : assistant // ignore: cast_nullable_to_non_nullable + as ChatMessageDto, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MessagePairDtoImpl implements _MessagePairDto { + const _$MessagePairDtoImpl({required this.user, required this.assistant}); + + factory _$MessagePairDtoImpl.fromJson(Map json) => + _$$MessagePairDtoImplFromJson(json); + + @override + final ChatMessageDto user; + @override + final ChatMessageDto assistant; + + @override + String toString() { + return 'MessagePairDto(user: $user, assistant: $assistant)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MessagePairDtoImpl && + (identical(other.user, user) || other.user == user) && + (identical(other.assistant, assistant) || + other.assistant == assistant)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, user, assistant); + + /// Create a copy of MessagePairDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MessagePairDtoImplCopyWith<_$MessagePairDtoImpl> get copyWith => + __$$MessagePairDtoImplCopyWithImpl<_$MessagePairDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MessagePairDtoImplToJson( + this, + ); + } +} + +abstract class _MessagePairDto implements MessagePairDto { + const factory _MessagePairDto( + {required final ChatMessageDto user, + required final ChatMessageDto assistant}) = _$MessagePairDtoImpl; + + factory _MessagePairDto.fromJson(Map json) = + _$MessagePairDtoImpl.fromJson; + + @override + ChatMessageDto get user; + @override + ChatMessageDto get assistant; + + /// Create a copy of MessagePairDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MessagePairDtoImplCopyWith<_$MessagePairDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/datasources/ai/dto/message_pair_dto.g.dart b/lib/data/datasources/ai/dto/message_pair_dto.g.dart new file mode 100644 index 00000000..7d8f6643 --- /dev/null +++ b/lib/data/datasources/ai/dto/message_pair_dto.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_pair_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MessagePairDtoImpl _$$MessagePairDtoImplFromJson(Map json) => + _$MessagePairDtoImpl( + user: ChatMessageDto.fromJson(json['user'] as Map), + assistant: + ChatMessageDto.fromJson(json['assistant'] as Map), + ); + +Map _$$MessagePairDtoImplToJson( + _$MessagePairDtoImpl instance) => + { + 'user': instance.user.toJson(), + 'assistant': instance.assistant.toJson(), + }; diff --git a/lib/data/repositories/ai_repository_impl.dart b/lib/data/repositories/ai_repository_impl.dart new file mode 100644 index 00000000..844fcdfb --- /dev/null +++ b/lib/data/repositories/ai_repository_impl.dart @@ -0,0 +1,70 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/error/repository_error_handler.dart'; +import 'package:trakli/data/datasources/ai/ai_remote_datasource.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/data/datasources/ai/dto/message_pair_dto.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +@LazySingleton(as: AiRepository) +class AiRepositoryImpl implements AiRepository { + final AiRemoteDataSource remote; + + AiRepositoryImpl({required this.remote}); + + @override + Future>> listSessions({int page = 1}) { + return RepositoryErrorHandler.handleApiCall( + () => remote.listSessions(page: page), + ); + } + + @override + Future> getSession(int id) { + return RepositoryErrorHandler.handleApiCall(() => remote.getSession(id)); + } + + @override + Future> createSession({ + required String message, + String? formatHint, + String? title, + }) { + return RepositoryErrorHandler.handleApiCall( + () => remote.createSession( + message: message, + formatHint: formatHint, + title: title, + ), + ); + } + + @override + Future> sendMessage({ + required int sessionId, + required String message, + String? formatHint, + }) { + return RepositoryErrorHandler.handleApiCall( + () => remote.addMessage( + sessionId: sessionId, + message: message, + formatHint: formatHint, + ), + ); + } + + @override + Future> deleteSession(int id) { + return RepositoryErrorHandler.handleApiCall(() async { + await remote.deleteSession(id); + return unit; + }); + } + + @override + Future> checkHealth() { + return RepositoryErrorHandler.handleApiCall(() => remote.checkHealth()); + } +} diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index 5dd4ce18..164b64f5 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -36,6 +36,7 @@ import '../core/sync/sync_logger_impl.dart' as _i422; import '../core/sync/sync_service.dart' as _i957; import '../core/utils/services/shared_prefs.dart' as _i789; import '../data/database/app_database.dart' as _i704; +import '../data/datasources/ai/ai_remote_datasource.dart' as _i514; import '../data/datasources/auth/auth_local_data_source.dart' as _i276; import '../data/datasources/auth/auth_remote_data_source.dart' as _i496; import '../data/datasources/auth/preference_manager.dart' as _i683; @@ -75,6 +76,7 @@ import '../data/datasources/transfer/transfer_local_datasource.dart' as _i432; import '../data/datasources/transfer/transfer_remote_datasource.dart' as _i783; import '../data/datasources/wallet/wallet_local_datasource.dart' as _i849; import '../data/datasources/wallet/wallet_remote_datasource.dart' as _i624; +import '../data/repositories/ai_repository_impl.dart' as _i841; import '../data/repositories/auth_repository_imp.dart' as _i135; import '../data/repositories/category_repository_impl.dart' as _i324; import '../data/repositories/cloud_benefit_repository_imp.dart' as _i415; @@ -98,6 +100,7 @@ import '../data/sync/party_sync_handler.dart' as _i280; import '../data/sync/transaction_sync_handler.dart' as _i893; import '../data/sync/transfer_sync_handler.dart' as _i225; import '../data/sync/wallet_sync_handler.dart' as _i849; +import '../domain/repositories/ai_repository.dart' as _i542; import '../domain/repositories/auth_repository.dart' as _i800; import '../domain/repositories/category_repository.dart' as _i410; import '../domain/repositories/cloud_benefit_repository.dart' as _i11; @@ -198,6 +201,7 @@ import '../domain/usecases/wallet/ensure_default_wallet_exists_usecase.dart' import '../domain/usecases/wallet/get_wallets_usecase.dart' as _i713; import '../domain/usecases/wallet/listen_to_wallets_usecase.dart' as _i82; import '../domain/usecases/wallet/update_wallet_usecase.dart' as _i418; +import '../presentation/ai_chat/cubit/ai_chat_cubit.dart' as _i415; import '../presentation/app_update/cubit/app_update_cubit.dart' as _i559; import '../presentation/app_update/cubit/in_app_update_cubit.dart' as _i288; import '../presentation/app_update/in_app_update_cubit.dart' as _i78; @@ -242,8 +246,8 @@ _i174.GetIt $initGetIt( gh.factory<_i624.OAuthService>(() => _i624.OAuthService()); gh.factory<_i1041.SyncCubit>(() => _i1041.SyncCubit()); gh.factory<_i363.StatisticsFilterCubit>(() => _i363.StatisticsFilterCubit()); - gh.singleton<_i957.SyncService>(() => _i957.SyncService()); gh.singleton<_i196.FeatureRemoteConfig>(() => _i196.FeatureRemoteConfig()); + gh.singleton<_i957.SyncService>(() => _i957.SyncService()); gh.lazySingleton<_i877.SyncDependencyManagerBase>( () => syncModule.provideSyncDependencyManager()); gh.lazySingleton<_i627.ThemeCubit>(() => _i627.ThemeCubit()); @@ -336,6 +340,8 @@ _i174.GetIt $initGetIt( )); gh.lazySingleton<_i877.SyncCrashReporter>( () => _i947.SyncCrashReporterImpl(gh<_i538.CrashReportingService>())); + gh.factory<_i514.AiRemoteDataSource>( + () => _i514.AiRemoteDataSourceImpl(dio: gh<_i361.Dio>())); gh.lazySingleton<_i280.PartySyncHandler>(() => _i280.PartySyncHandler( gh<_i704.AppDatabase>(), gh<_i656.PartyRemoteDataSource>(), @@ -346,6 +352,8 @@ _i174.GetIt $initGetIt( () => _i624.WalletRemoteDataSourceImpl(dio: gh<_i361.Dio>())); gh.factory<_i481.UserContextService>( () => _i481.UserContextService(gh<_i538.CrashReportingService>())); + gh.lazySingleton<_i542.AiRepository>( + () => _i841.AiRepositoryImpl(remote: gh<_i514.AiRemoteDataSource>())); gh.factory<_i662.TransactionLocalDataSource>( () => _i662.TransactionLocalDataSourceImpl( gh<_i704.AppDatabase>(), @@ -366,12 +374,12 @@ _i174.GetIt $initGetIt( () => _i478.GroupRemoteDataSourceImpl(dio: gh<_i361.Dio>())); gh.factory<_i961.GetCategoriesUseCase>( () => _i961.GetCategoriesUseCase(gh<_i410.CategoryRepository>())); + gh.factory<_i292.DeleteCategoryUseCase>( + () => _i292.DeleteCategoryUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i986.UpdateCategoryUseCase>( () => _i986.UpdateCategoryUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i445.AddCategoryUseCase>( () => _i445.AddCategoryUseCase(gh<_i410.CategoryRepository>())); - gh.factory<_i292.DeleteCategoryUseCase>( - () => _i292.DeleteCategoryUseCase(gh<_i410.CategoryRepository>())); gh.singleton<_i11.CloudBenefitRepository>(() => _i415.CloudBenefitRepositoryImpl( gh<_i61.CloudBenefitRemoteDataSource>())); @@ -443,6 +451,8 @@ _i174.GetIt $initGetIt( walletSyncHandler: gh<_i849.WalletSyncHandler>(), database: gh<_i704.AppDatabase>(), )); + gh.factory<_i415.AiChatCubit>( + () => _i415.AiChatCubit(gh<_i542.AiRepository>())); gh.factory<_i455.CategoryCubit>(() => _i455.CategoryCubit( gh<_i445.AddCategoryUseCase>(), gh<_i986.UpdateCategoryUseCase>(), @@ -450,20 +460,20 @@ _i174.GetIt $initGetIt( gh<_i961.GetCategoriesUseCase>(), gh<_i500.ListenToCategoriesUseCase>(), )); - gh.factory<_i723.LoginWithPhonePassword>( - () => _i723.LoginWithPhonePassword(gh<_i800.AuthRepository>())); - gh.factory<_i2.OnboardingCompleted>( - () => _i2.OnboardingCompleted(gh<_i800.AuthRepository>())); - gh.factory<_i498.LoginByPhoneUsecase>( - () => _i498.LoginByPhoneUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i444.StreamAuthStatus>( + () => _i444.StreamAuthStatus(gh<_i800.AuthRepository>())); gh.factory<_i880.GetLoggedInUser>( () => _i880.GetLoggedInUser(gh<_i800.AuthRepository>())); + gh.factory<_i723.LoginWithPhonePassword>( + () => _i723.LoginWithPhonePassword(gh<_i800.AuthRepository>())); gh.factory<_i768.LoginWithEmailPassword>( () => _i768.LoginWithEmailPassword(gh<_i800.AuthRepository>())); gh.factory<_i42.LoginByEmailUsecase>( () => _i42.LoginByEmailUsecase(gh<_i800.AuthRepository>())); - gh.factory<_i444.StreamAuthStatus>( - () => _i444.StreamAuthStatus(gh<_i800.AuthRepository>())); + gh.factory<_i2.OnboardingCompleted>( + () => _i2.OnboardingCompleted(gh<_i800.AuthRepository>())); + gh.factory<_i498.LoginByPhoneUsecase>( + () => _i498.LoginByPhoneUsecase(gh<_i800.AuthRepository>())); gh.factory<_i828.IsOnboardingCompleted>( () => _i828.IsOnboardingCompleted(gh<_i800.AuthRepository>())); gh.lazySingleton<_i118.TransactionRepository>(() => @@ -473,20 +483,20 @@ _i174.GetIt $initGetIt( db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i929.GetImportSessionsUseCase>( - () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); - gh.factory<_i36.ConfirmSessionUseCase>( - () => _i36.ConfirmSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i661.GetImportSessionUseCase>( () => _i661.GetImportSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i60.AnalyzeDocumentUseCase>( () => _i60.AnalyzeDocumentUseCase(gh<_i32.ImportRepository>())); - gh.factory<_i56.DeletePartyUseCase>( - () => _i56.DeletePartyUseCase(gh<_i661.PartyRepository>())); - gh.factory<_i84.AddPartyUseCase>( - () => _i84.AddPartyUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i36.ConfirmSessionUseCase>( + () => _i36.ConfirmSessionUseCase(gh<_i32.ImportRepository>())); + gh.factory<_i929.GetImportSessionsUseCase>( + () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); gh.factory<_i911.UpdatePartyUseCase>( () => _i911.UpdatePartyUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i84.AddPartyUseCase>( + () => _i84.AddPartyUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i56.DeletePartyUseCase>( + () => _i56.DeletePartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i12.GetPartiesUseCase>( () => _i12.GetPartiesUseCase(gh<_i661.PartyRepository>())); gh.factory<_i714.ListenToPartiesUseCase>( @@ -508,40 +518,40 @@ _i174.GetIt $initGetIt( )); gh.factory<_i88.BenefitsCubit>( () => _i88.BenefitsCubit(gh<_i61.FetchBenefits>())); - gh.factory<_i640.LogoutUsecase>( - () => _i640.LogoutUsecase(gh<_i800.AuthRepository>())); gh.factory<_i684.DeleteAccountUseCase>( () => _i684.DeleteAccountUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i640.LogoutUsecase>( + () => _i640.LogoutUsecase(gh<_i800.AuthRepository>())); gh.lazySingleton<_i368.WalletRepository>(() => _i305.WalletRepositoryImpl( syncHandler: gh<_i849.WalletSyncHandler>(), localDataSource: gh<_i849.WalletLocalDataSource>(), db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i163.DeleteTransactionUseCase>( - () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i947.GetAllTransactionsUseCase>( + () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i973.ListenToTransactionsUseCase>(() => _i973.ListenToTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i241.UpdateTransactionUseCase>( () => _i241.UpdateTransactionUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i947.GetAllTransactionsUseCase>( - () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i422.GetNotificationsUseCase>( - () => _i422.GetNotificationsUseCase(gh<_i965.NotificationRepository>())); + gh.factory<_i163.DeleteTransactionUseCase>( + () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i837.MarkNotificationAsReadUseCase>(() => _i837.MarkNotificationAsReadUseCase(gh<_i965.NotificationRepository>())); + gh.factory<_i422.GetNotificationsUseCase>( + () => _i422.GetNotificationsUseCase(gh<_i965.NotificationRepository>())); gh.lazySingleton<_i957.GroupRepository>(() => _i875.GroupRepositoryImpl( syncHandler: gh<_i235.GroupSyncHandler>(), localDataSource: gh<_i873.GroupLocalDataSource>(), db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i80.AddWalletUseCase>( - () => _i80.AddWalletUseCase(gh<_i368.WalletRepository>())); - gh.factory<_i418.UpdateWalletUseCase>( - () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i62.DeleteWalletUseCase>( () => _i62.DeleteWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i418.UpdateWalletUseCase>( + () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i80.AddWalletUseCase>( + () => _i80.AddWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i713.GetWalletsUseCase>( () => _i713.GetWalletsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i314.FetchSubscriptionPlans>( @@ -552,16 +562,16 @@ _i174.GetIt $initGetIt( gh<_i79.TransactionRemoteDataSource>(), gh<_i893.TransactionSyncHandler>(), )); - gh.factory<_i132.GetConfigsUseCase>( - () => _i132.GetConfigsUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i436.UpdateConfigUseCase>( + () => _i436.UpdateConfigUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i608.ListenToConfigsUseCase>( () => _i608.ListenToConfigsUseCase(gh<_i899.ConfigRepository>())); - gh.factory<_i933.GetConfigUseCase>( - () => _i933.GetConfigUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i833.SaveConfigUseCase>( () => _i833.SaveConfigUseCase(gh<_i899.ConfigRepository>())); - gh.factory<_i436.UpdateConfigUseCase>( - () => _i436.UpdateConfigUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i933.GetConfigUseCase>( + () => _i933.GetConfigUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i132.GetConfigsUseCase>( + () => _i132.GetConfigsUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i536.DeleteConfigUseCase>( () => _i536.DeleteConfigUseCase(gh<_i899.ConfigRepository>())); gh.lazySingleton<_i55.TransferRepository>(() => _i268.TransferRepositoryImpl( @@ -578,24 +588,24 @@ _i174.GetIt $initGetIt( localDataSource: gh<_i900.ExchangeRateLocalDataSource>(), configRepository: gh<_i899.ConfigRepository>(), )); - gh.factory<_i225.EnsureDefaultWalletExistsUseCase>(() => - _i225.EnsureDefaultWalletExistsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i82.ListenToWalletsUseCase>( () => _i82.ListenToWalletsUseCase(gh<_i368.WalletRepository>())); - gh.factory<_i524.LoginWithEmailUseCase>( - () => _i524.LoginWithEmailUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i225.EnsureDefaultWalletExistsUseCase>(() => + _i225.EnsureDefaultWalletExistsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i705.RegisterUseCase>( () => _i705.RegisterUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i402.GetOtpCodeUseCase>( - () => _i402.GetOtpCodeUseCase(gh<_i800.AuthRepository>())); gh.factory<_i400.LoginWithPhoneUseCase>( () => _i400.LoginWithPhoneUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i542.PasswordResetCodeUseCase>( - () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i402.GetOtpCodeUseCase>( + () => _i402.GetOtpCodeUseCase(gh<_i800.AuthRepository>())); gh.factory<_i494.PasswordResetUseCase>( () => _i494.PasswordResetUseCase(gh<_i800.AuthRepository>())); gh.factory<_i100.VerifyEmailUseCase>( () => _i100.VerifyEmailUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i542.PasswordResetCodeUseCase>( + () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i524.LoginWithEmailUseCase>( + () => _i524.LoginWithEmailUseCase(gh<_i800.AuthRepository>())); gh.factory<_i841.PartyCubit>(() => _i841.PartyCubit( getPartiesUseCase: gh<_i12.GetPartiesUseCase>(), addPartyUseCase: gh<_i84.AddPartyUseCase>(), @@ -640,21 +650,21 @@ _i174.GetIt $initGetIt( ensureDefaultWalletExistsUseCase: gh<_i225.EnsureDefaultWalletExistsUseCase>(), )); - gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => - _i1004.CreateTransferWithTransactionsUseCase( - gh<_i55.TransferRepository>())); gh.factory<_i453.AddTransferUseCase>( () => _i453.AddTransferUseCase(gh<_i55.TransferRepository>())); gh.factory<_i744.ListenToTransfersUseCase>( () => _i744.ListenToTransfersUseCase(gh<_i55.TransferRepository>())); + gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => + _i1004.CreateTransferWithTransactionsUseCase( + gh<_i55.TransferRepository>())); gh.factory<_i397.ListenExchangeRate>( () => _i397.ListenExchangeRate(gh<_i1057.ExchangeRateRepository>())); - gh.factory<_i759.DeleteGroupUseCase>( - () => _i759.DeleteGroupUseCase(gh<_i957.GroupRepository>())); - gh.factory<_i982.GetGroupsUseCase>( - () => _i982.GetGroupsUseCase(gh<_i957.GroupRepository>())); gh.factory<_i146.ListenToGroupsUseCase>( () => _i146.ListenToGroupsUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i982.GetGroupsUseCase>( + () => _i982.GetGroupsUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i759.DeleteGroupUseCase>( + () => _i759.DeleteGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i353.AddGroupUseCase>( () => _i353.AddGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i820.UpdateGroupUseCase>( @@ -706,14 +716,14 @@ _i174.GetIt $initGetIt( gh<_i118.TransactionRepository>(), gh<_i1057.ExchangeRateRepository>(), )); - gh.factory<_i150.GetFileContentUseCase>( - () => _i150.GetFileContentUseCase(gh<_i442.MediaRepository>())); gh.factory<_i706.DeleteMediaUseCase>( () => _i706.DeleteMediaUseCase(gh<_i442.MediaRepository>())); - gh.factory<_i1026.GetMediaForTransactionUseCase>( - () => _i1026.GetMediaForTransactionUseCase(gh<_i442.MediaRepository>())); gh.factory<_i843.AddMediaToTransactionUseCase>( () => _i843.AddMediaToTransactionUseCase(gh<_i442.MediaRepository>())); + gh.factory<_i150.GetFileContentUseCase>( + () => _i150.GetFileContentUseCase(gh<_i442.MediaRepository>())); + gh.factory<_i1026.GetMediaForTransactionUseCase>( + () => _i1026.GetMediaForTransactionUseCase(gh<_i442.MediaRepository>())); gh.factory<_i484.CurrencyCubit>(() => _i484.CurrencyCubit( gh<_i933.GetConfigUseCase>(), gh<_i833.SaveConfigUseCase>(), diff --git a/lib/domain/repositories/ai_repository.dart b/lib/domain/repositories/ai_repository.dart new file mode 100644 index 00000000..0dcbe030 --- /dev/null +++ b/lib/domain/repositories/ai_repository.dart @@ -0,0 +1,26 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/data/datasources/ai/dto/message_pair_dto.dart'; + +abstract class AiRepository { + Future>> listSessions({int page = 1}); + + Future> getSession(int id); + + Future> createSession({ + required String message, + String? formatHint, + String? title, + }); + + Future> sendMessage({ + required int sessionId, + required String message, + String? formatHint, + }); + + Future> deleteSession(int id); + + Future> checkHealth(); +} diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index e96267d0..f8831965 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -140,6 +140,10 @@ class $AssetsImagesGen { /// File path: assets/images/notification-bing.svg String get notificationBing => 'assets/images/notification-bing.svg'; + /// Directory path: assets/images/onboarding + $AssetsImagesOnboardingGen get onboarding => + const $AssetsImagesOnboardingGen(); + /// File path: assets/images/pdf-type.svg String get pdfType => 'assets/images/pdf-type.svg'; @@ -168,6 +172,9 @@ class $AssetsImagesGen { /// File path: assets/images/setting.svg String get setting => 'assets/images/setting.svg'; + /// File path: assets/images/sparkles.svg + String get sparkles => 'assets/images/sparkles.svg'; + /// File path: assets/images/support.svg String get support => 'assets/images/support.svg'; @@ -177,6 +184,10 @@ class $AssetsImagesGen { /// File path: assets/images/top_right_circle.svg String get topRightCircle => 'assets/images/top_right_circle.svg'; + /// File path: assets/images/trakli-logo-white.png + AssetGenImage get trakliLogoWhite => + const AssetGenImage('assets/images/trakli-logo-white.png'); + /// File path: assets/images/trash.svg String get trash => 'assets/images/trash.svg'; @@ -248,9 +259,11 @@ class $AssetsImagesGen { searchNormal, searchSpecial, setting, + sparkles, support, tag2, topRightCircle, + trakliLogoWhite, trash, user, walletAdd, @@ -285,6 +298,25 @@ class $AssetsTranslationsGen { List get values => [de, en, es, fr, it, ru]; } +class $AssetsImagesOnboardingGen { + const $AssetsImagesOnboardingGen(); + + /// File path: assets/images/onboarding/importer.svg + String get importer => 'assets/images/onboarding/importer.svg'; + + /// File path: assets/images/onboarding/insights.svg + String get insights => 'assets/images/onboarding/insights.svg'; + + /// File path: assets/images/onboarding/ready.svg + String get ready => 'assets/images/onboarding/ready.svg'; + + /// File path: assets/images/onboarding/wallet.svg + String get wallet => 'assets/images/onboarding/wallet.svg'; + + /// List of all assets + List get values => [importer, insights, ready, wallet]; +} + class Assets { const Assets._(); diff --git a/lib/gen/translations/codegen_loader.g.dart b/lib/gen/translations/codegen_loader.g.dart index 0900c36c..3e2524f5 100644 --- a/lib/gen/translations/codegen_loader.g.dart +++ b/lib/gen/translations/codegen_loader.g.dart @@ -46,6 +46,15 @@ abstract class LocaleKeys { static const parties = 'parties'; static const partyAddParty = 'partyAddParty'; static const partyCreateParty = 'partyCreateParty'; + static const partyEditParty = 'partyEditParty'; + static const partyName = 'partyName'; + static const partyNameRequired = 'partyNameRequired'; + static const partyDescription = 'partyDescription'; + static const partyUpdate = 'partyUpdate'; + static const partyAdd = 'partyAdd'; + static const partyDeleteParty = 'partyDeleteParty'; + static const partyDeletePartyConfirm = 'partyDeletePartyConfirm'; + static const partyNoParties = 'partyNoParties'; static const partyPartyName = 'partyPartyName'; static const partyEnterPartyName = 'partyEnterPartyName'; static const partyPartyDescription = 'partyPartyDescription'; @@ -97,6 +106,18 @@ abstract class LocaleKeys { static const groupCreateGroup = 'groupCreateGroup'; static const statistics = 'statistics'; static const profile = 'profile'; + static const ai = 'ai'; + static const aiChatTitle = 'aiChatTitle'; + static const aiChatEmptyHint = 'aiChatEmptyHint'; + static const aiChatComposerPlaceholder = 'aiChatComposerPlaceholder'; + static const aiChatNewChat = 'aiChatNewChat'; + static const aiChatThinking = 'aiChatThinking'; + static const aiChatError = 'aiChatError'; + static const aiChatHistory = 'aiChatHistory'; + static const aiChatUntitled = 'aiChatUntitled'; + static const aiChatNoSessions = 'aiChatNoSessions'; + static const aiChatNoSessionsHint = 'aiChatNoSessionsHint'; + static const aiChatDeleteConfirm = 'aiChatDeleteConfirm'; static const settings = 'settings'; static const typeHere = 'typeHere'; static const balanceAmountWithCurrency = 'balanceAmountWithCurrency'; @@ -104,6 +125,7 @@ abstract class LocaleKeys { static const selectLanguage = 'selectLanguage'; static const seeAll = 'seeAll'; static const phoneNumber = 'phoneNumber'; + static const phoneNumberHint = 'phoneNumberHint'; static const payment = 'payment'; static const notifications = 'notifications'; static const langEnglish = 'langEnglish'; @@ -139,15 +161,6 @@ abstract class LocaleKeys { static const selectCurrency = 'selectCurrency'; static const defaultWalletName = 'defaultWalletName'; static const defaultWalletDescription = 'defaultWalletDescription'; - static const partyEditParty = 'partyEditParty'; - static const partyName = 'partyName'; - static const partyNameRequired = 'partyNameRequired'; - static const partyDescription = 'partyDescription'; - static const partyUpdate = 'partyUpdate'; - static const partyAdd = 'partyAdd'; - static const partyDeleteParty = 'partyDeleteParty'; - static const partyDeletePartyConfirm = 'partyDeletePartyConfirm'; - static const partyNoParties = 'partyNoParties'; static const groupUpdate = 'groupUpdate'; static const defaultGroupName = 'defaultGroupName'; static const accountInfo = 'accountInfo'; @@ -163,7 +176,6 @@ abstract class LocaleKeys { static const toExcel = 'toExcel'; static const family = 'family'; static const searchHint = 'searchHint'; - static const totalIncome = 'totalIncome'; static const displaySettings = 'displaySettings'; static const transactionFormDisplayMode = 'transactionFormDisplayMode'; static const themeMode = 'themeMode'; @@ -177,6 +189,7 @@ abstract class LocaleKeys { static const anonymous = 'anonymous'; static const logOut = 'logOut'; static const logoutConfirm = 'logoutConfirm'; + static const alreadyHaveAccount = 'alreadyHaveAccount'; static const dontHaveAccount = 'dontHaveAccount'; static const benefitsAccount = 'benefitsAccount'; static const createAccountNow = 'createAccountNow'; @@ -191,6 +204,7 @@ abstract class LocaleKeys { static const about = 'about'; static const switchDefaultGroup = 'switchDefaultGroup'; static const walletTransfer = 'walletTransfer'; + static const transfer = 'transfer'; static const walletTransferDefaultDescription = 'walletTransferDefaultDescription'; static const sourceWallet = 'sourceWallet'; static const orangeMoney = 'orangeMoney'; @@ -213,10 +227,6 @@ abstract class LocaleKeys { static const to = 'to'; static const party = 'party'; static const category = 'category'; - static const description = 'description'; - static const attachment = 'attachment'; - static const date = 'date'; - static const time = 'time'; static const editCategory = 'editCategory'; static const addCategory = 'addCategory'; static const noCategoriesFound = 'noCategoriesFound'; @@ -225,11 +235,14 @@ abstract class LocaleKeys { static const addWallet = 'addWallet'; static const name = 'name'; static const nameIsRequired = 'nameIsRequired'; + static const description = 'description'; + static const attachment = 'attachment'; + static const date = 'date'; + static const time = 'time'; static const createSaving = 'createSaving'; static const pleaseSelectCurrency = 'pleaseSelectCurrency'; static const confirm = 'confirm'; static const categories = 'categories'; - static const edit = 'edit'; static const savings = 'savings'; static const addSaving = 'addSaving'; static const snapPicture = 'snapPicture'; @@ -290,7 +303,6 @@ abstract class LocaleKeys { static const selectCategory = 'selectCategory'; static const categoryIsRequired = 'categoryIsRequired'; static const pleaseSelectWallet = 'pleaseSelectWallet'; - static const phoneNumberHint = 'phoneNumberHint'; static const noWalletsYet = 'noWalletsYet'; static const deleteCategory = 'deleteCategory'; static const deleteCategoryConfirm = 'deleteCategoryConfirm'; @@ -298,6 +310,8 @@ abstract class LocaleKeys { static const transactionsIn = 'transactionsIn'; static const wallets = 'wallets'; static const noData = 'noData'; + static const totalIncome = 'totalIncome'; + static const totalExpense = 'totalExpense'; static const thisMonth = 'thisMonth'; static const thisWeek = 'thisWeek'; static const lastThreeMonths = 'lastThreeMonths'; @@ -312,6 +326,7 @@ abstract class LocaleKeys { static const categoryNameAlreadyExists = 'categoryNameAlreadyExists'; static const deleteWallet = 'deleteWallet'; static const deleteWalletConfirm = 'deleteWalletConfirm'; + static const edit = 'edit'; static const defaultName = 'defaultName'; static const officeElements = 'officeElements'; static const officeElementsDesc = 'officeElementsDesc'; @@ -444,6 +459,15 @@ abstract class LocaleKeys { static const setupWalletTitle = 'setupWalletTitle'; static const setupWalletDesc = 'setupWalletDesc'; static const walletSetup = 'walletSetup'; + static const setupGroupTitle = 'setupGroupTitle'; + static const setupGroupDesc = 'setupGroupDesc'; + static const groupSetup = 'groupSetup'; + static const setupCategoryTitle = 'setupCategoryTitle'; + static const setupCategoryDesc = 'setupCategoryDesc'; + static const createDefaultCategories = 'createDefaultCategories'; + static const createDefaultCategoriesDesc = 'createDefaultCategoriesDesc'; + static const skipCategories = 'skipCategories'; + static const skipCategoriesDesc = 'skipCategoriesDesc'; static const useDefaultWallet = 'useDefaultWallet'; static const renameDefaultWallet = 'renameDefaultWallet'; static const createNewWallet = 'createNewWallet'; @@ -454,17 +478,6 @@ abstract class LocaleKeys { static const createManually = 'createManually'; static const selectFromWalletList = 'selectFromWalletList'; static const selectFromGroupList = 'selectFromGroupList'; - static const groupSetup = 'groupSetup'; - static const setupGroupTitle = 'setupGroupTitle'; - static const setupGroupDesc = 'setupGroupDesc'; - static const setupCategoryTitle = 'setupCategoryTitle'; - static const setupCategoryDesc = 'setupCategoryDesc'; - static const createDefaultCategories = 'createDefaultCategories'; - static const createDefaultCategoriesDesc = 'createDefaultCategoriesDesc'; - static const skipCategories = 'skipCategories'; - static const skipCategoriesDesc = 'skipCategoriesDesc'; - static const alreadyHaveAccount = 'alreadyHaveAccount'; - static const totalExpense = 'totalExpense'; static const advanced = 'advanced'; static const synchronization = 'synchronization'; static const syncHistoryDesc = 'syncHistoryDesc'; @@ -547,9 +560,6 @@ abstract class LocaleKeys { static const files = 'files'; static const noImages = 'noImages'; static const noFiles = 'noFiles'; - static const orphanedMediaCleanupLog = 'orphanedMediaCleanupLog'; - static const orphanedMediaCleanupLogDesc = 'orphanedMediaCleanupLogDesc'; - static const orphanedMediaCleanupLogEmpty = 'orphanedMediaCleanupLogEmpty'; static const appUpdateGooglePlay = 'appUpdateGooglePlay'; static const appUpdateAppStore = 'appUpdateAppStore'; static const appUpdateNewVersionAvailable = 'appUpdateNewVersionAvailable'; @@ -562,6 +572,9 @@ abstract class LocaleKeys { static const appUpdateRestart = 'appUpdateRestart'; static const appUpdateReady = 'appUpdateReady'; static const appUpdateRestartPrompt = 'appUpdateRestartPrompt'; + static const orphanedMediaCleanupLog = 'orphanedMediaCleanupLog'; + static const orphanedMediaCleanupLogDesc = 'orphanedMediaCleanupLogDesc'; + static const orphanedMediaCleanupLogEmpty = 'orphanedMediaCleanupLogEmpty'; static const deleteYourAccount = 'deleteYourAccount'; static const deleteAccount = 'deleteAccount'; static const deleteAccountDesc = 'deleteAccountDesc'; diff --git a/lib/presentation/ai_chat/ai_chat_screen.dart b/lib/presentation/ai_chat/ai_chat_screen.dart new file mode 100644 index 00000000..b97d6417 --- /dev/null +++ b/lib/presentation/ai_chat/ai_chat_screen.dart @@ -0,0 +1,1018 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_message_dto.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/di/injection.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; +import 'package:trakli/gen/assets.gen.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/ai_chat/cubit/ai_chat_cubit.dart'; +import 'package:trakli/presentation/auth/cubits/auth/auth_cubit.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/icon_background_decor.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; + +class AiChatScreen extends StatelessWidget { + const AiChatScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => getIt()..loadMostRecent(), + child: const _AiChatView(), + ); + } +} + +class _AiChatView extends StatefulWidget { + const _AiChatView(); + + @override + State<_AiChatView> createState() => _AiChatViewState(); +} + +class _AiChatViewState extends State<_AiChatView> { + final _composer = TextEditingController(); + final _scroll = ScrollController(); + final _focus = FocusNode(); + + @override + void dispose() { + _composer.dispose(); + _scroll.dispose(); + _focus.dispose(); + super.dispose(); + } + + void _openHistorySheet(BuildContext context) { + final cubit = context.read(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _HistorySheet(cubit: cubit), + ); + } + + void _send([String? overrideText]) { + final text = (overrideText ?? _composer.text).trim(); + if (text.isEmpty) return; + context.read().sendMessage(text); + _composer.clear(); + _focus.unfocus(); + WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom()); + } + + void _scrollToBottom() { + if (!_scroll.hasClients) return; + _scroll.animateTo( + _scroll.position.maxScrollExtent, + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + ); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Scaffold( + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: LocaleKeys.aiChatTitle.tr(), + showBack: false, + actions: [ + PageAppBarAction( + icon: Icons.history_rounded, + tooltip: LocaleKeys.aiChatHistory.tr(), + onTap: () => _openHistorySheet(context), + ), + BlocBuilder( + buildWhen: (a, b) => a.hasSession != b.hasSession, + builder: (context, state) { + if (!state.hasSession) return const SizedBox.shrink(); + return Padding( + padding: EdgeInsets.only(left: 8.w), + child: PageAppBarAction( + icon: Icons.add_comment_outlined, + tooltip: LocaleKeys.aiChatNewChat.tr(), + onTap: () => context.read().startNewChat(), + ), + ); + }, + ), + ], + ), + body: Stack( + children: [ + IconBackgroundDecor(iconPath: Assets.images.sparkles), + BlocConsumer( + listenWhen: (a, b) => + a.messages.length != b.messages.length || + (a.isPolling && !b.isPolling), + listener: (context, state) { + WidgetsBinding.instance + .addPostFrameCallback((_) => _scrollToBottom()); + }, + builder: (context, state) { + if (state.isInitializing) { + return const Center(child: CircularProgressIndicator()); + } + return Column( + children: [ + if (state.failure != null) _ErrorBanner(state: state), + Expanded( + child: state.isEmpty + ? _EmptyState(onPick: _send) + : ListView.builder( + controller: _scroll, + padding: + EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 16.h), + itemCount: state.messages.length, + itemBuilder: (context, i) { + return _MessageRow(message: state.messages[i]); + }, + ), + ), + _Composer( + controller: _composer, + focus: _focus, + isSending: state.isBusy, + onSend: _send, + ), + ], + ); + }, + ), + ], + ), + ); + } +} + +class _ErrorBanner extends StatelessWidget { + final AiChatState state; + const _ErrorBanner({required this.state}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + margin: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 0), + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.h), + decoration: BoxDecoration( + color: tones.expense.background, + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: tones.expense.accent.withAlpha(80)), + ), + child: Row( + children: [ + Icon(Icons.error_outline_rounded, + color: tones.expense.deep, size: 18.sp), + SizedBox(width: 10.w), + Expanded( + child: Text( + LocaleKeys.aiChatError.tr(), + style: TextStyle( + color: tones.expense.deep, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + +class _MessageRow extends StatelessWidget { + final ChatMessageDto message; + const _MessageRow({required this.message}); + + @override + Widget build(BuildContext context) { + final isUser = message.isUser; + return Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + if (!isUser) ...[ + const _AssistantAvatar(), + SizedBox(width: 8.w), + ], + Flexible(child: _Bubble(message: message)), + if (isUser) ...[ + SizedBox(width: 8.w), + const _UserAvatar(), + ], + ], + ), + ); + } +} + +class _Bubble extends StatelessWidget { + final ChatMessageDto message; + const _Bubble({required this.message}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final isUser = message.isUser; + final inFlight = message.isInFlight; + final failed = message.isFailed; + + final radius = BorderRadius.only( + topLeft: Radius.circular(isUser ? 18.r : 6.r), + topRight: Radius.circular(isUser ? 6.r : 18.r), + bottomLeft: Radius.circular(18.r), + bottomRight: Radius.circular(18.r), + ); + + if (isUser) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.h), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [tones.brand.accent, tones.brand.deep], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: radius, + ), + child: SelectableText( + message.displayText, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + height: 1.4, + ), + ), + ); + } + + final bg = failed ? tones.expense.background : tones.bgCard; + final fg = failed ? tones.expense.deep : tones.textPrimary; + + final body = inFlight + ? Padding( + padding: EdgeInsets.symmetric(vertical: 4.h), + child: _ThinkingDots(color: tones.brand.deep.withAlpha(180)), + ) + : SelectableText( + failed + ? (message.error ?? LocaleKeys.aiChatError.tr()) + : message.displayText, + style: TextStyle( + color: fg, + fontSize: 14.sp, + height: 1.45, + ), + ); + + return Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.h), + decoration: BoxDecoration( + color: bg, + borderRadius: radius, + border: Border.all( + color: failed + ? tones.expense.accent.withAlpha(60) + : tones.borderLight, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(8), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: body, + ); + } +} + +class _ThinkingDots extends StatefulWidget { + final Color color; + const _ThinkingDots({required this.color}); + + @override + State<_ThinkingDots> createState() => _ThinkingDotsState(); +} + +class _ThinkingDotsState extends State<_ThinkingDots> + with SingleTickerProviderStateMixin { + late final AnimationController _ctl; + + @override + void initState() { + super.initState(); + _ctl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(); + } + + @override + void dispose() { + _ctl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _ctl, + builder: (context, _) { + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(3, (i) { + final t = (_ctl.value + i * 0.2) % 1.0; + final scale = 0.6 + 0.4 * (1 - (t - 0.5).abs() * 2).clamp(0.0, 1.0); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 2.w), + child: Transform.scale( + scale: scale, + child: Container( + width: 7.r, + height: 7.r, + decoration: BoxDecoration( + color: widget.color, + shape: BoxShape.circle, + ), + ), + ), + ); + }), + ); + }, + ); + } +} + +class _AssistantAvatar extends StatelessWidget { + const _AssistantAvatar(); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + width: 32.r, + height: 32.r, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [tones.brandSoft.background, tones.brand.background], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + border: Border.all(color: tones.brand.deep.withAlpha(30), width: 1), + ), + alignment: Alignment.center, + child: HeroIcon( + HeroIcons.sparkles, + style: HeroIconStyle.outline, + color: tones.brand.deep, + size: 16.sp, + ), + ); + } +} + +class _UserAvatar extends StatelessWidget { + const _UserAvatar(); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final user = context.read().state.user; + final initials = _initialsFor( + first: user?.firstName, + last: user?.lastName, + email: user?.email, + ); + + return Container( + width: 32.r, + height: 32.r, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [tones.brand.accent, tones.brand.deep], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + initials, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w700, + letterSpacing: 0.2, + ), + ), + ); + } +} + +String _initialsFor({String? first, String? last, String? email}) { + String pick(String? s) => + (s != null && s.trim().isNotEmpty) ? s.trim()[0].toUpperCase() : ''; + final a = pick(first); + final b = pick(last); + if (a.isNotEmpty || b.isNotEmpty) return '$a$b'; + final e = pick(email); + return e.isNotEmpty ? e : '·'; +} + +class _EmptyState extends StatelessWidget { + final void Function(String) onPick; + const _EmptyState({required this.onPick}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final suggestions = [ + 'How much did I spend this month?', + 'What was my biggest expense category last week?', + 'Show my income vs expenses for the year.', + 'What was my balance on the 15th?', + ]; + + return ListView( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 32.h), + children: [ + SizedBox(height: 24.h), + Center( + child: Container( + width: 76.r, + height: 76.r, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [tones.brand.accent, tones.brand.deep], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: tones.brand.deep.withAlpha(60), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + padding: EdgeInsets.all(20.r), + child: SvgPicture.asset( + Assets.images.sparkles, + colorFilter: + const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + ), + SizedBox(height: 20.h), + Text( + LocaleKeys.aiChatTitle.tr(), + textAlign: TextAlign.center, + style: TextStyle( + color: tones.textPrimary, + fontSize: 20.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 8.h), + Text( + LocaleKeys.aiChatEmptyHint.tr(), + textAlign: TextAlign.center, + style: TextStyle( + color: tones.textSecondary, + fontSize: 14.sp, + height: 1.5, + ), + ), + SizedBox(height: 28.h), + ...suggestions.map((q) { + return Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: InkWell( + onTap: () => onPick(q), + borderRadius: BorderRadius.circular(14.r), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h), + decoration: BoxDecoration( + color: tones.bgCard, + borderRadius: BorderRadius.circular(14.r), + border: Border.all(color: tones.borderLight), + ), + child: Row( + children: [ + Icon( + Icons.bolt_rounded, + size: 16.sp, + color: tones.brand.deep, + ), + SizedBox(width: 10.w), + Expanded( + child: Text( + q, + style: TextStyle( + color: tones.textPrimary, + fontSize: 13.sp, + fontWeight: FontWeight.w500, + height: 1.4, + ), + ), + ), + Icon( + Icons.arrow_outward_rounded, + size: 14.sp, + color: tones.textMuted, + ), + ], + ), + ), + ), + ); + }), + ], + ); + } +} + +class _Composer extends StatefulWidget { + final TextEditingController controller; + final FocusNode focus; + final bool isSending; + final void Function([String? overrideText]) onSend; + + const _Composer({ + required this.controller, + required this.focus, + required this.isSending, + required this.onSend, + }); + + @override + State<_Composer> createState() => _ComposerState(); +} + +class _ComposerState extends State<_Composer> { + bool _hasText = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onTextChanged); + } + + @override + void dispose() { + widget.controller.removeListener(_onTextChanged); + super.dispose(); + } + + void _onTextChanged() { + final hasText = widget.controller.text.trim().isNotEmpty; + if (hasText != _hasText) setState(() => _hasText = hasText); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final canSend = _hasText && !widget.isSending; + final keyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0; + final fabReserve = keyboardOpen ? 0.0 : 30.r; + + return Padding( + padding: EdgeInsets.fromLTRB(16.w, 6.h, 16.w, 10.h + fabReserve), + child: SafeArea( + top: false, + child: Container( + decoration: BoxDecoration( + color: tones.bgCard, + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: tones.borderLight), + ), + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 4.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 120.h), + child: TextField( + controller: widget.controller, + focusNode: widget.focus, + maxLines: null, + minLines: 1, + textInputAction: TextInputAction.newline, + keyboardType: TextInputType.multiline, + style: TextStyle( + color: tones.textPrimary, + fontSize: 14.sp, + height: 1.4, + ), + decoration: InputDecoration( + border: InputBorder.none, + isCollapsed: true, + contentPadding: EdgeInsets.symmetric(vertical: 12.h), + hintText: LocaleKeys.aiChatComposerPlaceholder.tr(), + hintStyle: TextStyle( + color: tones.textMuted, + fontSize: 14.sp, + ), + ), + ), + ), + ), + SizedBox(width: 8.w), + Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: _SendButton( + canSend: canSend, + isSending: widget.isSending, + onTap: () => widget.onSend(), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _SendButton extends StatelessWidget { + final bool canSend; + final bool isSending; + final VoidCallback onTap; + const _SendButton({ + required this.canSend, + required this.isSending, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final enabled = canSend && !isSending; + return AnimatedContainer( + duration: const Duration(milliseconds: 160), + width: 36.r, + height: 36.r, + decoration: BoxDecoration( + color: enabled ? tones.brand.deep : tones.borderLight, + shape: BoxShape.circle, + ), + child: Material( + color: Colors.transparent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: enabled ? onTap : null, + child: Center( + child: isSending + ? SizedBox( + width: 14.r, + height: 14.r, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Icon( + Icons.arrow_upward_rounded, + color: enabled ? Colors.white : tones.textMuted, + size: 18.sp, + ), + ), + ), + ), + ); + } +} + +class _HistorySheet extends StatefulWidget { + final AiChatCubit cubit; + const _HistorySheet({required this.cubit}); + + @override + State<_HistorySheet> createState() => _HistorySheetState(); +} + +class _HistorySheetState extends State<_HistorySheet> { + late Future> _future; + + @override + void initState() { + super.initState(); + _future = _fetch(); + } + + Future> _fetch() async { + final result = await getIt().listSessions(); + return result.fold((_) => [], (sessions) => sessions); + } + + void _reload() => setState(() => _future = _fetch()); + + Future _confirmDelete(ChatSessionDto session) async { + final confirmed = await showDialog( + context: context, + builder: (ctx) { + final tones = ctx.tones; + return AlertDialog( + backgroundColor: tones.bgCard, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + content: Text( + LocaleKeys.aiChatDeleteConfirm.tr(), + style: TextStyle( + color: tones.textPrimary, + fontSize: 14.sp, + height: 1.4, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: Text( + LocaleKeys.cancel.tr(), + style: TextStyle(color: tones.textSecondary), + ), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: Text( + LocaleKeys.delete.tr(), + style: TextStyle( + color: tones.expense.deep, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ); + }, + ); + if (confirmed != true || !mounted) return; + await widget.cubit.deleteSession(session.id); + _reload(); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.4, + maxChildSize: 0.92, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration( + color: tones.bgPage, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + child: Column( + children: [ + SizedBox(height: 8.h), + Container( + width: 40.w, + height: 4.h, + decoration: BoxDecoration( + color: tones.borderMedium, + borderRadius: BorderRadius.circular(2.r), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 8.w, 12.h), + child: Row( + children: [ + Expanded( + child: Text( + LocaleKeys.aiChatHistory.tr(), + style: TextStyle( + color: tones.textPrimary, + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + ), + ), + IconButton( + icon: Icon(Icons.close_rounded, + color: tones.textSecondary, size: 22.sp), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + Divider(height: 1, color: tones.borderLight), + Expanded( + child: FutureBuilder>( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator()); + } + final sessions = snapshot.data ?? const []; + if (sessions.isEmpty) { + return _HistoryEmpty(); + } + return BlocBuilder( + bloc: widget.cubit, + buildWhen: (a, b) => a.session?.id != b.session?.id, + builder: (context, state) { + return ListView.separated( + controller: scrollController, + padding: EdgeInsets.symmetric(vertical: 8.h), + itemCount: sessions.length, + separatorBuilder: (_, __) => + Divider(height: 1, color: tones.borderLight), + itemBuilder: (context, i) { + final s = sessions[i]; + final isCurrent = state.session?.id == s.id; + return _SessionRow( + session: s, + isCurrent: isCurrent, + onTap: () { + widget.cubit.openSession(s.id); + Navigator.of(context).pop(); + }, + onDelete: () => _confirmDelete(s), + ); + }, + ); + }, + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +class _HistoryEmpty extends StatelessWidget { + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 40.h), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 56.r, + height: 56.r, + decoration: BoxDecoration( + color: tones.brand.background, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: HeroIcon( + HeroIcons.sparkles, + style: HeroIconStyle.outline, + color: tones.brand.deep, + size: 24, + ), + ), + SizedBox(height: 14.h), + Text( + LocaleKeys.aiChatNoSessions.tr(), + style: TextStyle( + color: tones.textPrimary, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 4.h), + Text( + LocaleKeys.aiChatNoSessionsHint.tr(), + textAlign: TextAlign.center, + style: TextStyle( + color: tones.textSecondary, + fontSize: 13.sp, + ), + ), + ], + ), + ); + } +} + +class _SessionRow extends StatelessWidget { + final ChatSessionDto session; + final bool isCurrent; + final VoidCallback onTap; + final VoidCallback onDelete; + + const _SessionRow({ + required this.session, + required this.isCurrent, + required this.onTap, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final title = (session.title?.trim().isNotEmpty ?? false) + ? session.title!.trim() + : LocaleKeys.aiChatUntitled.tr(); + + return InkWell( + onTap: onTap, + child: Container( + color: isCurrent ? tones.brand.background : Colors.transparent, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 36.r, + height: 36.r, + decoration: BoxDecoration( + color: isCurrent ? tones.brand.deep : tones.brand.background, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: HeroIcon( + HeroIcons.sparkles, + style: HeroIconStyle.outline, + color: isCurrent ? Colors.white : tones.brand.deep, + size: 18, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: tones.textPrimary, + fontSize: 14.sp, + fontWeight: + isCurrent ? FontWeight.w700 : FontWeight.w600, + ), + ), + SizedBox(height: 2.h), + Text( + _relativeTime(session.updatedAt), + style: TextStyle( + color: tones.textMuted, + fontSize: 12.sp, + ), + ), + ], + ), + ), + IconButton( + icon: Icon( + Icons.delete_outline_rounded, + color: tones.textMuted, + size: 20.sp, + ), + onPressed: onDelete, + splashRadius: 20.r, + ), + ], + ), + ), + ); + } +} + +String _relativeTime(DateTime when) { + final now = DateTime.now(); + final diff = now.difference(when); + if (diff.inSeconds < 60) return 'now'; + if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; + if (diff.inHours < 24) return '${diff.inHours}h ago'; + if (diff.inDays < 7) return '${diff.inDays}d ago'; + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + return '${months[when.month - 1]} ${when.day}'; +} diff --git a/lib/presentation/ai_chat/cubit/ai_chat_cubit.dart b/lib/presentation/ai_chat/cubit/ai_chat_cubit.dart new file mode 100644 index 00000000..dad8638b --- /dev/null +++ b/lib/presentation/ai_chat/cubit/ai_chat_cubit.dart @@ -0,0 +1,178 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/utils/services/logger.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_message_dto.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +part 'ai_chat_cubit.freezed.dart'; +part 'ai_chat_state.dart'; + +@injectable +class AiChatCubit extends Cubit { + final AiRepository _repo; + + static const Duration _pollInterval = Duration(seconds: 3); + static const Duration _stuckThreshold = Duration(seconds: 90); + + Timer? _pollTimer; + DateTime? _pollStartedAt; + + AiChatCubit(this._repo) : super(AiChatState.initial()); + + Future loadMostRecent() async { + if (state.isInitializing) return; + emit(state.copyWith(isInitializing: true, failure: null)); + + final result = await _repo.listSessions(); + await result.fold( + (failure) async { + emit(state.copyWith(isInitializing: false, failure: failure)); + }, + (sessions) async { + if (sessions.isEmpty) { + emit(state.copyWith(isInitializing: false)); + return; + } + await _loadSession(sessions.first.id); + emit(state.copyWith(isInitializing: false)); + }, + ); + } + + Future _loadSession(int id) async { + final result = await _repo.getSession(id); + result.fold( + (failure) => emit(state.copyWith(failure: failure)), + (session) => emit( + state.copyWith(session: session, messages: session.messages), + ), + ); + } + + Future openSession(int id) async { + _stopPolling(); + emit(state.copyWith(failure: null)); + await _loadSession(id); + _maybeStartPolling(); + } + + Future deleteSession(int id) async { + final result = await _repo.deleteSession(id); + result.fold( + (failure) { + logger.e('AI deleteSession failed (id=$id): $failure'); + emit(state.copyWith(failure: failure)); + }, + (_) { + if (state.session?.id == id) { + _stopPolling(); + emit(AiChatState.initial()); + } + }, + ); + } + + void startNewChat() { + _stopPolling(); + emit(AiChatState.initial()); + } + + Future sendMessage(String text) async { + final trimmed = text.trim(); + if (trimmed.isEmpty || state.isSending) return; + + emit(state.copyWith(isSending: true, failure: null)); + + if (!state.hasSession) { + final result = await _repo.createSession(message: trimmed); + result.fold( + (failure) { + logger.e('AI createSession failed: $failure'); + emit(state.copyWith(isSending: false, failure: failure)); + }, + (session) { + emit(state.copyWith( + isSending: false, + session: session, + messages: session.messages, + )); + _maybeStartPolling(); + }, + ); + return; + } + + final sessionId = state.session!.id; + final result = await _repo.sendMessage( + sessionId: sessionId, + message: trimmed, + ); + result.fold( + (failure) { + logger.e('AI sendMessage failed (session=$sessionId): $failure'); + emit(state.copyWith(isSending: false, failure: failure)); + }, + (pair) { + final updated = [...state.messages, pair.user, pair.assistant]; + emit(state.copyWith(isSending: false, messages: updated)); + _maybeStartPolling(); + }, + ); + } + + void _maybeStartPolling() { + final latest = state.latestAssistantMessage; + if (latest == null || !latest.isInFlight) return; + _stopPolling(); + _pollStartedAt = DateTime.now(); + emit(state.copyWith(isPolling: true)); + _pollTimer = Timer.periodic(_pollInterval, (_) => _tick()); + } + + Future _tick() async { + final session = state.session; + if (session == null) { + _stopPolling(); + return; + } + + final started = _pollStartedAt; + if (started != null && + DateTime.now().difference(started) > _stuckThreshold) { + _stopPolling(); + return; + } + + final result = await _repo.getSession(session.id); + result.fold( + (_) {}, + (fresh) { + emit(state.copyWith(session: fresh, messages: fresh.messages)); + final latest = state.latestAssistantMessage; + if (latest == null || !latest.isInFlight) { + _stopPolling(); + } + }, + ); + } + + void _stopPolling() { + _pollTimer?.cancel(); + _pollTimer = null; + _pollStartedAt = null; + if (state.isPolling) { + emit(state.copyWith(isPolling: false)); + } + } + + @override + Future close() { + _stopPolling(); + return super.close(); + } +} diff --git a/lib/presentation/ai_chat/cubit/ai_chat_cubit.freezed.dart b/lib/presentation/ai_chat/cubit/ai_chat_cubit.freezed.dart new file mode 100644 index 00000000..03a34dc5 --- /dev/null +++ b/lib/presentation/ai_chat/cubit/ai_chat_cubit.freezed.dart @@ -0,0 +1,306 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'ai_chat_cubit.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$AiChatState { + bool get isInitializing => throw _privateConstructorUsedError; + bool get isSending => throw _privateConstructorUsedError; + bool get isPolling => throw _privateConstructorUsedError; + ChatSessionDto? get session => throw _privateConstructorUsedError; + List get messages => throw _privateConstructorUsedError; + Failure? get failure => throw _privateConstructorUsedError; + + /// Create a copy of AiChatState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AiChatStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AiChatStateCopyWith<$Res> { + factory $AiChatStateCopyWith( + AiChatState value, $Res Function(AiChatState) then) = + _$AiChatStateCopyWithImpl<$Res, AiChatState>; + @useResult + $Res call( + {bool isInitializing, + bool isSending, + bool isPolling, + ChatSessionDto? session, + List messages, + Failure? failure}); + + $ChatSessionDtoCopyWith<$Res>? get session; + $FailureCopyWith<$Res>? get failure; +} + +/// @nodoc +class _$AiChatStateCopyWithImpl<$Res, $Val extends AiChatState> + implements $AiChatStateCopyWith<$Res> { + _$AiChatStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AiChatState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInitializing = null, + Object? isSending = null, + Object? isPolling = null, + Object? session = freezed, + Object? messages = null, + Object? failure = freezed, + }) { + return _then(_value.copyWith( + isInitializing: null == isInitializing + ? _value.isInitializing + : isInitializing // ignore: cast_nullable_to_non_nullable + as bool, + isSending: null == isSending + ? _value.isSending + : isSending // ignore: cast_nullable_to_non_nullable + as bool, + isPolling: null == isPolling + ? _value.isPolling + : isPolling // ignore: cast_nullable_to_non_nullable + as bool, + session: freezed == session + ? _value.session + : session // ignore: cast_nullable_to_non_nullable + as ChatSessionDto?, + messages: null == messages + ? _value.messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + failure: freezed == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure?, + ) as $Val); + } + + /// Create a copy of AiChatState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ChatSessionDtoCopyWith<$Res>? get session { + if (_value.session == null) { + return null; + } + + return $ChatSessionDtoCopyWith<$Res>(_value.session!, (value) { + return _then(_value.copyWith(session: value) as $Val); + }); + } + + /// Create a copy of AiChatState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $FailureCopyWith<$Res>? get failure { + if (_value.failure == null) { + return null; + } + + return $FailureCopyWith<$Res>(_value.failure!, (value) { + return _then(_value.copyWith(failure: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AiChatStateImplCopyWith<$Res> + implements $AiChatStateCopyWith<$Res> { + factory _$$AiChatStateImplCopyWith( + _$AiChatStateImpl value, $Res Function(_$AiChatStateImpl) then) = + __$$AiChatStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool isInitializing, + bool isSending, + bool isPolling, + ChatSessionDto? session, + List messages, + Failure? failure}); + + @override + $ChatSessionDtoCopyWith<$Res>? get session; + @override + $FailureCopyWith<$Res>? get failure; +} + +/// @nodoc +class __$$AiChatStateImplCopyWithImpl<$Res> + extends _$AiChatStateCopyWithImpl<$Res, _$AiChatStateImpl> + implements _$$AiChatStateImplCopyWith<$Res> { + __$$AiChatStateImplCopyWithImpl( + _$AiChatStateImpl _value, $Res Function(_$AiChatStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AiChatState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInitializing = null, + Object? isSending = null, + Object? isPolling = null, + Object? session = freezed, + Object? messages = null, + Object? failure = freezed, + }) { + return _then(_$AiChatStateImpl( + isInitializing: null == isInitializing + ? _value.isInitializing + : isInitializing // ignore: cast_nullable_to_non_nullable + as bool, + isSending: null == isSending + ? _value.isSending + : isSending // ignore: cast_nullable_to_non_nullable + as bool, + isPolling: null == isPolling + ? _value.isPolling + : isPolling // ignore: cast_nullable_to_non_nullable + as bool, + session: freezed == session + ? _value.session + : session // ignore: cast_nullable_to_non_nullable + as ChatSessionDto?, + messages: null == messages + ? _value._messages + : messages // ignore: cast_nullable_to_non_nullable + as List, + failure: freezed == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure?, + )); + } +} + +/// @nodoc + +class _$AiChatStateImpl extends _AiChatState { + const _$AiChatStateImpl( + {this.isInitializing = false, + this.isSending = false, + this.isPolling = false, + this.session, + final List messages = const [], + this.failure}) + : _messages = messages, + super._(); + + @override + @JsonKey() + final bool isInitializing; + @override + @JsonKey() + final bool isSending; + @override + @JsonKey() + final bool isPolling; + @override + final ChatSessionDto? session; + final List _messages; + @override + @JsonKey() + List get messages { + if (_messages is EqualUnmodifiableListView) return _messages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_messages); + } + + @override + final Failure? failure; + + @override + String toString() { + return 'AiChatState(isInitializing: $isInitializing, isSending: $isSending, isPolling: $isPolling, session: $session, messages: $messages, failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AiChatStateImpl && + (identical(other.isInitializing, isInitializing) || + other.isInitializing == isInitializing) && + (identical(other.isSending, isSending) || + other.isSending == isSending) && + (identical(other.isPolling, isPolling) || + other.isPolling == isPolling) && + (identical(other.session, session) || other.session == session) && + const DeepCollectionEquality().equals(other._messages, _messages) && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + isInitializing, + isSending, + isPolling, + session, + const DeepCollectionEquality().hash(_messages), + failure); + + /// Create a copy of AiChatState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AiChatStateImplCopyWith<_$AiChatStateImpl> get copyWith => + __$$AiChatStateImplCopyWithImpl<_$AiChatStateImpl>(this, _$identity); +} + +abstract class _AiChatState extends AiChatState { + const factory _AiChatState( + {final bool isInitializing, + final bool isSending, + final bool isPolling, + final ChatSessionDto? session, + final List messages, + final Failure? failure}) = _$AiChatStateImpl; + const _AiChatState._() : super._(); + + @override + bool get isInitializing; + @override + bool get isSending; + @override + bool get isPolling; + @override + ChatSessionDto? get session; + @override + List get messages; + @override + Failure? get failure; + + /// Create a copy of AiChatState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AiChatStateImplCopyWith<_$AiChatStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/ai_chat/cubit/ai_chat_state.dart b/lib/presentation/ai_chat/cubit/ai_chat_state.dart new file mode 100644 index 00000000..a9c92d4a --- /dev/null +++ b/lib/presentation/ai_chat/cubit/ai_chat_state.dart @@ -0,0 +1,28 @@ +part of 'ai_chat_cubit.dart'; + +@freezed +class AiChatState with _$AiChatState { + const factory AiChatState({ + @Default(false) bool isInitializing, + @Default(false) bool isSending, + @Default(false) bool isPolling, + ChatSessionDto? session, + @Default([]) List messages, + Failure? failure, + }) = _AiChatState; + + const AiChatState._(); + + factory AiChatState.initial() => const AiChatState(); + + bool get hasSession => session != null; + bool get isEmpty => messages.isEmpty; + bool get isBusy => isSending || isPolling; + + ChatMessageDto? get latestAssistantMessage { + for (var i = messages.length - 1; i >= 0; i--) { + if (messages[i].isAssistant) return messages[i]; + } + return null; + } +} diff --git a/lib/presentation/root/main_navigation_screen.dart b/lib/presentation/root/main_navigation_screen.dart index 11327bf7..6e432c68 100644 --- a/lib/presentation/root/main_navigation_screen.dart +++ b/lib/presentation/root/main_navigation_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:heroicons/heroicons.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/core/sync/sync_database.dart'; import 'package:trakli/di/injection.dart'; @@ -42,10 +43,11 @@ class _MainNavigationScreenState extends State { child: BlocBuilder( builder: (context, state) { final cubit = context.read(); + final isAiTab = state == MainNavigationPageState.other; return Scaffold( key: scaffoldKey, - resizeToAvoidBottomInset: false, - extendBody: true, + resizeToAvoidBottomInset: isAiTab, + extendBody: !isAiTab, drawer: Drawer( shape: const RoundedRectangleBorder(), width: 0.8.sw, @@ -67,8 +69,8 @@ class _MainNavigationScreenState extends State { floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: SizedBox( - height: 64.r, - width: 64.r, + height: isAiTab ? 44.r : 64.r, + width: isAiTab ? 44.r : 64.r, child: FloatingActionButton( shape: const CircleBorder(), backgroundColor: Theme.of(context).primaryColor, @@ -107,8 +109,14 @@ class _MainNavigationScreenState extends State { text: LocaleKeys.wallet.tr(), ), FABBottomAppBarItem( - iconPath: Assets.images.user, - text: LocaleKeys.profile.tr(), + iconBuilder: (color) => HeroIcon( + HeroIcons.sparkles, + style: HeroIconStyle.outline, + color: color, + size: 24, + ), + text: LocaleKeys.ai.tr(), + selectedIndicatorPath: Assets.images.sparkles, ), ], backgroundColor: Theme.of(context).colorScheme.surface, diff --git a/lib/presentation/utils/bottom_nav.dart b/lib/presentation/utils/bottom_nav.dart index f7941162..12c4e135 100644 --- a/lib/presentation/utils/bottom_nav.dart +++ b/lib/presentation/utils/bottom_nav.dart @@ -5,10 +5,26 @@ import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/presentation/root/bloc/main_navigation_page_cubit.dart'; class FABBottomAppBarItem { - FABBottomAppBarItem({required this.iconPath, required this.text}); + FABBottomAppBarItem({ + this.iconPath, + this.iconBuilder, + required this.text, + this.selectedIndicatorPath, + }) : assert(iconPath != null || iconBuilder != null, + 'Provide either iconPath or iconBuilder'); + + /// SVG path for the tab icon. Ignored when [iconBuilder] is set. + final String? iconPath; + + /// Builds the tab icon for the current tint colour. Use this when the icon + /// is not a single static SVG (e.g. HeroIcons, animated icons). + final Widget Function(Color color)? iconBuilder; - final String iconPath; final String text; + + /// SVG asset shown below the icon when this tab is selected. + /// Defaults to [Assets.images.navEllipse] when null. + final String? selectedIndicatorPath; } class FABBottomAppBar extends StatefulWidget { @@ -93,21 +109,30 @@ class FABBottomAppBarState extends State { child: Stack( children: [ Center( - child: SvgPicture.asset( - item.iconPath, - colorFilter: ColorFilter.mode( - color, - BlendMode.srcIn, - ), + child: SizedBox( + width: widget.iconSize, + height: widget.iconSize, + child: item.iconBuilder != null + ? item.iconBuilder!(color) + : SvgPicture.asset( + item.iconPath!, + colorFilter: ColorFilter.mode( + color, + BlendMode.srcIn, + ), + ), ), ), Align( alignment: Alignment.bottomCenter, child: widget.state == MainNavigationPageState.values[index] ? SvgPicture.asset( - Assets.images.navEllipse, - fit: BoxFit.fill, - height: 6.h, + item.selectedIndicatorPath ?? Assets.images.navEllipse, + fit: BoxFit.contain, + height: item.selectedIndicatorPath != null ? 10.h : 6.h, + colorFilter: item.selectedIndicatorPath != null + ? ColorFilter.mode(color, BlendMode.srcIn) + : null, ) : SizedBox(height: 6.h), ) diff --git a/lib/presentation/utils/enums.dart b/lib/presentation/utils/enums.dart index 2f9ad1cb..2ab1a66d 100644 --- a/lib/presentation/utils/enums.dart +++ b/lib/presentation/utils/enums.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/ai_chat/ai_chat_screen.dart'; import 'package:trakli/presentation/home_screen.dart'; -import 'package:trakli/presentation/profile_screen.dart'; import 'package:trakli/presentation/statistics/statistics_screen.dart'; import 'package:trakli/presentation/wallets/wallet_screen.dart'; @@ -83,7 +83,7 @@ enum NavigationScreen { home, statistics, wallet, - profile; + aiChat; Widget get screen { switch (this) { @@ -93,8 +93,8 @@ enum NavigationScreen { return const StatisticsScreen(); case NavigationScreen.wallet: return const WalletScreen(); - case NavigationScreen.profile: - return const ProfileScreen(); + case NavigationScreen.aiChat: + return const AiChatScreen(); } } } diff --git a/lib/presentation/utils/icon_background_decor.dart b/lib/presentation/utils/icon_background_decor.dart new file mode 100644 index 00000000..c12bc44d --- /dev/null +++ b/lib/presentation/utils/icon_background_decor.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; + +/// Subtle three-motif background decoration: three tinted copies of a single +/// SVG icon scattered across the page at low opacity. Drop this as the first +/// child of a Stack inside a Scaffold body to give a screen a quiet identity +/// without competing with content. +/// +/// Positions, sizes and opacities are tuned for a balanced look across the +/// app; tints fall back to brand and warm-accent unless overridden. +class IconBackgroundDecor extends StatelessWidget { + final String iconPath; + final Color? primaryTint; + final Color? accentTint; + + const IconBackgroundDecor({ + super.key, + required this.iconPath, + this.primaryTint, + this.accentTint, + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final primary = primaryTint ?? tones.brand.deep; + final accent = accentTint ?? tones.accentWarm; + + return Positioned.fill( + child: IgnorePointer( + child: Stack( + children: [ + Positioned( + top: -40.h, + right: -20.w, + child: Opacity( + opacity: 0.10, + child: SvgPicture.asset( + iconPath, + width: 200.w, + colorFilter: ColorFilter.mode(primary, BlendMode.srcIn), + ), + ), + ), + Positioned( + top: 140.h, + left: -10.w, + child: Opacity( + opacity: 0.06, + child: SvgPicture.asset( + iconPath, + width: 120.w, + colorFilter: ColorFilter.mode(accent, BlendMode.srcIn), + ), + ), + ), + Positioned( + bottom: 180.h, + right: 40.w, + child: Opacity( + opacity: 0.08, + child: SvgPicture.asset( + iconPath, + width: 90.w, + colorFilter: ColorFilter.mode(primary, BlendMode.srcIn), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index cf5f72d8..93295228 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" charcode: dependency: transitive description: @@ -535,10 +535,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" url: "https://pub.dev" source: hosted - version: "0.9.5" + version: "0.9.4+4" file_selector_platform_interface: dependency: transitive description: @@ -738,10 +738,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 url: "https://pub.dev" source: hosted - version: "2.0.33" + version: "2.0.31" flutter_riverpod: dependency: "direct main" description: @@ -844,10 +844,10 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 + sha256: b738e35f8bb4957896c34957baf922f99c5d415b38ddc8b070d14b7fa95715d4 url: "https://pub.dev" source: hosted - version: "10.12.0" + version: "10.9.1" fpdart: dependency: "direct main" description: @@ -916,18 +916,18 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: f353140580797e01c1f35748810326f326664c52040b6f62d88e7d6d1cd30917 + sha256: "799165f4c0621ed233bccdded4c2e92739bc1fe73e970163b2f7493b301adad3" url: "https://pub.dev" source: hosted - version: "7.2.9" + version: "7.2.1" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950 + sha256: d9d80f953a244a099a40df1ff6aadc10ee375e6a098bbd5d55be332ce26db18c url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.2.1" google_sign_in_platform_interface: dependency: transitive description: @@ -940,10 +940,10 @@ packages: dependency: transitive description: name: google_sign_in_web - sha256: dac0676af14b96b11691cc3c3e152415a896a38f1224269241d7cc294bdb9102 + sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.0" graphs: dependency: transitive description: @@ -1044,10 +1044,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156 + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" url: "https://pub.dev" source: hosted - version: "0.8.13+14" + version: "0.8.13+1" image_picker_for_web: dependency: transitive description: @@ -1060,10 +1060,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.13" image_picker_linux: dependency: transitive description: @@ -1076,10 +1076,10 @@ packages: dependency: transitive description: name: image_picker_macos - sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 url: "https://pub.dev" source: hosted - version: "0.2.2+1" + version: "0.2.2" image_picker_platform_interface: dependency: transitive description: @@ -1276,10 +1276,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -1380,18 +1380,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" url: "https://pub.dev" source: hosted - version: "2.2.22" + version: "2.2.19" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -1556,26 +1556,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e url: "https://pub.dev" source: hosted - version: "2.4.21" + version: "2.4.13" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1713,10 +1713,10 @@ packages: dependency: transitive description: name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.2" + version: "1.10.1" sqflite: dependency: "direct main" description: @@ -1729,10 +1729,10 @@ packages: dependency: transitive description: name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" url: "https://pub.dev" source: hosted - version: "2.4.2+2" + version: "2.4.1" sqflite_common: dependency: transitive description: @@ -1969,18 +1969,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" url: "https://pub.dev" source: hosted - version: "6.3.28" + version: "6.3.20" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.3.4" url_launcher_linux: dependency: transitive description: @@ -1993,10 +1993,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.3" url_launcher_platform_interface: dependency: transitive description: @@ -2009,10 +2009,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -2049,10 +2049,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.1.19" vector_math: dependency: transitive description: @@ -2073,10 +2073,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.0.0" watcher: dependency: transitive description: @@ -2150,5 +2150,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" From 7ed2911c5eb0ee92e3dfada3582773540ae538c6 Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 22 May 2026 08:15:17 +0100 Subject: [PATCH 12/21] ui(decor): Apply shared icon background decoration to landing pages Parties, wallets, groups and categories now carry the same quiet three-motif background pattern as the AI tab, each scoped to its own representative icon. Gives every landing surface a subtle identity without affecting content or interactions. --- lib/presentation/category/category_screen.dart | 9 ++++++++- lib/presentation/groups/my_groups_screen.dart | 9 ++++++++- lib/presentation/parties/party_screen.dart | 11 ++++++++++- lib/presentation/wallets/wallet_screen.dart | 9 ++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/presentation/category/category_screen.dart b/lib/presentation/category/category_screen.dart index 0f6769c6..2676ccb2 100644 --- a/lib/presentation/category/category_screen.dart +++ b/lib/presentation/category/category_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/domain/entities/category_entity.dart'; +import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/category/add_category_screen.dart'; import 'package:trakli/presentation/category/category_detail_screen.dart'; @@ -13,6 +14,7 @@ import 'package:trakli/presentation/info_interfaces/info_interface.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/icon_background_decor.dart'; import 'package:trakli/presentation/utils/page_app_bar.dart'; class CategoryScreen extends StatefulWidget { @@ -56,7 +58,10 @@ class _CategoryScreenState extends State { ), ], ), - body: BlocBuilder( + body: Stack( + children: [ + IconBackgroundDecor(iconPath: Assets.images.category), + BlocBuilder( builder: (context, state) { if (state.isLoading) { return const Center(child: CircularProgressIndicator()); @@ -101,6 +106,8 @@ class _CategoryScreenState extends State { ); }, ), + ], + ), ); } } diff --git a/lib/presentation/groups/my_groups_screen.dart b/lib/presentation/groups/my_groups_screen.dart index 887dfb2e..fa155981 100644 --- a/lib/presentation/groups/my_groups_screen.dart +++ b/lib/presentation/groups/my_groups_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get_it/get_it.dart'; import 'package:trakli/core/constants/ui_constants.dart'; +import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/groups/add_group_screen.dart'; import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; @@ -11,6 +12,7 @@ import 'package:trakli/presentation/groups/widgets/group_list_tile.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/education_banner.dart'; +import 'package:trakli/presentation/utils/icon_background_decor.dart'; import 'package:trakli/presentation/utils/page_app_bar.dart'; class MyGroupsScreen extends StatefulWidget { @@ -42,7 +44,10 @@ class _MyGroupsScreenState extends State { ), ], ), - body: BlocBuilder( + body: Stack( + children: [ + IconBackgroundDecor(iconPath: Assets.images.people), + BlocBuilder( builder: (context, state) { if (state.isLoading) { return const Center(child: CircularProgressIndicator()); @@ -117,6 +122,8 @@ class _MyGroupsScreenState extends State { ); }, ), + ], + ), ), ); } diff --git a/lib/presentation/parties/party_screen.dart b/lib/presentation/parties/party_screen.dart index f47d7797..843033b1 100644 --- a/lib/presentation/parties/party_screen.dart +++ b/lib/presentation/parties/party_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/domain/entities/party_entity.dart'; +import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/info_interfaces/data.dart'; import 'package:trakli/presentation/info_interfaces/info_interface.dart'; @@ -14,6 +15,7 @@ import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/icon_background_decor.dart'; import 'package:trakli/presentation/utils/page_app_bar.dart'; class PartyScreen extends StatefulWidget { @@ -84,7 +86,12 @@ class _PartyScreenState extends State { ), ], ), - body: partyState.isLoading + body: Stack( + children: [ + IconBackgroundDecor( + iconPath: Assets.images.profile2user, + ), + partyState.isLoading ? const Center(child: CircularProgressIndicator()) : isEmpty ? InfoInterface( @@ -166,6 +173,8 @@ class _PartyScreenState extends State { ), ], ), + ], + ), ); }, ); diff --git a/lib/presentation/wallets/wallet_screen.dart b/lib/presentation/wallets/wallet_screen.dart index a9d5b3a1..80c3ca18 100644 --- a/lib/presentation/wallets/wallet_screen.dart +++ b/lib/presentation/wallets/wallet_screen.dart @@ -6,6 +6,7 @@ import 'package:trakli/core/constants/config_constants.dart'; import 'package:trakli/core/error/failures/failures.dart'; import 'package:trakli/domain/entities/transaction_complete_entity.dart'; import 'package:trakli/domain/entities/wallet_entity.dart'; +import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/config/cubit/config_cubit.dart'; import 'package:trakli/presentation/currency/cubit/currency_cubit.dart'; @@ -15,6 +16,7 @@ import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/icon_background_decor.dart'; import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/wallets/add_wallet_screen.dart'; import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; @@ -125,7 +127,10 @@ class _WalletScreenState extends State { ), ], ), - body: state.isLoading + body: Stack( + children: [ + IconBackgroundDecor(iconPath: Assets.images.wallet), + state.isLoading ? const Center(child: CircularProgressIndicator()) : isEmpty ? InfoInterface( @@ -207,6 +212,8 @@ class _WalletScreenState extends State { ), ], ), + ], + ), ); }, ); From 0b9b44b2b06a14bd0bc92b8e5d38c53a65e22cc2 Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 22 May 2026 08:15:30 +0100 Subject: [PATCH 13/21] ui(home): Add profile avatar entry to home app bar Replace the manual refresh button with a gradient initials avatar that opens the profile screen. Pull-to-refresh on the body continues to sync, so the refresh capability is unchanged. Notifications icon remains. --- lib/presentation/home_screen.dart | 73 ++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/lib/presentation/home_screen.dart b/lib/presentation/home_screen.dart index f395aad0..23ae7b03 100644 --- a/lib/presentation/home_screen.dart +++ b/lib/presentation/home_screen.dart @@ -7,8 +7,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:trakli/core/constants/config_constants.dart'; -import 'package:trakli/core/sync/sync_database.dart'; -import 'package:trakli/di/injection.dart'; import 'package:trakli/domain/entities/group_entity.dart'; import 'package:trakli/domain/entities/transaction_complete_entity.dart'; import 'package:trakli/domain/entities/wallet_entity.dart'; @@ -20,6 +18,7 @@ import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; import 'package:trakli/presentation/history_screen.dart'; import 'package:trakli/presentation/info_interfaces/empty_home_widget.dart'; import 'package:trakli/presentation/notifications/notifications_screen.dart'; +import 'package:trakli/presentation/profile_screen.dart'; import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/all_wallets_tile.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; @@ -238,16 +237,7 @@ class _HomeScreenState extends State { const NotificationsScreen(), ), ), - PageAppBarAction( - icon: Icons.refresh_rounded, - onTap: () { - final isAuthenticated = - context.read().state.isAuthenticated; - if (isAuthenticated) { - getIt().sync(); - } - }, - ), + const _HomeProfileAvatar(), ], ), body: BlocConsumer( @@ -514,3 +504,62 @@ class _HomeScreenState extends State { ); } } + +class _HomeProfileAvatar extends StatelessWidget { + const _HomeProfileAvatar(); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final user = context.watch().state.user; + final initials = _profileInitials( + first: user?.firstName, + last: user?.lastName, + email: user?.email, + ); + + final gradient = LinearGradient( + colors: [tones.brand.accent, tones.brand.deep], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + return Material( + color: Colors.transparent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => AppNavigator.push(context, const ProfileScreen()), + child: Container( + width: 40.r, + height: 40.r, + decoration: BoxDecoration( + gradient: gradient, + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withAlpha(60), width: 1.5), + ), + alignment: Alignment.center, + child: Text( + initials, + style: TextStyle( + color: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + ), + ), + ); + } +} + +String _profileInitials({String? first, String? last, String? email}) { + String pick(String? s) => + (s != null && s.trim().isNotEmpty) ? s.trim()[0].toUpperCase() : ''; + final a = pick(first); + final b = pick(last); + if (a.isNotEmpty || b.isNotEmpty) return '$a$b'; + final e = pick(email); + return e.isNotEmpty ? e : '·'; +} From 10187f0de52bcbca770610ee4e5f5f6a238ccb36 Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 22 May 2026 08:16:06 +0100 Subject: [PATCH 14/21] feat(transactions): Inline transfer as third segment in add flow Wallet-to-wallet transfers were buried behind a per-wallet overflow menu. Surface them alongside expense and income on the add-transaction screen so the action is discoverable from the primary entry point. The transfer form renders inline (no extra navigation) and saves through the existing flow. The original per-wallet entry remains. --- lib/presentation/add_transaction_screen.dart | 38 +++++++-- .../transfers/wallet_transfer_screen.dart | 81 ++++++++++++------- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/lib/presentation/add_transaction_screen.dart b/lib/presentation/add_transaction_screen.dart index 630a9ab0..e71000f2 100644 --- a/lib/presentation/add_transaction_screen.dart +++ b/lib/presentation/add_transaction_screen.dart @@ -9,6 +9,7 @@ import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/page_app_bar.dart'; import 'package:trakli/presentation/utils/enums.dart'; import 'package:trakli/presentation/utils/forms/add_transaction_form.dart'; +import 'package:trakli/presentation/transfers/wallet_transfer_screen.dart'; import 'package:trakli/providers/local_storage.dart'; class AddTransactionScreen extends StatefulWidget { @@ -41,7 +42,7 @@ class _AddTransactionScreenState extends State }); }); - final controlLenght = widget.transaction != null ? 1 : 2; + final controlLenght = widget.transaction != null ? 1 : 3; tabController = TabController(length: controlLenght, vsync: this); if (widget.transaction != null) { @@ -132,6 +133,11 @@ class _AddTransactionScreenState extends State widget.transaction, ), ), + if (widget.transaction == null) + const KeyedSubtree( + key: ValueKey('tx-form-transfer'), + child: WalletTransferScreen(embedded: true), + ), ]; return IndexedStack( index: tabController.index.clamp( @@ -191,6 +197,17 @@ class _TypeSegmented extends StatelessWidget { onTap: () => controller.animateTo(1), ), ), + if (controller.length > 2) + Expanded( + child: _SegmentItem( + label: LocaleKeys.transfer.tr(), + icon: Icons.swap_horiz_rounded, + active: controller.index == 2, + activeBg: tones.brand.background, + activeFg: tones.brand.deep, + onTap: () => controller.animateTo(2), + ), + ), ], ), ); @@ -232,6 +249,7 @@ class _SegmentItem extends StatelessWidget { ), alignment: Alignment.center, child: Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( @@ -240,13 +258,17 @@ class _SegmentItem extends StatelessWidget { color: active ? activeFg : tones.textMuted, ), SizedBox(width: 6.w), - Text( - label, - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w700, - color: active ? activeFg : tones.textSecondary, - letterSpacing: -0.1, + Flexible( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, + color: active ? activeFg : tones.textSecondary, + letterSpacing: -0.1, + ), ), ), ], diff --git a/lib/presentation/transfers/wallet_transfer_screen.dart b/lib/presentation/transfers/wallet_transfer_screen.dart index 34988fdc..e0111e07 100644 --- a/lib/presentation/transfers/wallet_transfer_screen.dart +++ b/lib/presentation/transfers/wallet_transfer_screen.dart @@ -27,10 +27,19 @@ class WalletTransferScreen extends StatefulWidget { const WalletTransferScreen({ super.key, this.initialFromWalletClientId, + this.embedded = false, + this.onSavedSuccessfully, }); final String? initialFromWalletClientId; + /// When true, omits the Scaffold + AppBar so this can be embedded inside + /// another screen (e.g. as a tab inside [AddTransactionScreen]). + final bool embedded; + + /// Called after a successful save. Defaults to `Navigator.pop()` when null. + final VoidCallback? onSavedSuccessfully; + @override State createState() => _WalletTransferScreenState(); } @@ -547,35 +556,7 @@ class _WalletTransferScreenState extends State { @override Widget build(BuildContext context) { - return BlocListener( - listenWhen: (previous, current) => - previous.isSaving != current.isSaving || - previous.saveSuccess != current.saveSuccess || - previous.failure != current.failure, - listener: (context, state) { - if (state.failure.hasError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(LocaleKeys.transferFailedWithMessage - .tr(namedArgs: {'message': state.failure.customMessage})), - backgroundColor: Colors.red, - ), - ); - context.read().resetSaveState(); - } else if (state.saveSuccess) { - context.read().resetSaveState(); - Navigator.of(context).pop(); - } - }, - child: Scaffold( - appBar: CustomAppBar( - backgroundColor: Theme.of(context).primaryColor, - leading: const CustomBackButton(), - titleText: LocaleKeys.walletTransfer.tr(), - headerTextColor: const Color(0xFFEBEDEC), - actions: [SizedBox(width: 16.w)], - ), - body: BlocBuilder( + final body = BlocBuilder( builder: (context, transferState) { final minTransferAmount = context .read() @@ -856,8 +837,48 @@ class _WalletTransferScreenState extends State { ), ); }, - ), + ); + + final listener = BlocListener( + listenWhen: (previous, current) => + previous.isSaving != current.isSaving || + previous.saveSuccess != current.saveSuccess || + previous.failure != current.failure, + listener: (context, state) { + if (state.failure.hasError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(LocaleKeys.transferFailedWithMessage + .tr(namedArgs: {'message': state.failure.customMessage})), + backgroundColor: Colors.red, + ), + ); + context.read().resetSaveState(); + } else if (state.saveSuccess) { + context.read().resetSaveState(); + if (widget.onSavedSuccessfully != null) { + widget.onSavedSuccessfully!(); + } else { + Navigator.of(context).pop(); + } + } + }, + child: body, + ); + + if (widget.embedded) { + return listener; + } + + return Scaffold( + appBar: CustomAppBar( + backgroundColor: Theme.of(context).primaryColor, + leading: const CustomBackButton(), + titleText: LocaleKeys.walletTransfer.tr(), + headerTextColor: const Color(0xFFEBEDEC), + actions: [SizedBox(width: 16.w)], ), + body: listener, ); } } From efd7db4ea49feb1e0f77ec103231448e9e8b9f30 Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 22 May 2026 08:16:54 +0100 Subject: [PATCH 15/21] fix(theme): Restore visible input borders in dark mode Dark mode left input borders transparent by default, so text fields and selectors had no visible outline at rest while the focused state showed a brand-colored ring. Apply a subtle visible border colour for the unfocused state so every field reads as bounded in dark mode while staying quiet in light. --- lib/presentation/utils/theme.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/utils/theme.dart b/lib/presentation/utils/theme.dart index 3fcee283..e262d2b0 100644 --- a/lib/presentation/utils/theme.dart +++ b/lib/presentation/utils/theme.dart @@ -299,7 +299,7 @@ final darkTheme = ThemeData( labelStyle: const TextStyle(color: Colors.white70), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), - borderSide: const BorderSide(color: Colors.transparent), + borderSide: BorderSide(color: neutralN500), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), @@ -307,7 +307,7 @@ final darkTheme = ThemeData( ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), - borderSide: const BorderSide(color: Colors.transparent), + borderSide: BorderSide(color: neutralN500), ), floatingLabelStyle: TextStyle(color: appPrimaryColor), ), From cd32c0702079937a9cdd2646bf7ff7847f390e0d Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 22 May 2026 08:17:04 +0100 Subject: [PATCH 16/21] fix(profile): Show back button on profile screen Profile was opened as a pushed route but explicitly hid its back button, leaving the only way back as the system gesture. Restore the default back affordance. --- lib/presentation/profile_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/presentation/profile_screen.dart b/lib/presentation/profile_screen.dart index 86820616..c5c0b665 100644 --- a/lib/presentation/profile_screen.dart +++ b/lib/presentation/profile_screen.dart @@ -90,7 +90,6 @@ class ProfileScreen extends StatelessWidget { backgroundColor: tones.bgPage, appBar: PageAppBar( title: LocaleKeys.profile.tr(), - showBack: false, ), body: SingleChildScrollView( padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 24.h), From 1b987568eb5720223bd0b87a5281b45cfe4b3a2f Mon Sep 17 00:00:00 2001 From: nfebe Date: Fri, 22 May 2026 09:49:17 +0100 Subject: [PATCH 17/21] feat(budgets): Add budget tracking with periods, targets, and progress Budgets can now be created and managed on mobile, matching the web feature. Each budget has a period (weekly, monthly, yearly, or custom), an optional target scope across categories, wallets, or groups, threshold and forecast alerts, and rollover support. The detail screen fetches live progress for the current period and shows past closed-period snapshots once they sync. Open it from the new Budgets entry in the sidebar. --- lib/core/module/sync_module.dart | 3 + lib/data/database/app_database.dart | 22 +- lib/data/database/app_database.g.dart | 8370 +++++++++++------ .../database/tables/budget_period_states.dart | 14 + lib/data/database/tables/budgetables.dart | 14 + lib/data/database/tables/budgets.dart | 24 + .../budget/budget_local_datasource.dart | 342 + .../budget/budget_remote_datasource.dart | 191 + .../budget/dtos/budget_complete_dto.dart | 67 + .../budget/dtos/budget_period_state_dto.dart | 35 + .../dtos/budget_period_state_dto.freezed.dart | 501 + .../dtos/budget_period_state_dto.g.dart | 43 + .../budget/dtos/budget_progress_dto.dart | 30 + .../dtos/budget_progress_dto.freezed.dart | 493 + .../budget/dtos/budget_progress_dto.g.dart | 52 + .../budget/dtos/budget_target_dto.dart | 18 + .../dtos/budget_target_dto.freezed.dart | 233 + .../budget/dtos/budget_target_dto.g.dart | 31 + .../dtos/budget_transactions_response.dart | 30 + lib/data/mappers/budget_mapper.dart | 91 + .../repositories/budget_repository_impl.dart | 312 + lib/data/sync/budget_sync_handler.dart | 223 + lib/di/injection.config.dart | 48 +- lib/domain/entities/budget_entity.dart | 34 + .../entities/budget_entity.freezed.dart | 637 ++ .../entities/budget_period_state_entity.dart | 19 + .../budget_period_state_entity.freezed.dart | 352 + .../entities/budget_progress_entity.dart | 24 + .../budget_progress_entity.freezed.dart | 436 + lib/domain/entities/budget_target_entity.dart | 14 + .../budget_target_entity.freezed.dart | 198 + .../repositories/budget_repository.dart | 66 + lib/presentation/app_widget.dart | 4 + .../budget/add_budget_screen.dart | 722 ++ .../budget/budget_detail_screen.dart | 581 ++ lib/presentation/budget/budget_screen.dart | 382 + .../budget/cubit/budget_cubit.dart | 243 + .../budget/cubit/budget_cubit.freezed.dart | 455 + .../budget/cubit/budget_state.dart | 32 + lib/presentation/utils/custom_drawer.dart | 36 + lib/presentation/utils/enums.dart | 86 + 41 files changed, 12810 insertions(+), 2698 deletions(-) create mode 100644 lib/data/database/tables/budget_period_states.dart create mode 100644 lib/data/database/tables/budgetables.dart create mode 100644 lib/data/database/tables/budgets.dart create mode 100644 lib/data/datasources/budget/budget_local_datasource.dart create mode 100644 lib/data/datasources/budget/budget_remote_datasource.dart create mode 100644 lib/data/datasources/budget/dtos/budget_complete_dto.dart create mode 100644 lib/data/datasources/budget/dtos/budget_period_state_dto.dart create mode 100644 lib/data/datasources/budget/dtos/budget_period_state_dto.freezed.dart create mode 100644 lib/data/datasources/budget/dtos/budget_period_state_dto.g.dart create mode 100644 lib/data/datasources/budget/dtos/budget_progress_dto.dart create mode 100644 lib/data/datasources/budget/dtos/budget_progress_dto.freezed.dart create mode 100644 lib/data/datasources/budget/dtos/budget_progress_dto.g.dart create mode 100644 lib/data/datasources/budget/dtos/budget_target_dto.dart create mode 100644 lib/data/datasources/budget/dtos/budget_target_dto.freezed.dart create mode 100644 lib/data/datasources/budget/dtos/budget_target_dto.g.dart create mode 100644 lib/data/datasources/budget/dtos/budget_transactions_response.dart create mode 100644 lib/data/mappers/budget_mapper.dart create mode 100644 lib/data/repositories/budget_repository_impl.dart create mode 100644 lib/data/sync/budget_sync_handler.dart create mode 100644 lib/domain/entities/budget_entity.dart create mode 100644 lib/domain/entities/budget_entity.freezed.dart create mode 100644 lib/domain/entities/budget_period_state_entity.dart create mode 100644 lib/domain/entities/budget_period_state_entity.freezed.dart create mode 100644 lib/domain/entities/budget_progress_entity.dart create mode 100644 lib/domain/entities/budget_progress_entity.freezed.dart create mode 100644 lib/domain/entities/budget_target_entity.dart create mode 100644 lib/domain/entities/budget_target_entity.freezed.dart create mode 100644 lib/domain/repositories/budget_repository.dart create mode 100644 lib/presentation/budget/add_budget_screen.dart create mode 100644 lib/presentation/budget/budget_detail_screen.dart create mode 100644 lib/presentation/budget/budget_screen.dart create mode 100644 lib/presentation/budget/cubit/budget_cubit.dart create mode 100644 lib/presentation/budget/cubit/budget_cubit.freezed.dart create mode 100644 lib/presentation/budget/cubit/budget_state.dart diff --git a/lib/core/module/sync_module.dart b/lib/core/module/sync_module.dart index 14b7b764..a6f16888 100644 --- a/lib/core/module/sync_module.dart +++ b/lib/core/module/sync_module.dart @@ -1,6 +1,7 @@ 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_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 +25,7 @@ abstract class SyncModule { TransactionSyncHandler transactionTypeHandler, TransferSyncHandler transferTypeHandler, MediaSyncHandler mediaSyncHandler, + BudgetSyncHandler budgetTypeHandler, ) { return { categoryTypeHandler, @@ -35,6 +37,7 @@ abstract class SyncModule { transactionTypeHandler, transferTypeHandler, mediaSyncHandler, + budgetTypeHandler, }; } diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index 7d2021f4..dbc95b2f 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -28,6 +28,9 @@ 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 'package:trakli/data/database/tables/budgets.dart'; +import 'package:trakli/data/database/tables/budgetables.dart'; +import 'package:trakli/data/database/tables/budget_period_states.dart'; import 'app_database.steps.dart'; part 'app_database.g.dart'; @@ -47,6 +50,9 @@ part 'app_database.g.dart'; Notifications, MediaFiles, Transfers, + Budgets, + Budgetables, + BudgetPeriodStates, ]) class AppDatabase extends _$AppDatabase with SynchronizerDb { final Set typeHandlers; @@ -58,7 +64,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 +72,16 @@ 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); + } + if (from < 5 && to >= 5) { + await m.createTable(budgets); + await m.createTable(budgetables); + await m.createTable(budgetPeriodStates); + } + }, ); } @@ -270,6 +285,9 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb { await notifications.deleteAll(); await mediaFiles.deleteAll(); await transfers.deleteAll(); + await budgetables.deleteAll(); + await budgetPeriodStates.deleteAll(); + await budgets.deleteAll(); } } diff --git a/lib/data/database/app_database.g.dart b/lib/data/database/app_database.g.dart index 413c8d85..be453b9b 100644 --- a/lib/data/database/app_database.g.dart +++ b/lib/data/database/app_database.g.dart @@ -6488,47 +6488,2536 @@ 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 GeneratedColumn amount = GeneratedColumn( + 'amount', aliasedName, false, + type: DriftSqlType.double, requiredDuringInsert: true); + @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 + 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 ]; @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: attachedDatabase.typeMapping + .read(DriftSqlType.double, 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']), + ); + } + + @override + $BudgetsTable createAlias(String alias) { + return $BudgetsTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converterperiodType = + const EnumNameConverter(BudgetPeriodType.values); +} + +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; + 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}); + @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(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); + } + 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), + ); + } + + 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: serializer.fromJson(json['amount']), + currency: serializer.fromJson(json['currency']), + periodType: $BudgetsTable.$converterperiodType + .fromJson(serializer.fromJson(json['periodType'])), + startDate: serializer.fromJson(json['startDate']), + endDate: serializer.fromJson(json['endDate']), + rolloverEnabled: serializer.fromJson(json['rolloverEnabled']), + thresholdPercent: serializer.fromJson(json['thresholdPercent']), + forecastAlertsEnabled: + serializer.fromJson(json['forecastAlertsEnabled']), + isActive: serializer.fromJson(json['isActive']), + ownerType: serializer.fromJson(json['ownerType']), + ownerId: serializer.fromJson(json['ownerId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'user_id': serializer.toJson(userId), + 'client_generated_id': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'created_at': serializer.toJson(createdAt), + 'updated_at': serializer.toJson(updatedAt), + 'deleted_at': serializer.toJson(deletedAt), + 'last_synced_at': serializer.toJson(lastSyncedAt), + 'name': serializer.toJson(name), + 'slug': serializer.toJson(slug), + 'description': serializer.toJson(description), + 'amount': serializer.toJson(amount), + 'currency': serializer.toJson(currency), + 'periodType': serializer.toJson( + $BudgetsTable.$converterperiodType.toJson(periodType)), + 'startDate': serializer.toJson(startDate), + 'endDate': serializer.toJson(endDate), + 'rolloverEnabled': serializer.toJson(rolloverEnabled), + 'thresholdPercent': serializer.toJson(thresholdPercent), + 'forecastAlertsEnabled': serializer.toJson(forecastAlertsEnabled), + 'isActive': serializer.toJson(isActive), + 'ownerType': serializer.toJson(ownerType), + 'ownerId': serializer.toJson(ownerId), + }; + } + + Budget copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? name, + String? slug, + Value description = const Value.absent(), + double? amount, + String? currency, + BudgetPeriodType? periodType, + DateTime? startDate, + Value endDate = const Value.absent(), + bool? rolloverEnabled, + int? thresholdPercent, + bool? forecastAlertsEnabled, + bool? isActive, + String? ownerType, + Value ownerId = const Value.absent()}) => + Budget( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + name: name ?? this.name, + slug: slug ?? this.slug, + description: description.present ? description.value : this.description, + amount: amount ?? this.amount, + currency: currency ?? this.currency, + periodType: periodType ?? this.periodType, + startDate: startDate ?? this.startDate, + endDate: endDate.present ? endDate.value : this.endDate, + rolloverEnabled: rolloverEnabled ?? this.rolloverEnabled, + thresholdPercent: thresholdPercent ?? this.thresholdPercent, + forecastAlertsEnabled: + forecastAlertsEnabled ?? this.forecastAlertsEnabled, + isActive: isActive ?? this.isActive, + ownerType: ownerType ?? this.ownerType, + ownerId: ownerId.present ? ownerId.value : this.ownerId, + ); + Budget copyWithCompanion(BudgetsCompanion data) { + return Budget( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + name: data.name.present ? data.name.value : this.name, + slug: data.slug.present ? data.slug.value : this.slug, + description: + data.description.present ? data.description.value : this.description, + amount: data.amount.present ? data.amount.value : this.amount, + currency: data.currency.present ? data.currency.value : this.currency, + periodType: + data.periodType.present ? data.periodType.value : this.periodType, + startDate: data.startDate.present ? data.startDate.value : this.startDate, + endDate: data.endDate.present ? data.endDate.value : this.endDate, + rolloverEnabled: data.rolloverEnabled.present + ? data.rolloverEnabled.value + : this.rolloverEnabled, + thresholdPercent: data.thresholdPercent.present + ? data.thresholdPercent.value + : this.thresholdPercent, + forecastAlertsEnabled: data.forecastAlertsEnabled.present + ? data.forecastAlertsEnabled.value + : this.forecastAlertsEnabled, + isActive: data.isActive.present ? data.isActive.value : this.isActive, + ownerType: data.ownerType.present ? data.ownerType.value : this.ownerType, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + ); + } + + @override + String toString() { + return (StringBuffer('Budget(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('slug: $slug, ') + ..write('description: $description, ') + ..write('amount: $amount, ') + ..write('currency: $currency, ') + ..write('periodType: $periodType, ') + ..write('startDate: $startDate, ') + ..write('endDate: $endDate, ') + ..write('rolloverEnabled: $rolloverEnabled, ') + ..write('thresholdPercent: $thresholdPercent, ') + ..write('forecastAlertsEnabled: $forecastAlertsEnabled, ') + ..write('isActive: $isActive, ') + ..write('ownerType: $ownerType, ') + ..write('ownerId: $ownerId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + name, + slug, + description, + amount, + currency, + periodType, + startDate, + endDate, + rolloverEnabled, + thresholdPercent, + forecastAlertsEnabled, + isActive, + ownerType, + ownerId + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Budget && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.name == this.name && + other.slug == this.slug && + other.description == this.description && + other.amount == this.amount && + other.currency == this.currency && + other.periodType == this.periodType && + other.startDate == this.startDate && + other.endDate == this.endDate && + other.rolloverEnabled == this.rolloverEnabled && + other.thresholdPercent == this.thresholdPercent && + other.forecastAlertsEnabled == this.forecastAlertsEnabled && + other.isActive == this.isActive && + other.ownerType == this.ownerType && + other.ownerId == this.ownerId); +} + +class BudgetsCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value name; + final Value slug; + final Value description; + final Value amount; + final Value currency; + final Value periodType; + final Value startDate; + final Value endDate; + final Value rolloverEnabled; + final Value thresholdPercent; + final Value forecastAlertsEnabled; + final Value isActive; + final Value ownerType; + final Value ownerId; + final Value rowid; + const BudgetsCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.name = const Value.absent(), + this.slug = const Value.absent(), + this.description = const Value.absent(), + this.amount = const Value.absent(), + this.currency = const Value.absent(), + this.periodType = const Value.absent(), + this.startDate = const Value.absent(), + this.endDate = const Value.absent(), + this.rolloverEnabled = const Value.absent(), + this.thresholdPercent = const Value.absent(), + this.forecastAlertsEnabled = const Value.absent(), + this.isActive = const Value.absent(), + this.ownerType = const Value.absent(), + this.ownerId = const Value.absent(), + this.rowid = const Value.absent(), + }); + BudgetsCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String name, + required String slug, + this.description = const Value.absent(), + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + this.endDate = const Value.absent(), + this.rolloverEnabled = const Value.absent(), + this.thresholdPercent = const Value.absent(), + this.forecastAlertsEnabled = const Value.absent(), + this.isActive = const Value.absent(), + this.ownerType = const Value.absent(), + this.ownerId = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name), + slug = Value(slug), + amount = Value(amount), + currency = Value(currency), + periodType = Value(periodType), + startDate = Value(startDate); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? name, + Expression? slug, + Expression? description, + Expression? amount, + Expression? currency, + Expression? periodType, + Expression? startDate, + Expression? endDate, + Expression? rolloverEnabled, + Expression? thresholdPercent, + Expression? forecastAlertsEnabled, + Expression? isActive, + Expression? ownerType, + Expression? ownerId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (name != null) 'name': name, + if (slug != null) 'slug': slug, + if (description != null) 'description': description, + if (amount != null) 'amount': amount, + if (currency != null) 'currency': currency, + if (periodType != null) 'period_type': periodType, + if (startDate != null) 'start_date': startDate, + if (endDate != null) 'end_date': endDate, + if (rolloverEnabled != null) 'rollover_enabled': rolloverEnabled, + if (thresholdPercent != null) 'threshold_percent': thresholdPercent, + if (forecastAlertsEnabled != null) + 'forecast_alerts_enabled': forecastAlertsEnabled, + if (isActive != null) 'is_active': isActive, + if (ownerType != null) 'owner_type': ownerType, + if (ownerId != null) 'owner_id': ownerId, + if (rowid != null) 'rowid': rowid, + }); + } + + BudgetsCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? name, + Value? slug, + Value? description, + Value? amount, + Value? currency, + Value? periodType, + Value? startDate, + Value? endDate, + Value? rolloverEnabled, + Value? thresholdPercent, + Value? forecastAlertsEnabled, + Value? isActive, + Value? ownerType, + Value? ownerId, + Value? rowid}) { + return BudgetsCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + name: name ?? this.name, + slug: slug ?? this.slug, + description: description ?? this.description, + amount: amount ?? this.amount, + currency: currency ?? this.currency, + periodType: periodType ?? this.periodType, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + rolloverEnabled: rolloverEnabled ?? this.rolloverEnabled, + thresholdPercent: thresholdPercent ?? this.thresholdPercent, + forecastAlertsEnabled: + forecastAlertsEnabled ?? this.forecastAlertsEnabled, + isActive: isActive ?? this.isActive, + ownerType: ownerType ?? this.ownerType, + ownerId: ownerId ?? this.ownerId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (slug.present) { + map['slug'] = Variable(slug.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (amount.present) { + map['amount'] = Variable(amount.value); + } + if (currency.present) { + map['currency'] = Variable(currency.value); + } + if (periodType.present) { + map['period_type'] = Variable( + $BudgetsTable.$converterperiodType.toSql(periodType.value)); + } + if (startDate.present) { + map['start_date'] = Variable(startDate.value); + } + if (endDate.present) { + map['end_date'] = Variable(endDate.value); + } + if (rolloverEnabled.present) { + map['rollover_enabled'] = Variable(rolloverEnabled.value); + } + if (thresholdPercent.present) { + map['threshold_percent'] = Variable(thresholdPercent.value); + } + if (forecastAlertsEnabled.present) { + map['forecast_alerts_enabled'] = + Variable(forecastAlertsEnabled.value); + } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } + if (ownerType.present) { + map['owner_type'] = Variable(ownerType.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BudgetsCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('slug: $slug, ') + ..write('description: $description, ') + ..write('amount: $amount, ') + ..write('currency: $currency, ') + ..write('periodType: $periodType, ') + ..write('startDate: $startDate, ') + ..write('endDate: $endDate, ') + ..write('rolloverEnabled: $rolloverEnabled, ') + ..write('thresholdPercent: $thresholdPercent, ') + ..write('forecastAlertsEnabled: $forecastAlertsEnabled, ') + ..write('isActive: $isActive, ') + ..write('ownerType: $ownerType, ') + ..write('ownerId: $ownerId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $BudgetablesTable extends Budgetables + with TableInfo<$BudgetablesTable, Budgetable> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $BudgetablesTable(this.attachedDatabase, [this._alias]); + @override + late final GeneratedColumn budgetClientId = GeneratedColumn( + 'budget_client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES budgets (client_id)')); + @override + late final GeneratedColumnWithTypeConverter + targetType = GeneratedColumn('target_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $BudgetablesTable.$convertertargetType); + @override + late final GeneratedColumn targetClientId = GeneratedColumn( + 'target_client_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => + [budgetClientId, targetType, targetClientId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'budgetables'; + @override + Set get $primaryKey => + {budgetClientId, targetType, targetClientId}; + @override + Budgetable map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Budgetable( + budgetClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}budget_client_id'])!, + targetType: $BudgetablesTable.$convertertargetType.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}target_type'])!), + targetClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}target_client_id'])!, + ); + } + + @override + $BudgetablesTable createAlias(String alias) { + return $BudgetablesTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $convertertargetType = + const EnumNameConverter(BudgetTargetType.values); +} + +class Budgetable extends DataClass implements Insertable { + final String budgetClientId; + final BudgetTargetType targetType; + final String targetClientId; + const Budgetable( + {required this.budgetClientId, + required this.targetType, + required this.targetClientId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['budget_client_id'] = Variable(budgetClientId); + { + map['target_type'] = Variable( + $BudgetablesTable.$convertertargetType.toSql(targetType)); + } + map['target_client_id'] = Variable(targetClientId); + return map; + } + + BudgetablesCompanion toCompanion(bool nullToAbsent) { + return BudgetablesCompanion( + budgetClientId: Value(budgetClientId), + targetType: Value(targetType), + targetClientId: Value(targetClientId), + ); + } + + factory Budgetable.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Budgetable( + budgetClientId: serializer.fromJson(json['budgetClientId']), + targetType: $BudgetablesTable.$convertertargetType + .fromJson(serializer.fromJson(json['targetType'])), + targetClientId: serializer.fromJson(json['targetClientId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'budgetClientId': serializer.toJson(budgetClientId), + 'targetType': serializer.toJson( + $BudgetablesTable.$convertertargetType.toJson(targetType)), + 'targetClientId': serializer.toJson(targetClientId), + }; + } + + Budgetable copyWith( + {String? budgetClientId, + BudgetTargetType? targetType, + String? targetClientId}) => + Budgetable( + budgetClientId: budgetClientId ?? this.budgetClientId, + targetType: targetType ?? this.targetType, + targetClientId: targetClientId ?? this.targetClientId, + ); + Budgetable copyWithCompanion(BudgetablesCompanion data) { + return Budgetable( + budgetClientId: data.budgetClientId.present + ? data.budgetClientId.value + : this.budgetClientId, + targetType: + data.targetType.present ? data.targetType.value : this.targetType, + targetClientId: data.targetClientId.present + ? data.targetClientId.value + : this.targetClientId, + ); + } + + @override + String toString() { + return (StringBuffer('Budgetable(') + ..write('budgetClientId: $budgetClientId, ') + ..write('targetType: $targetType, ') + ..write('targetClientId: $targetClientId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(budgetClientId, targetType, targetClientId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Budgetable && + other.budgetClientId == this.budgetClientId && + other.targetType == this.targetType && + other.targetClientId == this.targetClientId); +} + +class BudgetablesCompanion extends UpdateCompanion { + final Value budgetClientId; + final Value targetType; + final Value targetClientId; + final Value rowid; + const BudgetablesCompanion({ + this.budgetClientId = const Value.absent(), + this.targetType = const Value.absent(), + this.targetClientId = const Value.absent(), + this.rowid = const Value.absent(), + }); + BudgetablesCompanion.insert({ + required String budgetClientId, + required BudgetTargetType targetType, + required String targetClientId, + this.rowid = const Value.absent(), + }) : budgetClientId = Value(budgetClientId), + targetType = Value(targetType), + targetClientId = Value(targetClientId); + static Insertable custom({ + Expression? budgetClientId, + Expression? targetType, + Expression? targetClientId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (budgetClientId != null) 'budget_client_id': budgetClientId, + if (targetType != null) 'target_type': targetType, + if (targetClientId != null) 'target_client_id': targetClientId, + if (rowid != null) 'rowid': rowid, + }); + } + + BudgetablesCompanion copyWith( + {Value? budgetClientId, + Value? targetType, + Value? targetClientId, + Value? rowid}) { + return BudgetablesCompanion( + budgetClientId: budgetClientId ?? this.budgetClientId, + targetType: targetType ?? this.targetType, + targetClientId: targetClientId ?? this.targetClientId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (budgetClientId.present) { + map['budget_client_id'] = Variable(budgetClientId.value); + } + if (targetType.present) { + map['target_type'] = Variable( + $BudgetablesTable.$convertertargetType.toSql(targetType.value)); + } + if (targetClientId.present) { + map['target_client_id'] = Variable(targetClientId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BudgetablesCompanion(') + ..write('budgetClientId: $budgetClientId, ') + ..write('targetType: $targetType, ') + ..write('targetClientId: $targetClientId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $BudgetPeriodStatesTable extends BudgetPeriodStates + with TableInfo<$BudgetPeriodStatesTable, BudgetPeriodState> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $BudgetPeriodStatesTable(this.attachedDatabase, [this._alias]); + @override + 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 budgetClientId = GeneratedColumn( + 'budget_client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES budgets (client_id)')); + @override + late final GeneratedColumn periodStart = GeneratedColumn( + 'period_start', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + late final GeneratedColumn periodEnd = GeneratedColumn( + 'period_end', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + late final GeneratedColumn netSpent = GeneratedColumn( + 'net_spent', aliasedName, false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0)); + @override + late final GeneratedColumn rolloverIn = GeneratedColumn( + 'rollover_in', aliasedName, false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0)); + @override + late final GeneratedColumn rolloverOut = GeneratedColumn( + 'rollover_out', aliasedName, false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(0.0)); + @override + late final GeneratedColumn closedAt = GeneratedColumn( + 'closed_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + budgetClientId, + periodStart, + periodEnd, + netSpent, + rolloverIn, + rolloverOut, + closedAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'budget_period_states'; + @override + Set get $primaryKey => {clientId}; + @override + BudgetPeriodState map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BudgetPeriodState( + 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']), + budgetClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}budget_client_id'])!, + periodStart: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}period_start'])!, + periodEnd: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}period_end'])!, + netSpent: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}net_spent'])!, + rolloverIn: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}rollover_in'])!, + rolloverOut: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}rollover_out'])!, + closedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}closed_at']), + ); + } + + @override + $BudgetPeriodStatesTable createAlias(String alias) { + return $BudgetPeriodStatesTable(attachedDatabase, alias); + } +} + +class BudgetPeriodState 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 budgetClientId; + final DateTime periodStart; + final DateTime periodEnd; + final double netSpent; + final double rolloverIn; + final double rolloverOut; + final DateTime? closedAt; + const BudgetPeriodState( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.budgetClientId, + required this.periodStart, + required this.periodEnd, + required this.netSpent, + required this.rolloverIn, + required this.rolloverOut, + this.closedAt}); + @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['budget_client_id'] = Variable(budgetClientId); + map['period_start'] = Variable(periodStart); + map['period_end'] = Variable(periodEnd); + map['net_spent'] = Variable(netSpent); + map['rollover_in'] = Variable(rolloverIn); + map['rollover_out'] = Variable(rolloverOut); + if (!nullToAbsent || closedAt != null) { + map['closed_at'] = Variable(closedAt); + } + return map; + } + + BudgetPeriodStatesCompanion toCompanion(bool nullToAbsent) { + return BudgetPeriodStatesCompanion( + 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), + budgetClientId: Value(budgetClientId), + periodStart: Value(periodStart), + periodEnd: Value(periodEnd), + netSpent: Value(netSpent), + rolloverIn: Value(rolloverIn), + rolloverOut: Value(rolloverOut), + closedAt: closedAt == null && nullToAbsent + ? const Value.absent() + : Value(closedAt), + ); + } + + factory BudgetPeriodState.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BudgetPeriodState( + 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']), + budgetClientId: serializer.fromJson(json['budgetClientId']), + periodStart: serializer.fromJson(json['periodStart']), + periodEnd: serializer.fromJson(json['periodEnd']), + netSpent: serializer.fromJson(json['netSpent']), + rolloverIn: serializer.fromJson(json['rolloverIn']), + rolloverOut: serializer.fromJson(json['rolloverOut']), + closedAt: serializer.fromJson(json['closedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'user_id': serializer.toJson(userId), + 'client_generated_id': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'created_at': serializer.toJson(createdAt), + 'updated_at': serializer.toJson(updatedAt), + 'deleted_at': serializer.toJson(deletedAt), + 'last_synced_at': serializer.toJson(lastSyncedAt), + 'budgetClientId': serializer.toJson(budgetClientId), + 'periodStart': serializer.toJson(periodStart), + 'periodEnd': serializer.toJson(periodEnd), + 'netSpent': serializer.toJson(netSpent), + 'rolloverIn': serializer.toJson(rolloverIn), + 'rolloverOut': serializer.toJson(rolloverOut), + 'closedAt': serializer.toJson(closedAt), + }; + } + + BudgetPeriodState copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? budgetClientId, + DateTime? periodStart, + DateTime? periodEnd, + double? netSpent, + double? rolloverIn, + double? rolloverOut, + Value closedAt = const Value.absent()}) => + BudgetPeriodState( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + budgetClientId: budgetClientId ?? this.budgetClientId, + periodStart: periodStart ?? this.periodStart, + periodEnd: periodEnd ?? this.periodEnd, + netSpent: netSpent ?? this.netSpent, + rolloverIn: rolloverIn ?? this.rolloverIn, + rolloverOut: rolloverOut ?? this.rolloverOut, + closedAt: closedAt.present ? closedAt.value : this.closedAt, + ); + BudgetPeriodState copyWithCompanion(BudgetPeriodStatesCompanion data) { + return BudgetPeriodState( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + budgetClientId: data.budgetClientId.present + ? data.budgetClientId.value + : this.budgetClientId, + periodStart: + data.periodStart.present ? data.periodStart.value : this.periodStart, + periodEnd: data.periodEnd.present ? data.periodEnd.value : this.periodEnd, + netSpent: data.netSpent.present ? data.netSpent.value : this.netSpent, + rolloverIn: + data.rolloverIn.present ? data.rolloverIn.value : this.rolloverIn, + rolloverOut: + data.rolloverOut.present ? data.rolloverOut.value : this.rolloverOut, + closedAt: data.closedAt.present ? data.closedAt.value : this.closedAt, + ); + } + + @override + String toString() { + return (StringBuffer('BudgetPeriodState(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('budgetClientId: $budgetClientId, ') + ..write('periodStart: $periodStart, ') + ..write('periodEnd: $periodEnd, ') + ..write('netSpent: $netSpent, ') + ..write('rolloverIn: $rolloverIn, ') + ..write('rolloverOut: $rolloverOut, ') + ..write('closedAt: $closedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + budgetClientId, + periodStart, + periodEnd, + netSpent, + rolloverIn, + rolloverOut, + closedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BudgetPeriodState && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.budgetClientId == this.budgetClientId && + other.periodStart == this.periodStart && + other.periodEnd == this.periodEnd && + other.netSpent == this.netSpent && + other.rolloverIn == this.rolloverIn && + other.rolloverOut == this.rolloverOut && + other.closedAt == this.closedAt); +} + +class BudgetPeriodStatesCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value budgetClientId; + final Value periodStart; + final Value periodEnd; + final Value netSpent; + final Value rolloverIn; + final Value rolloverOut; + final Value closedAt; + final Value rowid; + const BudgetPeriodStatesCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.budgetClientId = const Value.absent(), + this.periodStart = const Value.absent(), + this.periodEnd = const Value.absent(), + this.netSpent = const Value.absent(), + this.rolloverIn = const Value.absent(), + this.rolloverOut = const Value.absent(), + this.closedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + BudgetPeriodStatesCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String budgetClientId, + required DateTime periodStart, + required DateTime periodEnd, + this.netSpent = const Value.absent(), + this.rolloverIn = const Value.absent(), + this.rolloverOut = const Value.absent(), + this.closedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : budgetClientId = Value(budgetClientId), + periodStart = Value(periodStart), + periodEnd = Value(periodEnd); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? budgetClientId, + Expression? periodStart, + Expression? periodEnd, + Expression? netSpent, + Expression? rolloverIn, + Expression? rolloverOut, + Expression? closedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (budgetClientId != null) 'budget_client_id': budgetClientId, + if (periodStart != null) 'period_start': periodStart, + if (periodEnd != null) 'period_end': periodEnd, + if (netSpent != null) 'net_spent': netSpent, + if (rolloverIn != null) 'rollover_in': rolloverIn, + if (rolloverOut != null) 'rollover_out': rolloverOut, + if (closedAt != null) 'closed_at': closedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + BudgetPeriodStatesCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? budgetClientId, + Value? periodStart, + Value? periodEnd, + Value? netSpent, + Value? rolloverIn, + Value? rolloverOut, + Value? closedAt, + Value? rowid}) { + return BudgetPeriodStatesCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + budgetClientId: budgetClientId ?? this.budgetClientId, + periodStart: periodStart ?? this.periodStart, + periodEnd: periodEnd ?? this.periodEnd, + netSpent: netSpent ?? this.netSpent, + rolloverIn: rolloverIn ?? this.rolloverIn, + rolloverOut: rolloverOut ?? this.rolloverOut, + closedAt: closedAt ?? this.closedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (budgetClientId.present) { + map['budget_client_id'] = Variable(budgetClientId.value); + } + if (periodStart.present) { + map['period_start'] = Variable(periodStart.value); + } + if (periodEnd.present) { + map['period_end'] = Variable(periodEnd.value); + } + if (netSpent.present) { + map['net_spent'] = Variable(netSpent.value); + } + if (rolloverIn.present) { + map['rollover_in'] = Variable(rolloverIn.value); + } + if (rolloverOut.present) { + map['rollover_out'] = Variable(rolloverOut.value); + } + if (closedAt.present) { + map['closed_at'] = Variable(closedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BudgetPeriodStatesCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('budgetClientId: $budgetClientId, ') + ..write('periodStart: $periodStart, ') + ..write('periodEnd: $periodEnd, ') + ..write('netSpent: $netSpent, ') + ..write('rolloverIn: $rolloverIn, ') + ..write('rolloverOut: $rolloverOut, ') + ..write('closedAt: $closedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +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); + late final $BudgetsTable budgets = $BudgetsTable(this); + late final $BudgetablesTable budgetables = $BudgetablesTable(this); + late final $BudgetPeriodStatesTable budgetPeriodStates = + $BudgetPeriodStatesTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + wallets, + parties, + groups, + transactions, + categories, + configs, + users, + localChanges, + syncMetadata, + categorizables, + notifications, + mediaFiles, + transfers, + budgets, + budgetables, + budgetPeriodStates + ]; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} + +typedef $$WalletsTableCreateCompanionBuilder = WalletsCompanion Function({ + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + required String name, + required WalletType type, + Value balance, + required String currency, + Value description, + Value stats, + Value icon, + Value rowid, +}); +typedef $$WalletsTableUpdateCompanionBuilder = WalletsCompanion Function({ + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + Value name, + Value type, + Value balance, + Value currency, + Value description, + Value stats, + Value icon, + Value rowid, +}); + +final class $$WalletsTableReferences + extends BaseReferences<_$AppDatabase, $WalletsTable, Wallet> { + $$WalletsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$TransactionsTable, List> + _transactionsRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable(db.transactions, + aliasName: $_aliasNameGenerator( + db.wallets.clientId, db.transactions.walletClientId)); + + $$TransactionsTableProcessedTableManager get transactionsRefs { + final manager = $$TransactionsTableTableManager($_db, $_db.transactions) + .filter((f) => f.walletClientId.clientId + .sqlEquals($_itemColumn('client_id')!)); + + final cache = $_typedResult.readTableOrNull(_transactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$WalletsTableFilterComposer + extends Composer<_$AppDatabase, $WalletsTable> { + $$WalletsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get balance => $composableBuilder( + column: $table.balance, builder: (column) => ColumnFilters(column)); + + ColumnFilters get currency => $composableBuilder( + column: $table.currency, builder: (column) => ColumnFilters(column)); + + ColumnFilters get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get stats => + $composableBuilder( + column: $table.stats, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters get icon => + $composableBuilder( + column: $table.icon, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + Expression transactionsRefs( + Expression Function($$TransactionsTableFilterComposer f) f) { + final $$TransactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.walletClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableFilterComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$WalletsTableOrderingComposer + extends Composer<_$AppDatabase, $WalletsTable> { + $$WalletsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get balance => $composableBuilder( + column: $table.balance, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get currency => $composableBuilder( + column: $table.currency, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get stats => $composableBuilder( + column: $table.stats, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get icon => $composableBuilder( + column: $table.icon, builder: (column) => ColumnOrderings(column)); +} + +class $$WalletsTableAnnotationComposer + extends Composer<_$AppDatabase, $WalletsTable> { + $$WalletsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get clientId => + $composableBuilder(column: $table.clientId, builder: (column) => column); + + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + GeneratedColumn get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get balance => + $composableBuilder(column: $table.balance, builder: (column) => column); + + GeneratedColumn get currency => + $composableBuilder(column: $table.currency, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + GeneratedColumnWithTypeConverter get stats => + $composableBuilder(column: $table.stats, builder: (column) => column); + + GeneratedColumnWithTypeConverter get icon => + $composableBuilder(column: $table.icon, builder: (column) => column); + + Expression transactionsRefs( + Expression Function($$TransactionsTableAnnotationComposer a) f) { + final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.walletClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableAnnotationComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$WalletsTableTableManager extends RootTableManager< + _$AppDatabase, + $WalletsTable, + Wallet, + $$WalletsTableFilterComposer, + $$WalletsTableOrderingComposer, + $$WalletsTableAnnotationComposer, + $$WalletsTableCreateCompanionBuilder, + $$WalletsTableUpdateCompanionBuilder, + (Wallet, $$WalletsTableReferences), + Wallet, + PrefetchHooks Function({bool transactionsRefs})> { + $$WalletsTableTableManager(_$AppDatabase db, $WalletsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$WalletsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$WalletsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$WalletsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + Value name = const Value.absent(), + Value type = const Value.absent(), + Value balance = const Value.absent(), + Value currency = const Value.absent(), + Value description = const Value.absent(), + Value stats = const Value.absent(), + Value icon = const Value.absent(), + Value rowid = const Value.absent(), + }) => + WalletsCompanion( + id: id, + userId: userId, + clientId: clientId, + rev: rev, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + lastSyncedAt: lastSyncedAt, + name: name, + type: type, + balance: balance, + currency: currency, + description: description, + stats: stats, + icon: icon, + rowid: rowid, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + required String name, + required WalletType type, + Value balance = const Value.absent(), + required String currency, + Value description = const Value.absent(), + Value stats = const Value.absent(), + Value icon = const Value.absent(), + Value rowid = const Value.absent(), + }) => + WalletsCompanion.insert( + id: id, + userId: userId, + clientId: clientId, + rev: rev, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + lastSyncedAt: lastSyncedAt, + name: name, + type: type, + balance: balance, + currency: currency, + description: description, + stats: stats, + icon: icon, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$WalletsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({transactionsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (transactionsRefs) db.transactions], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (transactionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$WalletsTableReferences._transactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$WalletsTableReferences(db, table, p0) + .transactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.walletClientId == item.clientId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$WalletsTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $WalletsTable, + Wallet, + $$WalletsTableFilterComposer, + $$WalletsTableOrderingComposer, + $$WalletsTableAnnotationComposer, + $$WalletsTableCreateCompanionBuilder, + $$WalletsTableUpdateCompanionBuilder, + (Wallet, $$WalletsTableReferences), + Wallet, + PrefetchHooks Function({bool transactionsRefs})>; +typedef $$PartiesTableCreateCompanionBuilder = PartiesCompanion Function({ + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + required String name, + Value description, + Value icon, + Value type, + Value rowid, +}); +typedef $$PartiesTableUpdateCompanionBuilder = PartiesCompanion Function({ + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + Value name, + Value description, + Value icon, + Value type, + Value rowid, +}); + +final class $$PartiesTableReferences + extends BaseReferences<_$AppDatabase, $PartiesTable, Party> { + $$PartiesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$TransactionsTable, List> + _transactionsRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable(db.transactions, + aliasName: $_aliasNameGenerator( + db.parties.clientId, db.transactions.partyClientId)); + + $$TransactionsTableProcessedTableManager get transactionsRefs { + final manager = $$TransactionsTableTableManager($_db, $_db.transactions) + .filter((f) => f.partyClientId.clientId + .sqlEquals($_itemColumn('client_id')!)); + + final cache = $_typedResult.readTableOrNull(_transactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$PartiesTableFilterComposer + extends Composer<_$AppDatabase, $PartiesTable> { + $$PartiesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get icon => + $composableBuilder( + column: $table.icon, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + Expression transactionsRefs( + Expression Function($$TransactionsTableFilterComposer f) f) { + final $$TransactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.partyClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableFilterComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$PartiesTableOrderingComposer + extends Composer<_$AppDatabase, $PartiesTable> { + $$PartiesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get icon => $composableBuilder( + column: $table.icon, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); +} + +class $$PartiesTableAnnotationComposer + extends Composer<_$AppDatabase, $PartiesTable> { + $$PartiesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get clientId => + $composableBuilder(column: $table.clientId, builder: (column) => column); + + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + GeneratedColumn get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + GeneratedColumnWithTypeConverter get icon => + $composableBuilder(column: $table.icon, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + Expression transactionsRefs( + Expression Function($$TransactionsTableAnnotationComposer a) f) { + final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.partyClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableAnnotationComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$PartiesTableTableManager extends RootTableManager< + _$AppDatabase, + $PartiesTable, + Party, + $$PartiesTableFilterComposer, + $$PartiesTableOrderingComposer, + $$PartiesTableAnnotationComposer, + $$PartiesTableCreateCompanionBuilder, + $$PartiesTableUpdateCompanionBuilder, + (Party, $$PartiesTableReferences), + Party, + PrefetchHooks Function({bool transactionsRefs})> { + $$PartiesTableTableManager(_$AppDatabase db, $PartiesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PartiesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PartiesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PartiesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + Value name = const Value.absent(), + Value description = const Value.absent(), + Value icon = const Value.absent(), + Value type = const Value.absent(), + Value rowid = const Value.absent(), + }) => + PartiesCompanion( + id: id, + userId: userId, + clientId: clientId, + rev: rev, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + lastSyncedAt: lastSyncedAt, + name: name, + description: description, + icon: icon, + type: type, + rowid: rowid, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + required String name, + Value description = const Value.absent(), + Value icon = const Value.absent(), + Value type = const Value.absent(), + Value rowid = const Value.absent(), + }) => + PartiesCompanion.insert( + id: id, + userId: userId, + clientId: clientId, + rev: rev, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + lastSyncedAt: lastSyncedAt, + name: name, + description: description, + icon: icon, + type: type, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$PartiesTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({transactionsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (transactionsRefs) db.transactions], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (transactionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$PartiesTableReferences._transactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$PartiesTableReferences(db, table, p0) + .transactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.partyClientId == item.clientId), + typedResults: items) + ]; + }, + ); + }, + )); } -typedef $$WalletsTableCreateCompanionBuilder = WalletsCompanion Function({ +typedef $$PartiesTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $PartiesTable, + Party, + $$PartiesTableFilterComposer, + $$PartiesTableOrderingComposer, + $$PartiesTableAnnotationComposer, + $$PartiesTableCreateCompanionBuilder, + $$PartiesTableUpdateCompanionBuilder, + (Party, $$PartiesTableReferences), + Party, + PrefetchHooks Function({bool transactionsRefs})>; +typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ Value id, Value userId, Value clientId, @@ -6538,15 +9027,11 @@ typedef $$WalletsTableCreateCompanionBuilder = WalletsCompanion Function({ Value deletedAt, Value lastSyncedAt, required String name, - required WalletType type, - Value balance, - required String currency, Value description, - Value stats, Value icon, Value rowid, }); -typedef $$WalletsTableUpdateCompanionBuilder = WalletsCompanion Function({ +typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value id, Value userId, Value clientId, @@ -6556,28 +9041,24 @@ typedef $$WalletsTableUpdateCompanionBuilder = WalletsCompanion Function({ Value deletedAt, Value lastSyncedAt, Value name, - Value type, - Value balance, - Value currency, Value description, - Value stats, Value icon, Value rowid, }); -final class $$WalletsTableReferences - extends BaseReferences<_$AppDatabase, $WalletsTable, Wallet> { - $$WalletsTableReferences(super.$_db, super.$_table, super.$_typedResult); +final class $$GroupsTableReferences + extends BaseReferences<_$AppDatabase, $GroupsTable, Group> { + $$GroupsTableReferences(super.$_db, super.$_table, super.$_typedResult); static MultiTypedResultKey<$TransactionsTable, List> _transactionsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable(db.transactions, aliasName: $_aliasNameGenerator( - db.wallets.clientId, db.transactions.walletClientId)); + db.groups.clientId, db.transactions.groupClientId)); $$TransactionsTableProcessedTableManager get transactionsRefs { final manager = $$TransactionsTableTableManager($_db, $_db.transactions) - .filter((f) => f.walletClientId.clientId + .filter((f) => f.groupClientId.clientId .sqlEquals($_itemColumn('client_id')!)); final cache = $_typedResult.readTableOrNull(_transactionsRefsTable($_db)); @@ -6586,9 +9067,9 @@ final class $$WalletsTableReferences } } -class $$WalletsTableFilterComposer - extends Composer<_$AppDatabase, $WalletsTable> { - $$WalletsTableFilterComposer({ +class $$GroupsTableFilterComposer + extends Composer<_$AppDatabase, $GroupsTable> { + $$GroupsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6622,25 +9103,9 @@ class $$WalletsTableFilterComposer ColumnFilters get name => $composableBuilder( column: $table.name, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get type => - $composableBuilder( - column: $table.type, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - ColumnFilters get balance => $composableBuilder( - column: $table.balance, builder: (column) => ColumnFilters(column)); - - ColumnFilters get currency => $composableBuilder( - column: $table.currency, builder: (column) => ColumnFilters(column)); - ColumnFilters get description => $composableBuilder( column: $table.description, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get stats => - $composableBuilder( - column: $table.stats, - builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters get icon => $composableBuilder( column: $table.icon, @@ -6652,7 +9117,7 @@ class $$WalletsTableFilterComposer composer: this, getCurrentColumn: (t) => t.clientId, referencedTable: $db.transactions, - getReferencedColumn: (t) => t.walletClientId, + getReferencedColumn: (t) => t.groupClientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -6668,9 +9133,9 @@ class $$WalletsTableFilterComposer } } -class $$WalletsTableOrderingComposer - extends Composer<_$AppDatabase, $WalletsTable> { - $$WalletsTableOrderingComposer({ +class $$GroupsTableOrderingComposer + extends Composer<_$AppDatabase, $GroupsTable> { + $$GroupsTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6705,28 +9170,16 @@ class $$WalletsTableOrderingComposer ColumnOrderings get name => $composableBuilder( column: $table.name, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get balance => $composableBuilder( - column: $table.balance, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get currency => $composableBuilder( - column: $table.currency, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get description => $composableBuilder( column: $table.description, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get stats => $composableBuilder( - column: $table.stats, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get icon => $composableBuilder( column: $table.icon, builder: (column) => ColumnOrderings(column)); } -class $$WalletsTableAnnotationComposer - extends Composer<_$AppDatabase, $WalletsTable> { - $$WalletsTableAnnotationComposer({ +class $$GroupsTableAnnotationComposer + extends Composer<_$AppDatabase, $GroupsTable> { + $$GroupsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6760,21 +9213,9 @@ class $$WalletsTableAnnotationComposer GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); - GeneratedColumnWithTypeConverter get type => - $composableBuilder(column: $table.type, builder: (column) => column); - - GeneratedColumn get balance => - $composableBuilder(column: $table.balance, builder: (column) => column); - - GeneratedColumn get currency => - $composableBuilder(column: $table.currency, builder: (column) => column); - GeneratedColumn get description => $composableBuilder( column: $table.description, builder: (column) => column); - GeneratedColumnWithTypeConverter get stats => - $composableBuilder(column: $table.stats, builder: (column) => column); - GeneratedColumnWithTypeConverter get icon => $composableBuilder(column: $table.icon, builder: (column) => column); @@ -6784,7 +9225,7 @@ class $$WalletsTableAnnotationComposer composer: this, getCurrentColumn: (t) => t.clientId, referencedTable: $db.transactions, - getReferencedColumn: (t) => t.walletClientId, + getReferencedColumn: (t) => t.groupClientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -6800,28 +9241,28 @@ class $$WalletsTableAnnotationComposer } } -class $$WalletsTableTableManager extends RootTableManager< +class $$GroupsTableTableManager extends RootTableManager< _$AppDatabase, - $WalletsTable, - Wallet, - $$WalletsTableFilterComposer, - $$WalletsTableOrderingComposer, - $$WalletsTableAnnotationComposer, - $$WalletsTableCreateCompanionBuilder, - $$WalletsTableUpdateCompanionBuilder, - (Wallet, $$WalletsTableReferences), - Wallet, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, $$GroupsTableReferences), + Group, PrefetchHooks Function({bool transactionsRefs})> { - $$WalletsTableTableManager(_$AppDatabase db, $WalletsTable table) + $$GroupsTableTableManager(_$AppDatabase db, $GroupsTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$WalletsTableFilterComposer($db: db, $table: table), + $$GroupsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$WalletsTableOrderingComposer($db: db, $table: table), + $$GroupsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$WalletsTableAnnotationComposer($db: db, $table: table), + $$GroupsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value userId = const Value.absent(), @@ -6832,15 +9273,11 @@ class $$WalletsTableTableManager extends RootTableManager< Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), Value name = const Value.absent(), - Value type = const Value.absent(), - Value balance = const Value.absent(), - Value currency = const Value.absent(), Value description = const Value.absent(), - Value stats = const Value.absent(), Value icon = const Value.absent(), Value rowid = const Value.absent(), }) => - WalletsCompanion( + GroupsCompanion( id: id, userId: userId, clientId: clientId, @@ -6850,11 +9287,7 @@ class $$WalletsTableTableManager extends RootTableManager< deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, name: name, - type: type, - balance: balance, - currency: currency, description: description, - stats: stats, icon: icon, rowid: rowid, ), @@ -6868,15 +9301,11 @@ class $$WalletsTableTableManager extends RootTableManager< Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), required String name, - required WalletType type, - Value balance = const Value.absent(), - required String currency, Value description = const Value.absent(), - Value stats = const Value.absent(), Value icon = const Value.absent(), Value rowid = const Value.absent(), }) => - WalletsCompanion.insert( + GroupsCompanion.insert( id: id, userId: userId, clientId: clientId, @@ -6886,17 +9315,13 @@ class $$WalletsTableTableManager extends RootTableManager< deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, name: name, - type: type, - balance: balance, - currency: currency, description: description, - stats: stats, icon: icon, rowid: rowid, ), withReferenceMapper: (p0) => p0 .map((e) => - (e.readTable(table), $$WalletsTableReferences(db, table, e))) + (e.readTable(table), $$GroupsTableReferences(db, table, e))) .toList(), prefetchHooksCallback: ({transactionsRefs = false}) { return PrefetchHooks( @@ -6906,17 +9331,16 @@ class $$WalletsTableTableManager extends RootTableManager< getPrefetchedDataCallback: (items) async { return [ if (transactionsRefs) - await $_getPrefetchedData( + await $_getPrefetchedData( currentTable: table, referencedTable: - $$WalletsTableReferences._transactionsRefsTable(db), + $$GroupsTableReferences._transactionsRefsTable(db), managerFromTypedResult: (p0) => - $$WalletsTableReferences(db, table, p0) + $$GroupsTableReferences(db, table, p0) .transactionsRefs, referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.walletClientId == item.clientId), + (item, referencedItems) => referencedItems + .where((e) => e.groupClientId == item.clientId), typedResults: items) ]; }, @@ -6925,19 +9349,20 @@ class $$WalletsTableTableManager extends RootTableManager< )); } -typedef $$WalletsTableProcessedTableManager = ProcessedTableManager< +typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $WalletsTable, - Wallet, - $$WalletsTableFilterComposer, - $$WalletsTableOrderingComposer, - $$WalletsTableAnnotationComposer, - $$WalletsTableCreateCompanionBuilder, - $$WalletsTableUpdateCompanionBuilder, - (Wallet, $$WalletsTableReferences), - Wallet, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, $$GroupsTableReferences), + Group, PrefetchHooks Function({bool transactionsRefs})>; -typedef $$PartiesTableCreateCompanionBuilder = PartiesCompanion Function({ +typedef $$TransactionsTableCreateCompanionBuilder = TransactionsCompanion + Function({ Value id, Value userId, Value clientId, @@ -6946,13 +9371,22 @@ typedef $$PartiesTableCreateCompanionBuilder = PartiesCompanion Function({ Value updatedAt, Value deletedAt, Value lastSyncedAt, - required String name, + required double amount, + required TransactionType type, Value description, - Value icon, - Value type, + Value datetime, + Value partyId, + Value walletId, + Value groupId, + required String walletClientId, + Value partyClientId, + Value groupClientId, + Value transferId, + Value transferClientId, Value rowid, }); -typedef $$PartiesTableUpdateCompanionBuilder = PartiesCompanion Function({ +typedef $$TransactionsTableUpdateCompanionBuilder = TransactionsCompanion + Function({ Value id, Value userId, Value clientId, @@ -6961,37 +9395,74 @@ typedef $$PartiesTableUpdateCompanionBuilder = PartiesCompanion Function({ Value updatedAt, Value deletedAt, Value lastSyncedAt, - Value name, + Value amount, + Value type, Value description, - Value icon, - Value type, + Value datetime, + Value partyId, + Value walletId, + Value groupId, + Value walletClientId, + Value partyClientId, + Value groupClientId, + Value transferId, + Value transferClientId, Value rowid, }); -final class $$PartiesTableReferences - extends BaseReferences<_$AppDatabase, $PartiesTable, Party> { - $$PartiesTableReferences(super.$_db, super.$_table, super.$_typedResult); +final class $$TransactionsTableReferences + extends BaseReferences<_$AppDatabase, $TransactionsTable, Transaction> { + $$TransactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$TransactionsTable, List> - _transactionsRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable(db.transactions, - aliasName: $_aliasNameGenerator( - db.parties.clientId, db.transactions.partyClientId)); + static $WalletsTable _walletClientIdTable(_$AppDatabase db) => + db.wallets.createAlias($_aliasNameGenerator( + db.transactions.walletClientId, db.wallets.clientId)); - $$TransactionsTableProcessedTableManager get transactionsRefs { - final manager = $$TransactionsTableTableManager($_db, $_db.transactions) - .filter((f) => f.partyClientId.clientId - .sqlEquals($_itemColumn('client_id')!)); + $$WalletsTableProcessedTableManager get walletClientId { + final $_column = $_itemColumn('wallet_client_id')!; - final cache = $_typedResult.readTableOrNull(_transactionsRefsTable($_db)); + final manager = $$WalletsTableTableManager($_db, $_db.wallets) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_walletClientIdTable($_db)); + if (item == null) return manager; return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); + manager.$state.copyWith(prefetchedData: [item])); + } + + static $PartiesTable _partyClientIdTable(_$AppDatabase db) => + db.parties.createAlias($_aliasNameGenerator( + db.transactions.partyClientId, db.parties.clientId)); + + $$PartiesTableProcessedTableManager? get partyClientId { + final $_column = $_itemColumn('party_client_id'); + if ($_column == null) return null; + final manager = $$PartiesTableTableManager($_db, $_db.parties) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_partyClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $GroupsTable _groupClientIdTable(_$AppDatabase db) => + db.groups.createAlias($_aliasNameGenerator( + db.transactions.groupClientId, db.groups.clientId)); + + $$GroupsTableProcessedTableManager? get groupClientId { + final $_column = $_itemColumn('group_client_id'); + if ($_column == null) return null; + final manager = $$GroupsTableTableManager($_db, $_db.groups) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); } } -class $$PartiesTableFilterComposer - extends Composer<_$AppDatabase, $PartiesTable> { - $$PartiesTableFilterComposer({ +class $$TransactionsTableFilterComposer + extends Composer<_$AppDatabase, $TransactionsTable> { + $$TransactionsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -7022,94 +9493,223 @@ class $$PartiesTableFilterComposer ColumnFilters get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); + ColumnFilters get amount => $composableBuilder( + column: $table.amount, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); ColumnFilters get description => $composableBuilder( column: $table.description, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get icon => - $composableBuilder( - column: $table.icon, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get datetime => $composableBuilder( + column: $table.datetime, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get type => - $composableBuilder( - column: $table.type, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get partyId => $composableBuilder( + column: $table.partyId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get walletId => $composableBuilder( + column: $table.walletId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get transferId => $composableBuilder( + column: $table.transferId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get transferClientId => $composableBuilder( + column: $table.transferClientId, + builder: (column) => ColumnFilters(column)); + + $$WalletsTableFilterComposer get walletClientId { + final $$WalletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.walletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableFilterComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$PartiesTableFilterComposer get partyClientId { + final $$PartiesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.partyClientId, + referencedTable: $db.parties, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$PartiesTableFilterComposer( + $db: $db, + $table: $db.parties, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$GroupsTableFilterComposer get groupClientId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupClientId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$TransactionsTableOrderingComposer + extends Composer<_$AppDatabase, $TransactionsTable> { + $$TransactionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get amount => $composableBuilder( + column: $table.amount, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get datetime => $composableBuilder( + column: $table.datetime, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get partyId => $composableBuilder( + column: $table.partyId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get walletId => $composableBuilder( + column: $table.walletId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get transferId => $composableBuilder( + column: $table.transferId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get transferClientId => $composableBuilder( + column: $table.transferClientId, + builder: (column) => ColumnOrderings(column)); + + $$WalletsTableOrderingComposer get walletClientId { + final $$WalletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.walletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableOrderingComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$PartiesTableOrderingComposer get partyClientId { + final $$PartiesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.partyClientId, + referencedTable: $db.parties, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$PartiesTableOrderingComposer( + $db: $db, + $table: $db.parties, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } - Expression transactionsRefs( - Expression Function($$TransactionsTableFilterComposer f) f) { - final $$TransactionsTableFilterComposer composer = $composerBuilder( + $$GroupsTableOrderingComposer get groupClientId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.clientId, - referencedTable: $db.transactions, - getReferencedColumn: (t) => t.partyClientId, + getCurrentColumn: (t) => t.groupClientId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableFilterComposer( + $$GroupsTableOrderingComposer( $db: $db, - $table: $db.transactions, + $table: $db.groups, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, )); - return f(composer); + return composer; } } -class $$PartiesTableOrderingComposer - extends Composer<_$AppDatabase, $PartiesTable> { - $$PartiesTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get description => $composableBuilder( - column: $table.description, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get icon => $composableBuilder( - column: $table.icon, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); -} - -class $$PartiesTableAnnotationComposer - extends Composer<_$AppDatabase, $PartiesTable> { - $$PartiesTableAnnotationComposer({ +class $$TransactionsTableAnnotationComposer + extends Composer<_$AppDatabase, $TransactionsTable> { + $$TransactionsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -7140,62 +9740,117 @@ class $$PartiesTableAnnotationComposer GeneratedColumn get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => column); - GeneratedColumn get name => - $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get amount => + $composableBuilder(column: $table.amount, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); GeneratedColumn get description => $composableBuilder( column: $table.description, builder: (column) => column); - GeneratedColumnWithTypeConverter get icon => - $composableBuilder(column: $table.icon, builder: (column) => column); + GeneratedColumn get datetime => + $composableBuilder(column: $table.datetime, builder: (column) => column); - GeneratedColumnWithTypeConverter get type => - $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get partyId => + $composableBuilder(column: $table.partyId, builder: (column) => column); - Expression transactionsRefs( - Expression Function($$TransactionsTableAnnotationComposer a) f) { - final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + GeneratedColumn get walletId => + $composableBuilder(column: $table.walletId, builder: (column) => column); + + GeneratedColumn get groupId => + $composableBuilder(column: $table.groupId, builder: (column) => column); + + GeneratedColumn get transferId => $composableBuilder( + column: $table.transferId, builder: (column) => column); + + GeneratedColumn get transferClientId => $composableBuilder( + column: $table.transferClientId, builder: (column) => column); + + $$WalletsTableAnnotationComposer get walletClientId { + final $$WalletsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.clientId, - referencedTable: $db.transactions, - getReferencedColumn: (t) => t.partyClientId, + getCurrentColumn: (t) => t.walletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableAnnotationComposer( + $$WalletsTableAnnotationComposer( $db: $db, - $table: $db.transactions, + $table: $db.wallets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, )); - return f(composer); + return composer; + } + + $$PartiesTableAnnotationComposer get partyClientId { + final $$PartiesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.partyClientId, + referencedTable: $db.parties, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$PartiesTableAnnotationComposer( + $db: $db, + $table: $db.parties, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$GroupsTableAnnotationComposer get groupClientId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupClientId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; } } -class $$PartiesTableTableManager extends RootTableManager< +class $$TransactionsTableTableManager extends RootTableManager< _$AppDatabase, - $PartiesTable, - Party, - $$PartiesTableFilterComposer, - $$PartiesTableOrderingComposer, - $$PartiesTableAnnotationComposer, - $$PartiesTableCreateCompanionBuilder, - $$PartiesTableUpdateCompanionBuilder, - (Party, $$PartiesTableReferences), - Party, - PrefetchHooks Function({bool transactionsRefs})> { - $$PartiesTableTableManager(_$AppDatabase db, $PartiesTable table) + $TransactionsTable, + Transaction, + $$TransactionsTableFilterComposer, + $$TransactionsTableOrderingComposer, + $$TransactionsTableAnnotationComposer, + $$TransactionsTableCreateCompanionBuilder, + $$TransactionsTableUpdateCompanionBuilder, + (Transaction, $$TransactionsTableReferences), + Transaction, + PrefetchHooks Function( + {bool walletClientId, bool partyClientId, bool groupClientId})> { + $$TransactionsTableTableManager(_$AppDatabase db, $TransactionsTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$PartiesTableFilterComposer($db: db, $table: table), + $$TransactionsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$PartiesTableOrderingComposer($db: db, $table: table), + $$TransactionsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$PartiesTableAnnotationComposer($db: db, $table: table), + $$TransactionsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value userId = const Value.absent(), @@ -7205,13 +9860,21 @@ class $$PartiesTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - Value name = const Value.absent(), + Value amount = const Value.absent(), + Value type = const Value.absent(), Value description = const Value.absent(), - Value icon = const Value.absent(), - Value type = const Value.absent(), + Value datetime = const Value.absent(), + Value partyId = const Value.absent(), + Value walletId = const Value.absent(), + Value groupId = const Value.absent(), + Value walletClientId = const Value.absent(), + Value partyClientId = const Value.absent(), + Value groupClientId = const Value.absent(), + Value transferId = const Value.absent(), + Value transferClientId = const Value.absent(), Value rowid = const Value.absent(), }) => - PartiesCompanion( + TransactionsCompanion( id: id, userId: userId, clientId: clientId, @@ -7220,10 +9883,18 @@ class $$PartiesTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - name: name, - description: description, - icon: icon, + amount: amount, type: type, + description: description, + datetime: datetime, + partyId: partyId, + walletId: walletId, + groupId: groupId, + walletClientId: walletClientId, + partyClientId: partyClientId, + groupClientId: groupClientId, + transferId: transferId, + transferClientId: transferClientId, rowid: rowid, ), createCompanionCallback: ({ @@ -7235,13 +9906,21 @@ class $$PartiesTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - required String name, + required double amount, + required TransactionType type, Value description = const Value.absent(), - Value icon = const Value.absent(), - Value type = const Value.absent(), + Value datetime = const Value.absent(), + Value partyId = const Value.absent(), + Value walletId = const Value.absent(), + Value groupId = const Value.absent(), + required String walletClientId, + Value partyClientId = const Value.absent(), + Value groupClientId = const Value.absent(), + Value transferId = const Value.absent(), + Value transferClientId = const Value.absent(), Value rowid = const Value.absent(), }) => - PartiesCompanion.insert( + TransactionsCompanion.insert( id: id, userId: userId, clientId: clientId, @@ -7250,56 +9929,104 @@ class $$PartiesTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - name: name, - description: description, - icon: icon, + amount: amount, type: type, + description: description, + datetime: datetime, + partyId: partyId, + walletId: walletId, + groupId: groupId, + walletClientId: walletClientId, + partyClientId: partyClientId, + groupClientId: groupClientId, + transferId: transferId, + transferClientId: transferClientId, rowid: rowid, ), - withReferenceMapper: (p0) => p0 - .map((e) => - (e.readTable(table), $$PartiesTableReferences(db, table, e))) + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$TransactionsTableReferences(db, table, e) + )) .toList(), - prefetchHooksCallback: ({transactionsRefs = false}) { + prefetchHooksCallback: ( + {walletClientId = false, + partyClientId = false, + groupClientId = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [if (transactionsRefs) db.transactions], - addJoins: null, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (walletClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.walletClientId, + referencedTable: + $$TransactionsTableReferences._walletClientIdTable(db), + referencedColumn: $$TransactionsTableReferences + ._walletClientIdTable(db) + .clientId, + ) as T; + } + if (partyClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.partyClientId, + referencedTable: + $$TransactionsTableReferences._partyClientIdTable(db), + referencedColumn: $$TransactionsTableReferences + ._partyClientIdTable(db) + .clientId, + ) as T; + } + if (groupClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.groupClientId, + referencedTable: + $$TransactionsTableReferences._groupClientIdTable(db), + referencedColumn: $$TransactionsTableReferences + ._groupClientIdTable(db) + .clientId, + ) as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { - return [ - if (transactionsRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: - $$PartiesTableReferences._transactionsRefsTable(db), - managerFromTypedResult: (p0) => - $$PartiesTableReferences(db, table, p0) - .transactionsRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.partyClientId == item.clientId), - typedResults: items) - ]; + return []; }, ); }, )); } -typedef $$PartiesTableProcessedTableManager = ProcessedTableManager< +typedef $$TransactionsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $PartiesTable, - Party, - $$PartiesTableFilterComposer, - $$PartiesTableOrderingComposer, - $$PartiesTableAnnotationComposer, - $$PartiesTableCreateCompanionBuilder, - $$PartiesTableUpdateCompanionBuilder, - (Party, $$PartiesTableReferences), - Party, - PrefetchHooks Function({bool transactionsRefs})>; -typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ + $TransactionsTable, + Transaction, + $$TransactionsTableFilterComposer, + $$TransactionsTableOrderingComposer, + $$TransactionsTableAnnotationComposer, + $$TransactionsTableCreateCompanionBuilder, + $$TransactionsTableUpdateCompanionBuilder, + (Transaction, $$TransactionsTableReferences), + Transaction, + PrefetchHooks Function( + {bool walletClientId, bool partyClientId, bool groupClientId})>; +typedef $$CategoriesTableCreateCompanionBuilder = CategoriesCompanion Function({ Value id, Value userId, Value clientId, @@ -7309,11 +10036,13 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ Value deletedAt, Value lastSyncedAt, required String name, + required String slug, Value description, + required TransactionType type, Value icon, Value rowid, }); -typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ +typedef $$CategoriesTableUpdateCompanionBuilder = CategoriesCompanion Function({ Value id, Value userId, Value clientId, @@ -7323,35 +10052,37 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value deletedAt, Value lastSyncedAt, Value name, + Value slug, Value description, + Value type, Value icon, Value rowid, }); -final class $$GroupsTableReferences - extends BaseReferences<_$AppDatabase, $GroupsTable, Group> { - $$GroupsTableReferences(super.$_db, super.$_table, super.$_typedResult); +final class $$CategoriesTableReferences + extends BaseReferences<_$AppDatabase, $CategoriesTable, Category> { + $$CategoriesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$TransactionsTable, List> - _transactionsRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable(db.transactions, + static MultiTypedResultKey<$CategorizablesTable, List> + _categorizablesRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable(db.categorizables, aliasName: $_aliasNameGenerator( - db.groups.clientId, db.transactions.groupClientId)); + db.categories.clientId, db.categorizables.categoryClientId)); - $$TransactionsTableProcessedTableManager get transactionsRefs { - final manager = $$TransactionsTableTableManager($_db, $_db.transactions) - .filter((f) => f.groupClientId.clientId + $$CategorizablesTableProcessedTableManager get categorizablesRefs { + final manager = $$CategorizablesTableTableManager($_db, $_db.categorizables) + .filter((f) => f.categoryClientId.clientId .sqlEquals($_itemColumn('client_id')!)); - final cache = $_typedResult.readTableOrNull(_transactionsRefsTable($_db)); + final cache = $_typedResult.readTableOrNull(_categorizablesRefsTable($_db)); return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } } -class $$GroupsTableFilterComposer - extends Composer<_$AppDatabase, $GroupsTable> { - $$GroupsTableFilterComposer({ +class $$CategoriesTableFilterComposer + extends Composer<_$AppDatabase, $CategoriesTable> { + $$CategoriesTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -7385,27 +10116,35 @@ class $$GroupsTableFilterComposer ColumnFilters get name => $composableBuilder( column: $table.name, builder: (column) => ColumnFilters(column)); + ColumnFilters get slug => $composableBuilder( + column: $table.slug, builder: (column) => ColumnFilters(column)); + ColumnFilters get description => $composableBuilder( column: $table.description, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters get icon => $composableBuilder( column: $table.icon, builder: (column) => ColumnWithTypeConverterFilters(column)); - Expression transactionsRefs( - Expression Function($$TransactionsTableFilterComposer f) f) { - final $$TransactionsTableFilterComposer composer = $composerBuilder( + Expression categorizablesRefs( + Expression Function($$CategorizablesTableFilterComposer f) f) { + final $$CategorizablesTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.clientId, - referencedTable: $db.transactions, - getReferencedColumn: (t) => t.groupClientId, + referencedTable: $db.categorizables, + getReferencedColumn: (t) => t.categoryClientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableFilterComposer( + $$CategorizablesTableFilterComposer( $db: $db, - $table: $db.transactions, + $table: $db.categorizables, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -7415,9 +10154,9 @@ class $$GroupsTableFilterComposer } } -class $$GroupsTableOrderingComposer - extends Composer<_$AppDatabase, $GroupsTable> { - $$GroupsTableOrderingComposer({ +class $$CategoriesTableOrderingComposer + extends Composer<_$AppDatabase, $CategoriesTable> { + $$CategoriesTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -7452,16 +10191,22 @@ class $$GroupsTableOrderingComposer ColumnOrderings get name => $composableBuilder( column: $table.name, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get slug => $composableBuilder( + column: $table.slug, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get description => $composableBuilder( column: $table.description, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get icon => $composableBuilder( column: $table.icon, builder: (column) => ColumnOrderings(column)); } -class $$GroupsTableAnnotationComposer - extends Composer<_$AppDatabase, $GroupsTable> { - $$GroupsTableAnnotationComposer({ +class $$CategoriesTableAnnotationComposer + extends Composer<_$AppDatabase, $CategoriesTable> { + $$CategoriesTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -7495,25 +10240,31 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get slug => + $composableBuilder(column: $table.slug, builder: (column) => column); + GeneratedColumn get description => $composableBuilder( column: $table.description, builder: (column) => column); + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumnWithTypeConverter get icon => $composableBuilder(column: $table.icon, builder: (column) => column); - Expression transactionsRefs( - Expression Function($$TransactionsTableAnnotationComposer a) f) { - final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + Expression categorizablesRefs( + Expression Function($$CategorizablesTableAnnotationComposer a) f) { + final $$CategorizablesTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.clientId, - referencedTable: $db.transactions, - getReferencedColumn: (t) => t.groupClientId, + referencedTable: $db.categorizables, + getReferencedColumn: (t) => t.categoryClientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableAnnotationComposer( + $$CategorizablesTableAnnotationComposer( $db: $db, - $table: $db.transactions, + $table: $db.categorizables, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -7523,28 +10274,28 @@ class $$GroupsTableAnnotationComposer } } -class $$GroupsTableTableManager extends RootTableManager< +class $$CategoriesTableTableManager extends RootTableManager< _$AppDatabase, - $GroupsTable, - Group, - $$GroupsTableFilterComposer, - $$GroupsTableOrderingComposer, - $$GroupsTableAnnotationComposer, - $$GroupsTableCreateCompanionBuilder, - $$GroupsTableUpdateCompanionBuilder, - (Group, $$GroupsTableReferences), - Group, - PrefetchHooks Function({bool transactionsRefs})> { - $$GroupsTableTableManager(_$AppDatabase db, $GroupsTable table) + $CategoriesTable, + Category, + $$CategoriesTableFilterComposer, + $$CategoriesTableOrderingComposer, + $$CategoriesTableAnnotationComposer, + $$CategoriesTableCreateCompanionBuilder, + $$CategoriesTableUpdateCompanionBuilder, + (Category, $$CategoriesTableReferences), + Category, + PrefetchHooks Function({bool categorizablesRefs})> { + $$CategoriesTableTableManager(_$AppDatabase db, $CategoriesTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$GroupsTableFilterComposer($db: db, $table: table), + $$CategoriesTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$GroupsTableOrderingComposer($db: db, $table: table), + $$CategoriesTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$GroupsTableAnnotationComposer($db: db, $table: table), + $$CategoriesTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value userId = const Value.absent(), @@ -7555,11 +10306,13 @@ class $$GroupsTableTableManager extends RootTableManager< Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), Value name = const Value.absent(), + Value slug = const Value.absent(), Value description = const Value.absent(), + Value type = const Value.absent(), Value icon = const Value.absent(), Value rowid = const Value.absent(), }) => - GroupsCompanion( + CategoriesCompanion( id: id, userId: userId, clientId: clientId, @@ -7569,7 +10322,9 @@ class $$GroupsTableTableManager extends RootTableManager< deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, name: name, + slug: slug, description: description, + type: type, icon: icon, rowid: rowid, ), @@ -7583,11 +10338,13 @@ class $$GroupsTableTableManager extends RootTableManager< Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), required String name, + required String slug, Value description = const Value.absent(), + required TransactionType type, Value icon = const Value.absent(), Value rowid = const Value.absent(), }) => - GroupsCompanion.insert( + CategoriesCompanion.insert( id: id, userId: userId, clientId: clientId, @@ -7597,32 +10354,39 @@ class $$GroupsTableTableManager extends RootTableManager< deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, name: name, + slug: slug, description: description, + type: type, icon: icon, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => - (e.readTable(table), $$GroupsTableReferences(db, table, e))) + .map((e) => ( + e.readTable(table), + $$CategoriesTableReferences(db, table, e) + )) .toList(), - prefetchHooksCallback: ({transactionsRefs = false}) { + prefetchHooksCallback: ({categorizablesRefs = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [if (transactionsRefs) db.transactions], + explicitlyWatchedTables: [ + if (categorizablesRefs) db.categorizables + ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ - if (transactionsRefs) - await $_getPrefetchedData( + if (categorizablesRefs) + await $_getPrefetchedData( currentTable: table, - referencedTable: - $$GroupsTableReferences._transactionsRefsTable(db), + referencedTable: $$CategoriesTableReferences + ._categorizablesRefsTable(db), managerFromTypedResult: (p0) => - $$GroupsTableReferences(db, table, p0) - .transactionsRefs, + $$CategoriesTableReferences(db, table, p0) + .categorizablesRefs, referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.groupClientId == item.clientId), + (item, referencedItems) => referencedItems.where( + (e) => e.categoryClientId == item.clientId), typedResults: items) ]; }, @@ -7631,20 +10395,19 @@ class $$GroupsTableTableManager extends RootTableManager< )); } -typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< +typedef $$CategoriesTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $GroupsTable, - Group, - $$GroupsTableFilterComposer, - $$GroupsTableOrderingComposer, - $$GroupsTableAnnotationComposer, - $$GroupsTableCreateCompanionBuilder, - $$GroupsTableUpdateCompanionBuilder, - (Group, $$GroupsTableReferences), - Group, - PrefetchHooks Function({bool transactionsRefs})>; -typedef $$TransactionsTableCreateCompanionBuilder = TransactionsCompanion - Function({ + $CategoriesTable, + Category, + $$CategoriesTableFilterComposer, + $$CategoriesTableOrderingComposer, + $$CategoriesTableAnnotationComposer, + $$CategoriesTableCreateCompanionBuilder, + $$CategoriesTableUpdateCompanionBuilder, + (Category, $$CategoriesTableReferences), + Category, + PrefetchHooks Function({bool categorizablesRefs})>; +typedef $$ConfigsTableCreateCompanionBuilder = ConfigsCompanion Function({ Value id, Value userId, Value clientId, @@ -7653,22 +10416,12 @@ typedef $$TransactionsTableCreateCompanionBuilder = TransactionsCompanion Value updatedAt, Value deletedAt, Value lastSyncedAt, - required double amount, - required TransactionType type, - Value description, - Value datetime, - Value partyId, - Value walletId, - Value groupId, - required String walletClientId, - Value partyClientId, - Value groupClientId, - Value transferId, - Value transferClientId, + required String key, + required ConfigType type, + required dynamic value, Value rowid, }); -typedef $$TransactionsTableUpdateCompanionBuilder = TransactionsCompanion - Function({ +typedef $$ConfigsTableUpdateCompanionBuilder = ConfigsCompanion Function({ Value id, Value userId, Value clientId, @@ -7677,74 +10430,15 @@ typedef $$TransactionsTableUpdateCompanionBuilder = TransactionsCompanion Value updatedAt, Value deletedAt, Value lastSyncedAt, - Value amount, - Value type, - Value description, - Value datetime, - Value partyId, - Value walletId, - Value groupId, - Value walletClientId, - Value partyClientId, - Value groupClientId, - Value transferId, - Value transferClientId, + Value key, + Value type, + Value value, Value rowid, }); -final class $$TransactionsTableReferences - extends BaseReferences<_$AppDatabase, $TransactionsTable, Transaction> { - $$TransactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); - - static $WalletsTable _walletClientIdTable(_$AppDatabase db) => - db.wallets.createAlias($_aliasNameGenerator( - db.transactions.walletClientId, db.wallets.clientId)); - - $$WalletsTableProcessedTableManager get walletClientId { - final $_column = $_itemColumn('wallet_client_id')!; - - final manager = $$WalletsTableTableManager($_db, $_db.wallets) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_walletClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } - - static $PartiesTable _partyClientIdTable(_$AppDatabase db) => - db.parties.createAlias($_aliasNameGenerator( - db.transactions.partyClientId, db.parties.clientId)); - - $$PartiesTableProcessedTableManager? get partyClientId { - final $_column = $_itemColumn('party_client_id'); - if ($_column == null) return null; - final manager = $$PartiesTableTableManager($_db, $_db.parties) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_partyClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } - - static $GroupsTable _groupClientIdTable(_$AppDatabase db) => - db.groups.createAlias($_aliasNameGenerator( - db.transactions.groupClientId, db.groups.clientId)); - - $$GroupsTableProcessedTableManager? get groupClientId { - final $_column = $_itemColumn('group_client_id'); - if ($_column == null) return null; - final manager = $$GroupsTableTableManager($_db, $_db.groups) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_groupClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } -} - -class $$TransactionsTableFilterComposer - extends Composer<_$AppDatabase, $TransactionsTable> { - $$TransactionsTableFilterComposer({ +class $$ConfigsTableFilterComposer + extends Composer<_$AppDatabase, $ConfigsTable> { + $$ConfigsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -7775,100 +10469,23 @@ class $$TransactionsTableFilterComposer ColumnFilters get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get amount => $composableBuilder( - column: $table.amount, builder: (column) => ColumnFilters(column)); + ColumnFilters get key => $composableBuilder( + column: $table.key, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters - get type => $composableBuilder( + ColumnWithTypeConverterFilters get type => + $composableBuilder( column: $table.type, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get description => $composableBuilder( - column: $table.description, builder: (column) => ColumnFilters(column)); - - ColumnFilters get datetime => $composableBuilder( - column: $table.datetime, builder: (column) => ColumnFilters(column)); - - ColumnFilters get partyId => $composableBuilder( - column: $table.partyId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get walletId => $composableBuilder( - column: $table.walletId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get transferId => $composableBuilder( - column: $table.transferId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get transferClientId => $composableBuilder( - column: $table.transferClientId, - builder: (column) => ColumnFilters(column)); - - $$WalletsTableFilterComposer get walletClientId { - final $$WalletsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.walletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableFilterComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - - $$PartiesTableFilterComposer get partyClientId { - final $$PartiesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.partyClientId, - referencedTable: $db.parties, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PartiesTableFilterComposer( - $db: $db, - $table: $db.parties, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - - $$GroupsTableFilterComposer get groupClientId { - final $$GroupsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.groupClientId, - referencedTable: $db.groups, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$GroupsTableFilterComposer( - $db: $db, - $table: $db.groups, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnWithTypeConverterFilters get value => + $composableBuilder( + column: $table.value, + builder: (column) => ColumnWithTypeConverterFilters(column)); } -class $$TransactionsTableOrderingComposer - extends Composer<_$AppDatabase, $TransactionsTable> { - $$TransactionsTableOrderingComposer({ +class $$ConfigsTableOrderingComposer + extends Composer<_$AppDatabase, $ConfigsTable> { + $$ConfigsTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -7881,117 +10498,38 @@ class $$TransactionsTableOrderingComposer ColumnOrderings get userId => $composableBuilder( column: $table.userId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get amount => $composableBuilder( - column: $table.amount, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get description => $composableBuilder( - column: $table.description, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get datetime => $composableBuilder( - column: $table.datetime, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get partyId => $composableBuilder( - column: $table.partyId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get walletId => $composableBuilder( - column: $table.walletId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get groupId => $composableBuilder( - column: $table.groupId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get transferId => $composableBuilder( - column: $table.transferId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get transferClientId => $composableBuilder( - column: $table.transferClientId, + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => ColumnOrderings(column)); - $$WalletsTableOrderingComposer get walletClientId { - final $$WalletsTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.walletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableOrderingComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnOrderings get key => $composableBuilder( + column: $table.key, builder: (column) => ColumnOrderings(column)); - $$PartiesTableOrderingComposer get partyClientId { - final $$PartiesTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.partyClientId, - referencedTable: $db.parties, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PartiesTableOrderingComposer( - $db: $db, - $table: $db.parties, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); - $$GroupsTableOrderingComposer get groupClientId { - final $$GroupsTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.groupClientId, - referencedTable: $db.groups, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$GroupsTableOrderingComposer( - $db: $db, - $table: $db.groups, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnOrderings get value => $composableBuilder( + column: $table.value, builder: (column) => ColumnOrderings(column)); } -class $$TransactionsTableAnnotationComposer - extends Composer<_$AppDatabase, $TransactionsTable> { - $$TransactionsTableAnnotationComposer({ +class $$ConfigsTableAnnotationComposer + extends Composer<_$AppDatabase, $ConfigsTable> { + $$ConfigsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -8022,117 +10560,38 @@ class $$TransactionsTableAnnotationComposer GeneratedColumn get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => column); - GeneratedColumn get amount => - $composableBuilder(column: $table.amount, builder: (column) => column); + GeneratedColumn get key => + $composableBuilder(column: $table.key, builder: (column) => column); - GeneratedColumnWithTypeConverter get type => + GeneratedColumnWithTypeConverter get type => $composableBuilder(column: $table.type, builder: (column) => column); - GeneratedColumn get description => $composableBuilder( - column: $table.description, builder: (column) => column); - - GeneratedColumn get datetime => - $composableBuilder(column: $table.datetime, builder: (column) => column); - - GeneratedColumn get partyId => - $composableBuilder(column: $table.partyId, builder: (column) => column); - - GeneratedColumn get walletId => - $composableBuilder(column: $table.walletId, builder: (column) => column); - - GeneratedColumn get groupId => - $composableBuilder(column: $table.groupId, builder: (column) => column); - - GeneratedColumn get transferId => $composableBuilder( - column: $table.transferId, builder: (column) => column); - - GeneratedColumn get transferClientId => $composableBuilder( - column: $table.transferClientId, builder: (column) => column); - - $$WalletsTableAnnotationComposer get walletClientId { - final $$WalletsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.walletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableAnnotationComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - - $$PartiesTableAnnotationComposer get partyClientId { - final $$PartiesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.partyClientId, - referencedTable: $db.parties, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PartiesTableAnnotationComposer( - $db: $db, - $table: $db.parties, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } - - $$GroupsTableAnnotationComposer get groupClientId { - final $$GroupsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.groupClientId, - referencedTable: $db.groups, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$GroupsTableAnnotationComposer( - $db: $db, - $table: $db.groups, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + GeneratedColumnWithTypeConverter get value => + $composableBuilder(column: $table.value, builder: (column) => column); } -class $$TransactionsTableTableManager extends RootTableManager< +class $$ConfigsTableTableManager extends RootTableManager< _$AppDatabase, - $TransactionsTable, - Transaction, - $$TransactionsTableFilterComposer, - $$TransactionsTableOrderingComposer, - $$TransactionsTableAnnotationComposer, - $$TransactionsTableCreateCompanionBuilder, - $$TransactionsTableUpdateCompanionBuilder, - (Transaction, $$TransactionsTableReferences), - Transaction, - PrefetchHooks Function( - {bool walletClientId, bool partyClientId, bool groupClientId})> { - $$TransactionsTableTableManager(_$AppDatabase db, $TransactionsTable table) + $ConfigsTable, + Config, + $$ConfigsTableFilterComposer, + $$ConfigsTableOrderingComposer, + $$ConfigsTableAnnotationComposer, + $$ConfigsTableCreateCompanionBuilder, + $$ConfigsTableUpdateCompanionBuilder, + (Config, BaseReferences<_$AppDatabase, $ConfigsTable, Config>), + Config, + PrefetchHooks Function()> { + $$ConfigsTableTableManager(_$AppDatabase db, $ConfigsTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$TransactionsTableFilterComposer($db: db, $table: table), + $$ConfigsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$TransactionsTableOrderingComposer($db: db, $table: table), + $$ConfigsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$TransactionsTableAnnotationComposer($db: db, $table: table), + $$ConfigsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value userId = const Value.absent(), @@ -8142,21 +10601,12 @@ class $$TransactionsTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - Value amount = const Value.absent(), - Value type = const Value.absent(), - Value description = const Value.absent(), - Value datetime = const Value.absent(), - Value partyId = const Value.absent(), - Value walletId = const Value.absent(), - Value groupId = const Value.absent(), - Value walletClientId = const Value.absent(), - Value partyClientId = const Value.absent(), - Value groupClientId = const Value.absent(), - Value transferId = const Value.absent(), - Value transferClientId = const Value.absent(), + Value key = const Value.absent(), + Value type = const Value.absent(), + Value value = const Value.absent(), Value rowid = const Value.absent(), }) => - TransactionsCompanion( + ConfigsCompanion( id: id, userId: userId, clientId: clientId, @@ -8165,18 +10615,9 @@ class $$TransactionsTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - amount: amount, + key: key, type: type, - description: description, - datetime: datetime, - partyId: partyId, - walletId: walletId, - groupId: groupId, - walletClientId: walletClientId, - partyClientId: partyClientId, - groupClientId: groupClientId, - transferId: transferId, - transferClientId: transferClientId, + value: value, rowid: rowid, ), createCompanionCallback: ({ @@ -8188,21 +10629,12 @@ class $$TransactionsTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - required double amount, - required TransactionType type, - Value description = const Value.absent(), - Value datetime = const Value.absent(), - Value partyId = const Value.absent(), - Value walletId = const Value.absent(), - Value groupId = const Value.absent(), - required String walletClientId, - Value partyClientId = const Value.absent(), - Value groupClientId = const Value.absent(), - Value transferId = const Value.absent(), - Value transferClientId = const Value.absent(), + required String key, + required ConfigType type, + required dynamic value, Value rowid = const Value.absent(), }) => - TransactionsCompanion.insert( + ConfigsCompanion.insert( id: id, userId: userId, clientId: clientId, @@ -8211,160 +10643,55 @@ class $$TransactionsTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - amount: amount, + key: key, type: type, - description: description, - datetime: datetime, - partyId: partyId, - walletId: walletId, - groupId: groupId, - walletClientId: walletClientId, - partyClientId: partyClientId, - groupClientId: groupClientId, - transferId: transferId, - transferClientId: transferClientId, + value: value, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$TransactionsTableReferences(db, table, e) - )) + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) .toList(), - prefetchHooksCallback: ( - {walletClientId = false, - partyClientId = false, - groupClientId = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic>>(state) { - if (walletClientId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.walletClientId, - referencedTable: - $$TransactionsTableReferences._walletClientIdTable(db), - referencedColumn: $$TransactionsTableReferences - ._walletClientIdTable(db) - .clientId, - ) as T; - } - if (partyClientId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.partyClientId, - referencedTable: - $$TransactionsTableReferences._partyClientIdTable(db), - referencedColumn: $$TransactionsTableReferences - ._partyClientIdTable(db) - .clientId, - ) as T; - } - if (groupClientId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.groupClientId, - referencedTable: - $$TransactionsTableReferences._groupClientIdTable(db), - referencedColumn: $$TransactionsTableReferences - ._groupClientIdTable(db) - .clientId, - ) as T; - } - - return state; - }, - getPrefetchedDataCallback: (items) async { - return []; - }, - ); - }, + prefetchHooksCallback: null, )); } -typedef $$TransactionsTableProcessedTableManager = ProcessedTableManager< +typedef $$ConfigsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $TransactionsTable, - Transaction, - $$TransactionsTableFilterComposer, - $$TransactionsTableOrderingComposer, - $$TransactionsTableAnnotationComposer, - $$TransactionsTableCreateCompanionBuilder, - $$TransactionsTableUpdateCompanionBuilder, - (Transaction, $$TransactionsTableReferences), - Transaction, - PrefetchHooks Function( - {bool walletClientId, bool partyClientId, bool groupClientId})>; -typedef $$CategoriesTableCreateCompanionBuilder = CategoriesCompanion Function({ - Value id, - Value userId, - Value clientId, - Value rev, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value lastSyncedAt, - required String name, - required String slug, - Value description, - required TransactionType type, - Value icon, - Value rowid, + $ConfigsTable, + Config, + $$ConfigsTableFilterComposer, + $$ConfigsTableOrderingComposer, + $$ConfigsTableAnnotationComposer, + $$ConfigsTableCreateCompanionBuilder, + $$ConfigsTableUpdateCompanionBuilder, + (Config, BaseReferences<_$AppDatabase, $ConfigsTable, Config>), + Config, + PrefetchHooks Function()>; +typedef $$UsersTableCreateCompanionBuilder = UsersCompanion Function({ + Value id, + required String email, + required String firstName, + Value lastName, + Value username, + Value phone, + Value avatar, + Value createdAt, + Value updatedAt, }); -typedef $$CategoriesTableUpdateCompanionBuilder = CategoriesCompanion Function({ - Value id, - Value userId, - Value clientId, - Value rev, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value lastSyncedAt, - Value name, - Value slug, - Value description, - Value type, - Value icon, - Value rowid, +typedef $$UsersTableUpdateCompanionBuilder = UsersCompanion Function({ + Value id, + Value email, + Value firstName, + Value lastName, + Value username, + Value phone, + Value avatar, + Value createdAt, + Value updatedAt, }); -final class $$CategoriesTableReferences - extends BaseReferences<_$AppDatabase, $CategoriesTable, Category> { - $$CategoriesTableReferences(super.$_db, super.$_table, super.$_typedResult); - - static MultiTypedResultKey<$CategorizablesTable, List> - _categorizablesRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable(db.categorizables, - aliasName: $_aliasNameGenerator( - db.categories.clientId, db.categorizables.categoryClientId)); - - $$CategorizablesTableProcessedTableManager get categorizablesRefs { - final manager = $$CategorizablesTableTableManager($_db, $_db.categorizables) - .filter((f) => f.categoryClientId.clientId - .sqlEquals($_itemColumn('client_id')!)); - - final cache = $_typedResult.readTableOrNull(_categorizablesRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } -} - -class $$CategoriesTableFilterComposer - extends Composer<_$AppDatabase, $CategoriesTable> { - $$CategoriesTableFilterComposer({ +class $$UsersTableFilterComposer extends Composer<_$AppDatabase, $UsersTable> { + $$UsersTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -8374,71 +10701,34 @@ class $$CategoriesTableFilterComposer ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); + ColumnFilters get email => $composableBuilder( + column: $table.email, builder: (column) => ColumnFilters(column)); - ColumnFilters get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnFilters(column)); + ColumnFilters get firstName => $composableBuilder( + column: $table.firstName, builder: (column) => ColumnFilters(column)); - ColumnFilters get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastName => $composableBuilder( + column: $table.lastName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get username => $composableBuilder( + column: $table.username, builder: (column) => ColumnFilters(column)); + + ColumnFilters get phone => $composableBuilder( + column: $table.phone, builder: (column) => ColumnFilters(column)); + + ColumnFilters get avatar => $composableBuilder( + column: $table.avatar, builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); ColumnFilters get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); - - ColumnFilters get slug => $composableBuilder( - column: $table.slug, builder: (column) => ColumnFilters(column)); - - ColumnFilters get description => $composableBuilder( - column: $table.description, builder: (column) => ColumnFilters(column)); - - ColumnWithTypeConverterFilters - get type => $composableBuilder( - column: $table.type, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - ColumnWithTypeConverterFilters get icon => - $composableBuilder( - column: $table.icon, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - Expression categorizablesRefs( - Expression Function($$CategorizablesTableFilterComposer f) f) { - final $$CategorizablesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.clientId, - referencedTable: $db.categorizables, - getReferencedColumn: (t) => t.categoryClientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$CategorizablesTableFilterComposer( - $db: $db, - $table: $db.categorizables, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } } -class $$CategoriesTableOrderingComposer - extends Composer<_$AppDatabase, $CategoriesTable> { - $$CategoriesTableOrderingComposer({ +class $$UsersTableOrderingComposer + extends Composer<_$AppDatabase, $UsersTable> { + $$UsersTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -8448,47 +10738,34 @@ class $$CategoriesTableOrderingComposer ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get email => $composableBuilder( + column: $table.email, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get firstName => $composableBuilder( + column: $table.firstName, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastName => $composableBuilder( + column: $table.lastName, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get username => $composableBuilder( + column: $table.username, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get phone => $composableBuilder( + column: $table.phone, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get avatar => $composableBuilder( + column: $table.avatar, builder: (column) => ColumnOrderings(column)); ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); ColumnOrderings get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get slug => $composableBuilder( - column: $table.slug, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get description => $composableBuilder( - column: $table.description, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get icon => $composableBuilder( - column: $table.icon, builder: (column) => ColumnOrderings(column)); } -class $$CategoriesTableAnnotationComposer - extends Composer<_$AppDatabase, $CategoriesTable> { - $$CategoriesTableAnnotationComposer({ +class $$UsersTableAnnotationComposer + extends Composer<_$AppDatabase, $UsersTable> { + $$UsersTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -8498,436 +10775,345 @@ class $$CategoriesTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumn get email => + $composableBuilder(column: $table.email, builder: (column) => column); - GeneratedColumn get clientId => - $composableBuilder(column: $table.clientId, builder: (column) => column); + GeneratedColumn get firstName => + $composableBuilder(column: $table.firstName, builder: (column) => column); - GeneratedColumn get rev => - $composableBuilder(column: $table.rev, builder: (column) => column); + GeneratedColumn get lastName => + $composableBuilder(column: $table.lastName, builder: (column) => column); + + GeneratedColumn get username => + $composableBuilder(column: $table.username, builder: (column) => column); + + GeneratedColumn get phone => + $composableBuilder(column: $table.phone, builder: (column) => column); + + GeneratedColumn get avatar => + $composableBuilder(column: $table.avatar, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - - GeneratedColumn get deletedAt => - $composableBuilder(column: $table.deletedAt, builder: (column) => column); - - GeneratedColumn get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, builder: (column) => column); - - GeneratedColumn get name => - $composableBuilder(column: $table.name, builder: (column) => column); - - GeneratedColumn get slug => - $composableBuilder(column: $table.slug, builder: (column) => column); - - GeneratedColumn get description => $composableBuilder( - column: $table.description, builder: (column) => column); - - GeneratedColumnWithTypeConverter get type => - $composableBuilder(column: $table.type, builder: (column) => column); - - GeneratedColumnWithTypeConverter get icon => - $composableBuilder(column: $table.icon, builder: (column) => column); - - Expression categorizablesRefs( - Expression Function($$CategorizablesTableAnnotationComposer a) f) { - final $$CategorizablesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.clientId, - referencedTable: $db.categorizables, - getReferencedColumn: (t) => t.categoryClientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$CategorizablesTableAnnotationComposer( - $db: $db, - $table: $db.categorizables, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } } -class $$CategoriesTableTableManager extends RootTableManager< +class $$UsersTableTableManager extends RootTableManager< _$AppDatabase, - $CategoriesTable, - Category, - $$CategoriesTableFilterComposer, - $$CategoriesTableOrderingComposer, - $$CategoriesTableAnnotationComposer, - $$CategoriesTableCreateCompanionBuilder, - $$CategoriesTableUpdateCompanionBuilder, - (Category, $$CategoriesTableReferences), - Category, - PrefetchHooks Function({bool categorizablesRefs})> { - $$CategoriesTableTableManager(_$AppDatabase db, $CategoriesTable table) + $UsersTable, + User, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (User, BaseReferences<_$AppDatabase, $UsersTable, User>), + User, + PrefetchHooks Function()> { + $$UsersTableTableManager(_$AppDatabase db, $UsersTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$CategoriesTableFilterComposer($db: db, $table: table), + $$UsersTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$CategoriesTableOrderingComposer($db: db, $table: table), + $$UsersTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$CategoriesTableAnnotationComposer($db: db, $table: table), + $$UsersTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value id = const Value.absent(), - Value userId = const Value.absent(), - Value clientId = const Value.absent(), - Value rev = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value deletedAt = const Value.absent(), - Value lastSyncedAt = const Value.absent(), - Value name = const Value.absent(), - Value slug = const Value.absent(), - Value description = const Value.absent(), - Value type = const Value.absent(), - Value icon = const Value.absent(), - Value rowid = const Value.absent(), + Value id = const Value.absent(), + Value email = const Value.absent(), + Value firstName = const Value.absent(), + Value lastName = const Value.absent(), + Value username = const Value.absent(), + Value phone = const Value.absent(), + Value avatar = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), }) => - CategoriesCompanion( + UsersCompanion( id: id, - userId: userId, - clientId: clientId, - rev: rev, + email: email, + firstName: firstName, + lastName: lastName, + username: username, + phone: phone, + avatar: avatar, createdAt: createdAt, updatedAt: updatedAt, - deletedAt: deletedAt, - lastSyncedAt: lastSyncedAt, - name: name, - slug: slug, - description: description, - type: type, - icon: icon, - rowid: rowid, ), createCompanionCallback: ({ - Value id = const Value.absent(), - Value userId = const Value.absent(), - Value clientId = const Value.absent(), - Value rev = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value deletedAt = const Value.absent(), - Value lastSyncedAt = const Value.absent(), - required String name, - required String slug, - Value description = const Value.absent(), - required TransactionType type, - Value icon = const Value.absent(), - Value rowid = const Value.absent(), + Value id = const Value.absent(), + required String email, + required String firstName, + Value lastName = const Value.absent(), + Value username = const Value.absent(), + Value phone = const Value.absent(), + Value avatar = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), }) => - CategoriesCompanion.insert( + UsersCompanion.insert( id: id, - userId: userId, - clientId: clientId, - rev: rev, - createdAt: createdAt, - updatedAt: updatedAt, - deletedAt: deletedAt, - lastSyncedAt: lastSyncedAt, - name: name, - slug: slug, - description: description, - type: type, - icon: icon, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$CategoriesTableReferences(db, table, e) - )) - .toList(), - prefetchHooksCallback: ({categorizablesRefs = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [ - if (categorizablesRefs) db.categorizables - ], - addJoins: null, - getPrefetchedDataCallback: (items) async { - return [ - if (categorizablesRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: $$CategoriesTableReferences - ._categorizablesRefsTable(db), - managerFromTypedResult: (p0) => - $$CategoriesTableReferences(db, table, p0) - .categorizablesRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.categoryClientId == item.clientId), - typedResults: items) - ]; - }, - ); - }, + email: email, + firstName: firstName, + lastName: lastName, + username: username, + phone: phone, + avatar: avatar, + createdAt: createdAt, + updatedAt: updatedAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, )); } -typedef $$CategoriesTableProcessedTableManager = ProcessedTableManager< +typedef $$UsersTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $CategoriesTable, - Category, - $$CategoriesTableFilterComposer, - $$CategoriesTableOrderingComposer, - $$CategoriesTableAnnotationComposer, - $$CategoriesTableCreateCompanionBuilder, - $$CategoriesTableUpdateCompanionBuilder, - (Category, $$CategoriesTableReferences), - Category, - PrefetchHooks Function({bool categorizablesRefs})>; -typedef $$ConfigsTableCreateCompanionBuilder = ConfigsCompanion Function({ - Value id, - Value userId, - Value clientId, - Value rev, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value lastSyncedAt, - required String key, - required ConfigType type, - required dynamic value, + $UsersTable, + User, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (User, BaseReferences<_$AppDatabase, $UsersTable, User>), + User, + PrefetchHooks Function()>; +typedef $$LocalChangesTableCreateCompanionBuilder = LocalChangesCompanion + Function({ + required String entityType, + required String entityId, + required String entityRev, + required bool deleted, + required Map data, + required DateTime createAt, + required bool concluded, + Value concludedMoment, + Value error, + required bool dismissed, Value rowid, }); -typedef $$ConfigsTableUpdateCompanionBuilder = ConfigsCompanion Function({ - Value id, - Value userId, - Value clientId, - Value rev, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value lastSyncedAt, - Value key, - Value type, - Value value, +typedef $$LocalChangesTableUpdateCompanionBuilder = LocalChangesCompanion + Function({ + Value entityType, + Value entityId, + Value entityRev, + Value deleted, + Value> data, + Value createAt, + Value concluded, + Value concludedMoment, + Value error, + Value dismissed, Value rowid, }); -class $$ConfigsTableFilterComposer - extends Composer<_$AppDatabase, $ConfigsTable> { - $$ConfigsTableFilterComposer({ +class $$LocalChangesTableFilterComposer + extends Composer<_$AppDatabase, $LocalChangesTable> { + $$LocalChangesTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); + ColumnFilters get entityType => $composableBuilder( + column: $table.entityType, builder: (column) => ColumnFilters(column)); - ColumnFilters get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnFilters(column)); + ColumnFilters get entityId => $composableBuilder( + column: $table.entityId, builder: (column) => ColumnFilters(column)); - ColumnFilters get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnFilters(column)); + ColumnFilters get entityRev => $composableBuilder( + column: $table.entityRev, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get deleted => $composableBuilder( + column: $table.deleted, builder: (column) => ColumnFilters(column)); - ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters, Map, + String> + get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get createAt => $composableBuilder( + column: $table.createAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get concluded => $composableBuilder( + column: $table.concluded, builder: (column) => ColumnFilters(column)); - ColumnFilters get key => $composableBuilder( - column: $table.key, builder: (column) => ColumnFilters(column)); + ColumnFilters get concludedMoment => $composableBuilder( + column: $table.concludedMoment, + builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get type => - $composableBuilder( - column: $table.type, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get error => $composableBuilder( + column: $table.error, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get value => - $composableBuilder( - column: $table.value, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get dismissed => $composableBuilder( + column: $table.dismissed, builder: (column) => ColumnFilters(column)); } -class $$ConfigsTableOrderingComposer - extends Composer<_$AppDatabase, $ConfigsTable> { - $$ConfigsTableOrderingComposer({ +class $$LocalChangesTableOrderingComposer + extends Composer<_$AppDatabase, $LocalChangesTable> { + $$LocalChangesTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get entityType => $composableBuilder( + column: $table.entityType, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get entityId => $composableBuilder( + column: $table.entityId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get entityRev => $composableBuilder( + column: $table.entityRev, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deleted => $composableBuilder( + column: $table.deleted, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get data => $composableBuilder( + column: $table.data, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createAt => $composableBuilder( + column: $table.createAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get concluded => $composableBuilder( + column: $table.concluded, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, + ColumnOrderings get concludedMoment => $composableBuilder( + column: $table.concludedMoment, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get key => $composableBuilder( - column: $table.key, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get error => $composableBuilder( + column: $table.error, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get value => $composableBuilder( - column: $table.value, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get dismissed => $composableBuilder( + column: $table.dismissed, builder: (column) => ColumnOrderings(column)); } -class $$ConfigsTableAnnotationComposer - extends Composer<_$AppDatabase, $ConfigsTable> { - $$ConfigsTableAnnotationComposer({ +class $$LocalChangesTableAnnotationComposer + extends Composer<_$AppDatabase, $LocalChangesTable> { + $$LocalChangesTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumn get entityType => $composableBuilder( + column: $table.entityType, builder: (column) => column); - GeneratedColumn get clientId => - $composableBuilder(column: $table.clientId, builder: (column) => column); + GeneratedColumn get entityId => + $composableBuilder(column: $table.entityId, builder: (column) => column); - GeneratedColumn get rev => - $composableBuilder(column: $table.rev, builder: (column) => column); + GeneratedColumn get entityRev => + $composableBuilder(column: $table.entityRev, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get deleted => + $composableBuilder(column: $table.deleted, builder: (column) => column); - GeneratedColumn get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumnWithTypeConverter, String> get data => + $composableBuilder(column: $table.data, builder: (column) => column); - GeneratedColumn get deletedAt => - $composableBuilder(column: $table.deletedAt, builder: (column) => column); + GeneratedColumn get createAt => + $composableBuilder(column: $table.createAt, builder: (column) => column); - GeneratedColumn get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, builder: (column) => column); + GeneratedColumn get concluded => + $composableBuilder(column: $table.concluded, builder: (column) => column); - GeneratedColumn get key => - $composableBuilder(column: $table.key, builder: (column) => column); + GeneratedColumn get concludedMoment => $composableBuilder( + column: $table.concludedMoment, builder: (column) => column); - GeneratedColumnWithTypeConverter get type => - $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get error => + $composableBuilder(column: $table.error, builder: (column) => column); - GeneratedColumnWithTypeConverter get value => - $composableBuilder(column: $table.value, builder: (column) => column); + GeneratedColumn get dismissed => + $composableBuilder(column: $table.dismissed, builder: (column) => column); } -class $$ConfigsTableTableManager extends RootTableManager< +class $$LocalChangesTableTableManager extends RootTableManager< _$AppDatabase, - $ConfigsTable, - Config, - $$ConfigsTableFilterComposer, - $$ConfigsTableOrderingComposer, - $$ConfigsTableAnnotationComposer, - $$ConfigsTableCreateCompanionBuilder, - $$ConfigsTableUpdateCompanionBuilder, - (Config, BaseReferences<_$AppDatabase, $ConfigsTable, Config>), - Config, + $LocalChangesTable, + LocalChange, + $$LocalChangesTableFilterComposer, + $$LocalChangesTableOrderingComposer, + $$LocalChangesTableAnnotationComposer, + $$LocalChangesTableCreateCompanionBuilder, + $$LocalChangesTableUpdateCompanionBuilder, + ( + LocalChange, + BaseReferences<_$AppDatabase, $LocalChangesTable, LocalChange> + ), + LocalChange, PrefetchHooks Function()> { - $$ConfigsTableTableManager(_$AppDatabase db, $ConfigsTable table) + $$LocalChangesTableTableManager(_$AppDatabase db, $LocalChangesTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$ConfigsTableFilterComposer($db: db, $table: table), + $$LocalChangesTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$ConfigsTableOrderingComposer($db: db, $table: table), + $$LocalChangesTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$ConfigsTableAnnotationComposer($db: db, $table: table), + $$LocalChangesTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value id = const Value.absent(), - Value userId = const Value.absent(), - Value clientId = const Value.absent(), - Value rev = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value deletedAt = const Value.absent(), - Value lastSyncedAt = const Value.absent(), - Value key = const Value.absent(), - Value type = const Value.absent(), - Value value = const Value.absent(), + Value entityType = const Value.absent(), + Value entityId = const Value.absent(), + Value entityRev = const Value.absent(), + Value deleted = const Value.absent(), + Value> data = const Value.absent(), + Value createAt = const Value.absent(), + Value concluded = const Value.absent(), + Value concludedMoment = const Value.absent(), + Value error = const Value.absent(), + Value dismissed = const Value.absent(), Value rowid = const Value.absent(), }) => - ConfigsCompanion( - id: id, - userId: userId, - clientId: clientId, - rev: rev, - createdAt: createdAt, - updatedAt: updatedAt, - deletedAt: deletedAt, - lastSyncedAt: lastSyncedAt, - key: key, - type: type, - value: value, + LocalChangesCompanion( + entityType: entityType, + entityId: entityId, + entityRev: entityRev, + deleted: deleted, + data: data, + createAt: createAt, + concluded: concluded, + concludedMoment: concludedMoment, + error: error, + dismissed: dismissed, rowid: rowid, ), createCompanionCallback: ({ - Value id = const Value.absent(), - Value userId = const Value.absent(), - Value clientId = const Value.absent(), - Value rev = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value deletedAt = const Value.absent(), - Value lastSyncedAt = const Value.absent(), - required String key, - required ConfigType type, - required dynamic value, + required String entityType, + required String entityId, + required String entityRev, + required bool deleted, + required Map data, + required DateTime createAt, + required bool concluded, + Value concludedMoment = const Value.absent(), + Value error = const Value.absent(), + required bool dismissed, Value rowid = const Value.absent(), }) => - ConfigsCompanion.insert( - id: id, - userId: userId, - clientId: clientId, - rev: rev, - createdAt: createdAt, - updatedAt: updatedAt, - deletedAt: deletedAt, - lastSyncedAt: lastSyncedAt, - key: key, - type: type, - value: value, + LocalChangesCompanion.insert( + entityType: entityType, + entityId: entityId, + entityRev: entityRev, + deleted: deleted, + data: data, + createAt: createAt, + concluded: concluded, + concludedMoment: concludedMoment, + error: error, + dismissed: dismissed, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -8937,216 +11123,127 @@ class $$ConfigsTableTableManager extends RootTableManager< )); } -typedef $$ConfigsTableProcessedTableManager = ProcessedTableManager< +typedef $$LocalChangesTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $ConfigsTable, - Config, - $$ConfigsTableFilterComposer, - $$ConfigsTableOrderingComposer, - $$ConfigsTableAnnotationComposer, - $$ConfigsTableCreateCompanionBuilder, - $$ConfigsTableUpdateCompanionBuilder, - (Config, BaseReferences<_$AppDatabase, $ConfigsTable, Config>), - Config, + $LocalChangesTable, + LocalChange, + $$LocalChangesTableFilterComposer, + $$LocalChangesTableOrderingComposer, + $$LocalChangesTableAnnotationComposer, + $$LocalChangesTableCreateCompanionBuilder, + $$LocalChangesTableUpdateCompanionBuilder, + ( + LocalChange, + BaseReferences<_$AppDatabase, $LocalChangesTable, LocalChange> + ), + LocalChange, PrefetchHooks Function()>; -typedef $$UsersTableCreateCompanionBuilder = UsersCompanion Function({ - Value id, - required String email, - required String firstName, - Value lastName, - Value username, - Value phone, - Value avatar, - Value createdAt, - Value updatedAt, +typedef $$SyncMetadataTableCreateCompanionBuilder = SyncMetadataCompanion + Function({ + required String entityType, + Value lastSyncedAt, + Value rowid, }); -typedef $$UsersTableUpdateCompanionBuilder = UsersCompanion Function({ - Value id, - Value email, - Value firstName, - Value lastName, - Value username, - Value phone, - Value avatar, - Value createdAt, - Value updatedAt, +typedef $$SyncMetadataTableUpdateCompanionBuilder = SyncMetadataCompanion + Function({ + Value entityType, + Value lastSyncedAt, + Value rowid, }); -class $$UsersTableFilterComposer extends Composer<_$AppDatabase, $UsersTable> { - $$UsersTableFilterComposer({ +class $$SyncMetadataTableFilterComposer + extends Composer<_$AppDatabase, $SyncMetadataTable> { + $$SyncMetadataTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get email => $composableBuilder( - column: $table.email, builder: (column) => ColumnFilters(column)); - - ColumnFilters get firstName => $composableBuilder( - column: $table.firstName, builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastName => $composableBuilder( - column: $table.lastName, builder: (column) => ColumnFilters(column)); - - ColumnFilters get username => $composableBuilder( - column: $table.username, builder: (column) => ColumnFilters(column)); - - ColumnFilters get phone => $composableBuilder( - column: $table.phone, builder: (column) => ColumnFilters(column)); - - ColumnFilters get avatar => $composableBuilder( - column: $table.avatar, builder: (column) => ColumnFilters(column)); - - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get entityType => $composableBuilder( + column: $table.entityType, builder: (column) => ColumnFilters(column)); - ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); } -class $$UsersTableOrderingComposer - extends Composer<_$AppDatabase, $UsersTable> { - $$UsersTableOrderingComposer({ +class $$SyncMetadataTableOrderingComposer + extends Composer<_$AppDatabase, $SyncMetadataTable> { + $$SyncMetadataTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get email => $composableBuilder( - column: $table.email, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get firstName => $composableBuilder( - column: $table.firstName, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get lastName => $composableBuilder( - column: $table.lastName, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get username => $composableBuilder( - column: $table.username, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get phone => $composableBuilder( - column: $table.phone, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get avatar => $composableBuilder( - column: $table.avatar, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get entityType => $composableBuilder( + column: $table.entityType, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => ColumnOrderings(column)); } -class $$UsersTableAnnotationComposer - extends Composer<_$AppDatabase, $UsersTable> { - $$UsersTableAnnotationComposer({ +class $$SyncMetadataTableAnnotationComposer + extends Composer<_$AppDatabase, $SyncMetadataTable> { + $$SyncMetadataTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get email => - $composableBuilder(column: $table.email, builder: (column) => column); - - GeneratedColumn get firstName => - $composableBuilder(column: $table.firstName, builder: (column) => column); - - GeneratedColumn get lastName => - $composableBuilder(column: $table.lastName, builder: (column) => column); - - GeneratedColumn get username => - $composableBuilder(column: $table.username, builder: (column) => column); - - GeneratedColumn get phone => - $composableBuilder(column: $table.phone, builder: (column) => column); - - GeneratedColumn get avatar => - $composableBuilder(column: $table.avatar, builder: (column) => column); - - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get entityType => $composableBuilder( + column: $table.entityType, builder: (column) => column); - GeneratedColumn get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => column); } -class $$UsersTableTableManager extends RootTableManager< +class $$SyncMetadataTableTableManager extends RootTableManager< _$AppDatabase, - $UsersTable, - User, - $$UsersTableFilterComposer, - $$UsersTableOrderingComposer, - $$UsersTableAnnotationComposer, - $$UsersTableCreateCompanionBuilder, - $$UsersTableUpdateCompanionBuilder, - (User, BaseReferences<_$AppDatabase, $UsersTable, User>), - User, + $SyncMetadataTable, + SyncMetadatas, + $$SyncMetadataTableFilterComposer, + $$SyncMetadataTableOrderingComposer, + $$SyncMetadataTableAnnotationComposer, + $$SyncMetadataTableCreateCompanionBuilder, + $$SyncMetadataTableUpdateCompanionBuilder, + ( + SyncMetadatas, + BaseReferences<_$AppDatabase, $SyncMetadataTable, SyncMetadatas> + ), + SyncMetadatas, PrefetchHooks Function()> { - $$UsersTableTableManager(_$AppDatabase db, $UsersTable table) + $$SyncMetadataTableTableManager(_$AppDatabase db, $SyncMetadataTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$UsersTableFilterComposer($db: db, $table: table), + $$SyncMetadataTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$UsersTableOrderingComposer($db: db, $table: table), + $$SyncMetadataTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$UsersTableAnnotationComposer($db: db, $table: table), + $$SyncMetadataTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value id = const Value.absent(), - Value email = const Value.absent(), - Value firstName = const Value.absent(), - Value lastName = const Value.absent(), - Value username = const Value.absent(), - Value phone = const Value.absent(), - Value avatar = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), + Value entityType = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + Value rowid = const Value.absent(), }) => - UsersCompanion( - id: id, - email: email, - firstName: firstName, - lastName: lastName, - username: username, - phone: phone, - avatar: avatar, - createdAt: createdAt, - updatedAt: updatedAt, + SyncMetadataCompanion( + entityType: entityType, + lastSyncedAt: lastSyncedAt, + rowid: rowid, ), createCompanionCallback: ({ - Value id = const Value.absent(), - required String email, - required String firstName, - Value lastName = const Value.absent(), - Value username = const Value.absent(), - Value phone = const Value.absent(), - Value avatar = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), + required String entityType, + Value lastSyncedAt = const Value.absent(), + Value rowid = const Value.absent(), }) => - UsersCompanion.insert( - id: id, - email: email, - firstName: firstName, - lastName: lastName, - username: username, - phone: phone, - avatar: avatar, - createdAt: createdAt, - updatedAt: updatedAt, + SyncMetadataCompanion.insert( + entityType: entityType, + lastSyncedAt: lastSyncedAt, + rowid: rowid, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), BaseReferences(db, table, e))) @@ -9155,376 +11252,550 @@ class $$UsersTableTableManager extends RootTableManager< )); } -typedef $$UsersTableProcessedTableManager = ProcessedTableManager< +typedef $$SyncMetadataTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $UsersTable, - User, - $$UsersTableFilterComposer, - $$UsersTableOrderingComposer, - $$UsersTableAnnotationComposer, - $$UsersTableCreateCompanionBuilder, - $$UsersTableUpdateCompanionBuilder, - (User, BaseReferences<_$AppDatabase, $UsersTable, User>), - User, + $SyncMetadataTable, + SyncMetadatas, + $$SyncMetadataTableFilterComposer, + $$SyncMetadataTableOrderingComposer, + $$SyncMetadataTableAnnotationComposer, + $$SyncMetadataTableCreateCompanionBuilder, + $$SyncMetadataTableUpdateCompanionBuilder, + ( + SyncMetadatas, + BaseReferences<_$AppDatabase, $SyncMetadataTable, SyncMetadatas> + ), + SyncMetadatas, PrefetchHooks Function()>; -typedef $$LocalChangesTableCreateCompanionBuilder = LocalChangesCompanion +typedef $$CategorizablesTableCreateCompanionBuilder = CategorizablesCompanion Function({ - required String entityType, - required String entityId, - required String entityRev, - required bool deleted, - required Map data, - required DateTime createAt, - required bool concluded, - Value concludedMoment, - Value error, - required bool dismissed, + required String categorizableId, + required CategorizableType categorizableType, + required String categoryClientId, Value rowid, }); -typedef $$LocalChangesTableUpdateCompanionBuilder = LocalChangesCompanion +typedef $$CategorizablesTableUpdateCompanionBuilder = CategorizablesCompanion Function({ - Value entityType, - Value entityId, - Value entityRev, - Value deleted, - Value> data, - Value createAt, - Value concluded, - Value concludedMoment, - Value error, - Value dismissed, + Value categorizableId, + Value categorizableType, + Value categoryClientId, Value rowid, }); -class $$LocalChangesTableFilterComposer - extends Composer<_$AppDatabase, $LocalChangesTable> { - $$LocalChangesTableFilterComposer({ +final class $$CategorizablesTableReferences + extends BaseReferences<_$AppDatabase, $CategorizablesTable, Categorizable> { + $$CategorizablesTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $CategoriesTable _categoryClientIdTable(_$AppDatabase db) => + db.categories.createAlias($_aliasNameGenerator( + db.categorizables.categoryClientId, db.categories.clientId)); + + $$CategoriesTableProcessedTableManager get categoryClientId { + final $_column = $_itemColumn('category_client_id')!; + + final manager = $$CategoriesTableTableManager($_db, $_db.categories) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_categoryClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$CategorizablesTableFilterComposer + extends Composer<_$AppDatabase, $CategorizablesTable> { + $$CategorizablesTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get entityType => $composableBuilder( - column: $table.entityType, builder: (column) => ColumnFilters(column)); - - ColumnFilters get entityId => $composableBuilder( - column: $table.entityId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get entityRev => $composableBuilder( - column: $table.entityRev, builder: (column) => ColumnFilters(column)); - - ColumnFilters get deleted => $composableBuilder( - column: $table.deleted, builder: (column) => ColumnFilters(column)); - - ColumnWithTypeConverterFilters, Map, - String> - get data => $composableBuilder( - column: $table.data, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - ColumnFilters get createAt => $composableBuilder( - column: $table.createAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get concluded => $composableBuilder( - column: $table.concluded, builder: (column) => ColumnFilters(column)); - - ColumnFilters get concludedMoment => $composableBuilder( - column: $table.concludedMoment, + ColumnFilters get categorizableId => $composableBuilder( + column: $table.categorizableId, builder: (column) => ColumnFilters(column)); - ColumnFilters get error => $composableBuilder( - column: $table.error, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters + get categorizableType => $composableBuilder( + column: $table.categorizableType, + builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get dismissed => $composableBuilder( - column: $table.dismissed, builder: (column) => ColumnFilters(column)); + $$CategoriesTableFilterComposer get categoryClientId { + final $$CategoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.categoryClientId, + referencedTable: $db.categories, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$CategoriesTableFilterComposer( + $db: $db, + $table: $db.categories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } -class $$LocalChangesTableOrderingComposer - extends Composer<_$AppDatabase, $LocalChangesTable> { - $$LocalChangesTableOrderingComposer({ +class $$CategorizablesTableOrderingComposer + extends Composer<_$AppDatabase, $CategorizablesTable> { + $$CategorizablesTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get entityType => $composableBuilder( - column: $table.entityType, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get entityId => $composableBuilder( - column: $table.entityId, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get entityRev => $composableBuilder( - column: $table.entityRev, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get deleted => $composableBuilder( - column: $table.deleted, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get createAt => $composableBuilder( - column: $table.createAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get concluded => $composableBuilder( - column: $table.concluded, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get concludedMoment => $composableBuilder( - column: $table.concludedMoment, + ColumnOrderings get categorizableId => $composableBuilder( + column: $table.categorizableId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get error => $composableBuilder( - column: $table.error, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get categorizableType => $composableBuilder( + column: $table.categorizableType, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get dismissed => $composableBuilder( - column: $table.dismissed, builder: (column) => ColumnOrderings(column)); + $$CategoriesTableOrderingComposer get categoryClientId { + final $$CategoriesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.categoryClientId, + referencedTable: $db.categories, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$CategoriesTableOrderingComposer( + $db: $db, + $table: $db.categories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } -class $$LocalChangesTableAnnotationComposer - extends Composer<_$AppDatabase, $LocalChangesTable> { - $$LocalChangesTableAnnotationComposer({ +class $$CategorizablesTableAnnotationComposer + extends Composer<_$AppDatabase, $CategorizablesTable> { + $$CategorizablesTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get entityType => $composableBuilder( - column: $table.entityType, builder: (column) => column); - - GeneratedColumn get entityId => - $composableBuilder(column: $table.entityId, builder: (column) => column); - - GeneratedColumn get entityRev => - $composableBuilder(column: $table.entityRev, builder: (column) => column); - - GeneratedColumn get deleted => - $composableBuilder(column: $table.deleted, builder: (column) => column); - - GeneratedColumnWithTypeConverter, String> get data => - $composableBuilder(column: $table.data, builder: (column) => column); - - GeneratedColumn get createAt => - $composableBuilder(column: $table.createAt, builder: (column) => column); - - GeneratedColumn get concluded => - $composableBuilder(column: $table.concluded, builder: (column) => column); - - GeneratedColumn get concludedMoment => $composableBuilder( - column: $table.concludedMoment, builder: (column) => column); + GeneratedColumn get categorizableId => $composableBuilder( + column: $table.categorizableId, builder: (column) => column); - GeneratedColumn get error => - $composableBuilder(column: $table.error, builder: (column) => column); + GeneratedColumnWithTypeConverter + get categorizableType => $composableBuilder( + column: $table.categorizableType, builder: (column) => column); - GeneratedColumn get dismissed => - $composableBuilder(column: $table.dismissed, builder: (column) => column); + $$CategoriesTableAnnotationComposer get categoryClientId { + final $$CategoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.categoryClientId, + referencedTable: $db.categories, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$CategoriesTableAnnotationComposer( + $db: $db, + $table: $db.categories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } -class $$LocalChangesTableTableManager extends RootTableManager< +class $$CategorizablesTableTableManager extends RootTableManager< _$AppDatabase, - $LocalChangesTable, - LocalChange, - $$LocalChangesTableFilterComposer, - $$LocalChangesTableOrderingComposer, - $$LocalChangesTableAnnotationComposer, - $$LocalChangesTableCreateCompanionBuilder, - $$LocalChangesTableUpdateCompanionBuilder, - ( - LocalChange, - BaseReferences<_$AppDatabase, $LocalChangesTable, LocalChange> - ), - LocalChange, - PrefetchHooks Function()> { - $$LocalChangesTableTableManager(_$AppDatabase db, $LocalChangesTable table) + $CategorizablesTable, + Categorizable, + $$CategorizablesTableFilterComposer, + $$CategorizablesTableOrderingComposer, + $$CategorizablesTableAnnotationComposer, + $$CategorizablesTableCreateCompanionBuilder, + $$CategorizablesTableUpdateCompanionBuilder, + (Categorizable, $$CategorizablesTableReferences), + Categorizable, + PrefetchHooks Function({bool categoryClientId})> { + $$CategorizablesTableTableManager( + _$AppDatabase db, $CategorizablesTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$LocalChangesTableFilterComposer($db: db, $table: table), + $$CategorizablesTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$LocalChangesTableOrderingComposer($db: db, $table: table), + $$CategorizablesTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$LocalChangesTableAnnotationComposer($db: db, $table: table), + $$CategorizablesTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value entityType = const Value.absent(), - Value entityId = const Value.absent(), - Value entityRev = const Value.absent(), - Value deleted = const Value.absent(), - Value> data = const Value.absent(), - Value createAt = const Value.absent(), - Value concluded = const Value.absent(), - Value concludedMoment = const Value.absent(), - Value error = const Value.absent(), - Value dismissed = const Value.absent(), + Value categorizableId = const Value.absent(), + Value categorizableType = const Value.absent(), + Value categoryClientId = const Value.absent(), Value rowid = const Value.absent(), }) => - LocalChangesCompanion( - entityType: entityType, - entityId: entityId, - entityRev: entityRev, - deleted: deleted, - data: data, - createAt: createAt, - concluded: concluded, - concludedMoment: concludedMoment, - error: error, - dismissed: dismissed, + CategorizablesCompanion( + categorizableId: categorizableId, + categorizableType: categorizableType, + categoryClientId: categoryClientId, rowid: rowid, ), createCompanionCallback: ({ - required String entityType, - required String entityId, - required String entityRev, - required bool deleted, - required Map data, - required DateTime createAt, - required bool concluded, - Value concludedMoment = const Value.absent(), - Value error = const Value.absent(), - required bool dismissed, + required String categorizableId, + required CategorizableType categorizableType, + required String categoryClientId, Value rowid = const Value.absent(), }) => - LocalChangesCompanion.insert( - entityType: entityType, - entityId: entityId, - entityRev: entityRev, - deleted: deleted, - data: data, - createAt: createAt, - concluded: concluded, - concludedMoment: concludedMoment, - error: error, - dismissed: dismissed, + CategorizablesCompanion.insert( + categorizableId: categorizableId, + categorizableType: categorizableType, + categoryClientId: categoryClientId, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .map((e) => ( + e.readTable(table), + $$CategorizablesTableReferences(db, table, e) + )) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({categoryClientId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (categoryClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.categoryClientId, + referencedTable: $$CategorizablesTableReferences + ._categoryClientIdTable(db), + referencedColumn: $$CategorizablesTableReferences + ._categoryClientIdTable(db) + .clientId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, )); } -typedef $$LocalChangesTableProcessedTableManager = ProcessedTableManager< +typedef $$CategorizablesTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $LocalChangesTable, - LocalChange, - $$LocalChangesTableFilterComposer, - $$LocalChangesTableOrderingComposer, - $$LocalChangesTableAnnotationComposer, - $$LocalChangesTableCreateCompanionBuilder, - $$LocalChangesTableUpdateCompanionBuilder, - ( - LocalChange, - BaseReferences<_$AppDatabase, $LocalChangesTable, LocalChange> - ), - LocalChange, - PrefetchHooks Function()>; -typedef $$SyncMetadataTableCreateCompanionBuilder = SyncMetadataCompanion + $CategorizablesTable, + Categorizable, + $$CategorizablesTableFilterComposer, + $$CategorizablesTableOrderingComposer, + $$CategorizablesTableAnnotationComposer, + $$CategorizablesTableCreateCompanionBuilder, + $$CategorizablesTableUpdateCompanionBuilder, + (Categorizable, $$CategorizablesTableReferences), + Categorizable, + PrefetchHooks Function({bool categoryClientId})>; +typedef $$NotificationsTableCreateCompanionBuilder = NotificationsCompanion Function({ - required String entityType, + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, Value lastSyncedAt, + required NotificationType type, + required String title, + required String body, + required Map data, + Value readAt, Value rowid, }); -typedef $$SyncMetadataTableUpdateCompanionBuilder = SyncMetadataCompanion +typedef $$NotificationsTableUpdateCompanionBuilder = NotificationsCompanion Function({ - Value entityType, + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, Value lastSyncedAt, + Value type, + Value title, + Value body, + Value> data, + Value readAt, Value rowid, }); -class $$SyncMetadataTableFilterComposer - extends Composer<_$AppDatabase, $SyncMetadataTable> { - $$SyncMetadataTableFilterComposer({ +class $$NotificationsTableFilterComposer + extends Composer<_$AppDatabase, $NotificationsTable> { + $$NotificationsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get entityType => $composableBuilder( - column: $table.entityType, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnFilters(column)); ColumnFilters get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get title => $composableBuilder( + column: $table.title, builder: (column) => ColumnFilters(column)); + + ColumnFilters get body => $composableBuilder( + column: $table.body, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters, Map, + String> + get data => $composableBuilder( + column: $table.data, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get readAt => $composableBuilder( + column: $table.readAt, builder: (column) => ColumnFilters(column)); } -class $$SyncMetadataTableOrderingComposer - extends Composer<_$AppDatabase, $SyncMetadataTable> { - $$SyncMetadataTableOrderingComposer({ +class $$NotificationsTableOrderingComposer + extends Composer<_$AppDatabase, $NotificationsTable> { + $$NotificationsTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get entityType => $composableBuilder( - column: $table.entityType, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); ColumnOrderings get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get title => $composableBuilder( + column: $table.title, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get body => $composableBuilder( + column: $table.body, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get data => $composableBuilder( + column: $table.data, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get readAt => $composableBuilder( + column: $table.readAt, builder: (column) => ColumnOrderings(column)); } -class $$SyncMetadataTableAnnotationComposer - extends Composer<_$AppDatabase, $SyncMetadataTable> { - $$SyncMetadataTableAnnotationComposer({ +class $$NotificationsTableAnnotationComposer + extends Composer<_$AppDatabase, $NotificationsTable> { + $$NotificationsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get entityType => $composableBuilder( - column: $table.entityType, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get clientId => + $composableBuilder(column: $table.clientId, builder: (column) => column); + + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); GeneratedColumn get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get title => + $composableBuilder(column: $table.title, builder: (column) => column); + + GeneratedColumn get body => + $composableBuilder(column: $table.body, builder: (column) => column); + + GeneratedColumnWithTypeConverter, String> get data => + $composableBuilder(column: $table.data, builder: (column) => column); + + GeneratedColumn get readAt => + $composableBuilder(column: $table.readAt, builder: (column) => column); } -class $$SyncMetadataTableTableManager extends RootTableManager< +class $$NotificationsTableTableManager extends RootTableManager< _$AppDatabase, - $SyncMetadataTable, - SyncMetadatas, - $$SyncMetadataTableFilterComposer, - $$SyncMetadataTableOrderingComposer, - $$SyncMetadataTableAnnotationComposer, - $$SyncMetadataTableCreateCompanionBuilder, - $$SyncMetadataTableUpdateCompanionBuilder, + $NotificationsTable, + Notification, + $$NotificationsTableFilterComposer, + $$NotificationsTableOrderingComposer, + $$NotificationsTableAnnotationComposer, + $$NotificationsTableCreateCompanionBuilder, + $$NotificationsTableUpdateCompanionBuilder, ( - SyncMetadatas, - BaseReferences<_$AppDatabase, $SyncMetadataTable, SyncMetadatas> + Notification, + BaseReferences<_$AppDatabase, $NotificationsTable, Notification> ), - SyncMetadatas, + Notification, PrefetchHooks Function()> { - $$SyncMetadataTableTableManager(_$AppDatabase db, $SyncMetadataTable table) + $$NotificationsTableTableManager(_$AppDatabase db, $NotificationsTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$SyncMetadataTableFilterComposer($db: db, $table: table), + $$NotificationsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$SyncMetadataTableOrderingComposer($db: db, $table: table), + $$NotificationsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$SyncMetadataTableAnnotationComposer($db: db, $table: table), + $$NotificationsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value entityType = const Value.absent(), + Value id = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), + Value type = const Value.absent(), + Value title = const Value.absent(), + Value body = const Value.absent(), + Value> data = const Value.absent(), + Value readAt = const Value.absent(), Value rowid = const Value.absent(), }) => - SyncMetadataCompanion( - entityType: entityType, + NotificationsCompanion( + id: id, + userId: userId, + clientId: clientId, + rev: rev, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, + type: type, + title: title, + body: body, + data: data, + readAt: readAt, rowid: rowid, ), createCompanionCallback: ({ - required String entityType, + Value id = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), + required NotificationType type, + required String title, + required String body, + required Map data, + Value readAt = const Value.absent(), Value rowid = const Value.absent(), }) => - SyncMetadataCompanion.insert( - entityType: entityType, + NotificationsCompanion.insert( + id: id, + userId: userId, + clientId: clientId, + rev: rev, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, + type: type, + title: title, + body: body, + data: data, + readAt: readAt, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -9534,277 +11805,252 @@ class $$SyncMetadataTableTableManager extends RootTableManager< )); } -typedef $$SyncMetadataTableProcessedTableManager = ProcessedTableManager< +typedef $$NotificationsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $SyncMetadataTable, - SyncMetadatas, - $$SyncMetadataTableFilterComposer, - $$SyncMetadataTableOrderingComposer, - $$SyncMetadataTableAnnotationComposer, - $$SyncMetadataTableCreateCompanionBuilder, - $$SyncMetadataTableUpdateCompanionBuilder, + $NotificationsTable, + Notification, + $$NotificationsTableFilterComposer, + $$NotificationsTableOrderingComposer, + $$NotificationsTableAnnotationComposer, + $$NotificationsTableCreateCompanionBuilder, + $$NotificationsTableUpdateCompanionBuilder, ( - SyncMetadatas, - BaseReferences<_$AppDatabase, $SyncMetadataTable, SyncMetadatas> + Notification, + BaseReferences<_$AppDatabase, $NotificationsTable, Notification> ), - SyncMetadatas, + Notification, PrefetchHooks Function()>; -typedef $$CategorizablesTableCreateCompanionBuilder = CategorizablesCompanion - Function({ - required String categorizableId, - required CategorizableType categorizableType, - required String categoryClientId, +typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ + required String path, + Value id, + Value type, + Value fileableType, + Value fileableId, + Value localFileableType, + Value localFileableId, + Value createdAt, + Value updatedAt, Value rowid, }); -typedef $$CategorizablesTableUpdateCompanionBuilder = CategorizablesCompanion - Function({ - Value categorizableId, - Value categorizableType, - Value categoryClientId, +typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ + Value path, + Value id, + Value type, + Value fileableType, + Value fileableId, + Value localFileableType, + Value localFileableId, + Value createdAt, + Value updatedAt, Value rowid, }); -final class $$CategorizablesTableReferences - extends BaseReferences<_$AppDatabase, $CategorizablesTable, Categorizable> { - $$CategorizablesTableReferences( - super.$_db, super.$_table, super.$_typedResult); - - static $CategoriesTable _categoryClientIdTable(_$AppDatabase db) => - db.categories.createAlias($_aliasNameGenerator( - db.categorizables.categoryClientId, db.categories.clientId)); - - $$CategoriesTableProcessedTableManager get categoryClientId { - final $_column = $_itemColumn('category_client_id')!; - - final manager = $$CategoriesTableTableManager($_db, $_db.categories) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_categoryClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } -} - -class $$CategorizablesTableFilterComposer - extends Composer<_$AppDatabase, $CategorizablesTable> { - $$CategorizablesTableFilterComposer({ +class $$MediaFilesTableFilterComposer + extends Composer<_$AppDatabase, $MediaFilesTable> { + $$MediaFilesTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get categorizableId => $composableBuilder( - column: $table.categorizableId, + ColumnFilters get path => $composableBuilder( + column: $table.path, builder: (column) => ColumnFilters(column)); + + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnFilters(column)); + + ColumnFilters get fileableType => $composableBuilder( + column: $table.fileableType, builder: (column) => ColumnFilters(column)); + + ColumnFilters get fileableId => $composableBuilder( + column: $table.fileableId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get localFileableType => $composableBuilder( + column: $table.localFileableType, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters - get categorizableType => $composableBuilder( - column: $table.categorizableType, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get localFileableId => $composableBuilder( + column: $table.localFileableId, + builder: (column) => ColumnFilters(column)); - $$CategoriesTableFilterComposer get categoryClientId { - final $$CategoriesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.categoryClientId, - referencedTable: $db.categories, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$CategoriesTableFilterComposer( - $db: $db, - $table: $db.categories, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); } -class $$CategorizablesTableOrderingComposer - extends Composer<_$AppDatabase, $CategorizablesTable> { - $$CategorizablesTableOrderingComposer({ +class $$MediaFilesTableOrderingComposer + extends Composer<_$AppDatabase, $MediaFilesTable> { + $$MediaFilesTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get categorizableId => $composableBuilder( - column: $table.categorizableId, + ColumnOrderings get path => $composableBuilder( + column: $table.path, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get fileableType => $composableBuilder( + column: $table.fileableType, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get categorizableType => $composableBuilder( - column: $table.categorizableType, + ColumnOrderings get fileableId => $composableBuilder( + column: $table.fileableId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get localFileableType => $composableBuilder( + column: $table.localFileableType, builder: (column) => ColumnOrderings(column)); - $$CategoriesTableOrderingComposer get categoryClientId { - final $$CategoriesTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.categoryClientId, - referencedTable: $db.categories, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$CategoriesTableOrderingComposer( - $db: $db, - $table: $db.categories, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnOrderings get localFileableId => $composableBuilder( + column: $table.localFileableId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); } -class $$CategorizablesTableAnnotationComposer - extends Composer<_$AppDatabase, $CategorizablesTable> { - $$CategorizablesTableAnnotationComposer({ +class $$MediaFilesTableAnnotationComposer + extends Composer<_$AppDatabase, $MediaFilesTable> { + $$MediaFilesTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get categorizableId => $composableBuilder( - column: $table.categorizableId, builder: (column) => column); + GeneratedColumn get path => + $composableBuilder(column: $table.path, builder: (column) => column); - GeneratedColumnWithTypeConverter - get categorizableType => $composableBuilder( - column: $table.categorizableType, builder: (column) => column); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); - $$CategoriesTableAnnotationComposer get categoryClientId { - final $$CategoriesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.categoryClientId, - referencedTable: $db.categories, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$CategoriesTableAnnotationComposer( - $db: $db, - $table: $db.categories, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get fileableType => $composableBuilder( + column: $table.fileableType, builder: (column) => column); + + GeneratedColumn get fileableId => $composableBuilder( + column: $table.fileableId, builder: (column) => column); + + GeneratedColumn get localFileableType => $composableBuilder( + column: $table.localFileableType, builder: (column) => column); + + GeneratedColumn get localFileableId => $composableBuilder( + column: $table.localFileableId, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); } -class $$CategorizablesTableTableManager extends RootTableManager< +class $$MediaFilesTableTableManager extends RootTableManager< _$AppDatabase, - $CategorizablesTable, - Categorizable, - $$CategorizablesTableFilterComposer, - $$CategorizablesTableOrderingComposer, - $$CategorizablesTableAnnotationComposer, - $$CategorizablesTableCreateCompanionBuilder, - $$CategorizablesTableUpdateCompanionBuilder, - (Categorizable, $$CategorizablesTableReferences), - Categorizable, - PrefetchHooks Function({bool categoryClientId})> { - $$CategorizablesTableTableManager( - _$AppDatabase db, $CategorizablesTable table) + $MediaFilesTable, + MediaFile, + $$MediaFilesTableFilterComposer, + $$MediaFilesTableOrderingComposer, + $$MediaFilesTableAnnotationComposer, + $$MediaFilesTableCreateCompanionBuilder, + $$MediaFilesTableUpdateCompanionBuilder, + (MediaFile, BaseReferences<_$AppDatabase, $MediaFilesTable, MediaFile>), + MediaFile, + PrefetchHooks Function()> { + $$MediaFilesTableTableManager(_$AppDatabase db, $MediaFilesTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$CategorizablesTableFilterComposer($db: db, $table: table), + $$MediaFilesTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$CategorizablesTableOrderingComposer($db: db, $table: table), + $$MediaFilesTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$CategorizablesTableAnnotationComposer($db: db, $table: table), + $$MediaFilesTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value categorizableId = const Value.absent(), - Value categorizableType = const Value.absent(), - Value categoryClientId = const Value.absent(), + Value path = const Value.absent(), + Value id = const Value.absent(), + Value type = const Value.absent(), + Value fileableType = const Value.absent(), + Value fileableId = const Value.absent(), + Value localFileableType = const Value.absent(), + Value localFileableId = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), Value rowid = const Value.absent(), - }) => - CategorizablesCompanion( - categorizableId: categorizableId, - categorizableType: categorizableType, - categoryClientId: categoryClientId, + }) => + MediaFilesCompanion( + path: path, + id: id, + type: type, + fileableType: fileableType, + fileableId: fileableId, + localFileableType: localFileableType, + localFileableId: localFileableId, + createdAt: createdAt, + updatedAt: updatedAt, rowid: rowid, ), createCompanionCallback: ({ - required String categorizableId, - required CategorizableType categorizableType, - required String categoryClientId, + required String path, + Value id = const Value.absent(), + Value type = const Value.absent(), + Value fileableType = const Value.absent(), + Value fileableId = const Value.absent(), + Value localFileableType = const Value.absent(), + Value localFileableId = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), Value rowid = const Value.absent(), }) => - CategorizablesCompanion.insert( - categorizableId: categorizableId, - categorizableType: categorizableType, - categoryClientId: categoryClientId, + MediaFilesCompanion.insert( + path: path, + id: id, + type: type, + fileableType: fileableType, + fileableId: fileableId, + localFileableType: localFileableType, + localFileableId: localFileableId, + createdAt: createdAt, + updatedAt: updatedAt, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$CategorizablesTableReferences(db, table, e) - )) + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) .toList(), - prefetchHooksCallback: ({categoryClientId = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic>>(state) { - if (categoryClientId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.categoryClientId, - referencedTable: $$CategorizablesTableReferences - ._categoryClientIdTable(db), - referencedColumn: $$CategorizablesTableReferences - ._categoryClientIdTable(db) - .clientId, - ) as T; - } - - return state; - }, - getPrefetchedDataCallback: (items) async { - return []; - }, - ); - }, + prefetchHooksCallback: null, )); } -typedef $$CategorizablesTableProcessedTableManager = ProcessedTableManager< +typedef $$MediaFilesTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $CategorizablesTable, - Categorizable, - $$CategorizablesTableFilterComposer, - $$CategorizablesTableOrderingComposer, - $$CategorizablesTableAnnotationComposer, - $$CategorizablesTableCreateCompanionBuilder, - $$CategorizablesTableUpdateCompanionBuilder, - (Categorizable, $$CategorizablesTableReferences), - Categorizable, - PrefetchHooks Function({bool categoryClientId})>; -typedef $$NotificationsTableCreateCompanionBuilder = NotificationsCompanion - Function({ + $MediaFilesTable, + MediaFile, + $$MediaFilesTableFilterComposer, + $$MediaFilesTableOrderingComposer, + $$MediaFilesTableAnnotationComposer, + $$MediaFilesTableCreateCompanionBuilder, + $$MediaFilesTableUpdateCompanionBuilder, + (MediaFile, BaseReferences<_$AppDatabase, $MediaFilesTable, MediaFile>), + MediaFile, + PrefetchHooks Function()>; +typedef $$TransfersTableCreateCompanionBuilder = TransfersCompanion Function({ Value id, Value userId, Value clientId, @@ -9813,15 +12059,18 @@ typedef $$NotificationsTableCreateCompanionBuilder = NotificationsCompanion Value updatedAt, Value deletedAt, Value lastSyncedAt, - required NotificationType type, - required String title, - required String body, - required Map data, - Value readAt, + required double amount, + Value fromWalletId, + Value toWalletId, + Value fromWalletClientId, + Value toWalletClientId, + Value exchangeRate, + required DateTime datetime, + Value expenseTransactionClientId, + Value incomeTransactionClientId, Value rowid, }); -typedef $$NotificationsTableUpdateCompanionBuilder = NotificationsCompanion - Function({ +typedef $$TransfersTableUpdateCompanionBuilder = TransfersCompanion Function({ Value id, Value userId, Value clientId, @@ -9830,17 +12079,89 @@ typedef $$NotificationsTableUpdateCompanionBuilder = NotificationsCompanion Value updatedAt, Value deletedAt, Value lastSyncedAt, - Value type, - Value title, - Value body, - Value> data, - Value readAt, + Value amount, + Value fromWalletId, + Value toWalletId, + Value fromWalletClientId, + Value toWalletClientId, + Value exchangeRate, + Value datetime, + Value expenseTransactionClientId, + Value incomeTransactionClientId, Value rowid, }); -class $$NotificationsTableFilterComposer - extends Composer<_$AppDatabase, $NotificationsTable> { - $$NotificationsTableFilterComposer({ +final class $$TransfersTableReferences + extends BaseReferences<_$AppDatabase, $TransfersTable, Transfer> { + $$TransfersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $WalletsTable _fromWalletClientIdTable(_$AppDatabase db) => + db.wallets.createAlias($_aliasNameGenerator( + db.transfers.fromWalletClientId, db.wallets.clientId)); + + $$WalletsTableProcessedTableManager? get fromWalletClientId { + final $_column = $_itemColumn('from_wallet_client_id'); + if ($_column == null) return null; + final manager = $$WalletsTableTableManager($_db, $_db.wallets) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_fromWalletClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $WalletsTable _toWalletClientIdTable(_$AppDatabase db) => + db.wallets.createAlias($_aliasNameGenerator( + db.transfers.toWalletClientId, db.wallets.clientId)); + + $$WalletsTableProcessedTableManager? get toWalletClientId { + final $_column = $_itemColumn('to_wallet_client_id'); + if ($_column == null) return null; + final manager = $$WalletsTableTableManager($_db, $_db.wallets) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_toWalletClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $TransactionsTable _expenseTransactionClientIdTable( + _$AppDatabase db) => + db.transactions.createAlias($_aliasNameGenerator( + db.transfers.expenseTransactionClientId, db.transactions.clientId)); + + $$TransactionsTableProcessedTableManager? get expenseTransactionClientId { + final $_column = $_itemColumn('expense_transaction_client_id'); + if ($_column == null) return null; + final manager = $$TransactionsTableTableManager($_db, $_db.transactions) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = + $_typedResult.readTableOrNull(_expenseTransactionClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $TransactionsTable _incomeTransactionClientIdTable(_$AppDatabase db) => + db.transactions.createAlias($_aliasNameGenerator( + db.transfers.incomeTransactionClientId, db.transactions.clientId)); + + $$TransactionsTableProcessedTableManager? get incomeTransactionClientId { + final $_column = $_itemColumn('income_transaction_client_id'); + if ($_column == null) return null; + final manager = $$TransactionsTableTableManager($_db, $_db.transactions) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = + $_typedResult.readTableOrNull(_incomeTransactionClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$TransfersTableFilterComposer + extends Composer<_$AppDatabase, $TransfersTable> { + $$TransfersTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -9871,30 +12192,105 @@ class $$NotificationsTableFilterComposer ColumnFilters get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters - get type => $composableBuilder( - column: $table.type, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get amount => $composableBuilder( + column: $table.amount, builder: (column) => ColumnFilters(column)); - ColumnFilters get title => $composableBuilder( - column: $table.title, builder: (column) => ColumnFilters(column)); + ColumnFilters get fromWalletId => $composableBuilder( + column: $table.fromWalletId, builder: (column) => ColumnFilters(column)); - ColumnFilters get body => $composableBuilder( - column: $table.body, builder: (column) => ColumnFilters(column)); + ColumnFilters get toWalletId => $composableBuilder( + column: $table.toWalletId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get exchangeRate => $composableBuilder( + column: $table.exchangeRate, builder: (column) => ColumnFilters(column)); + + ColumnFilters get datetime => $composableBuilder( + column: $table.datetime, builder: (column) => ColumnFilters(column)); + + $$WalletsTableFilterComposer get fromWalletClientId { + final $$WalletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.fromWalletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableFilterComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$WalletsTableFilterComposer get toWalletClientId { + final $$WalletsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.toWalletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableFilterComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } - ColumnWithTypeConverterFilters, Map, - String> - get data => $composableBuilder( - column: $table.data, - builder: (column) => ColumnWithTypeConverterFilters(column)); + $$TransactionsTableFilterComposer get expenseTransactionClientId { + final $$TransactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.expenseTransactionClientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableFilterComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } - ColumnFilters get readAt => $composableBuilder( - column: $table.readAt, builder: (column) => ColumnFilters(column)); + $$TransactionsTableFilterComposer get incomeTransactionClientId { + final $$TransactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.incomeTransactionClientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableFilterComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } -class $$NotificationsTableOrderingComposer - extends Composer<_$AppDatabase, $NotificationsTable> { - $$NotificationsTableOrderingComposer({ +class $$TransfersTableOrderingComposer + extends Composer<_$AppDatabase, $TransfersTable> { + $$TransfersTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -9926,25 +12322,107 @@ class $$NotificationsTableOrderingComposer column: $table.lastSyncedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get amount => $composableBuilder( + column: $table.amount, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get title => $composableBuilder( - column: $table.title, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get fromWalletId => $composableBuilder( + column: $table.fromWalletId, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get body => $composableBuilder( - column: $table.body, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get toWalletId => $composableBuilder( + column: $table.toWalletId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get data => $composableBuilder( - column: $table.data, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get exchangeRate => $composableBuilder( + column: $table.exchangeRate, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get readAt => $composableBuilder( - column: $table.readAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get datetime => $composableBuilder( + column: $table.datetime, builder: (column) => ColumnOrderings(column)); + + $$WalletsTableOrderingComposer get fromWalletClientId { + final $$WalletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.fromWalletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableOrderingComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$WalletsTableOrderingComposer get toWalletClientId { + final $$WalletsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.toWalletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableOrderingComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$TransactionsTableOrderingComposer get expenseTransactionClientId { + final $$TransactionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.expenseTransactionClientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableOrderingComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$TransactionsTableOrderingComposer get incomeTransactionClientId { + final $$TransactionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.incomeTransactionClientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableOrderingComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } -class $$NotificationsTableAnnotationComposer - extends Composer<_$AppDatabase, $NotificationsTable> { - $$NotificationsTableAnnotationComposer({ +class $$TransfersTableAnnotationComposer + extends Composer<_$AppDatabase, $TransfersTable> { + $$TransfersTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -9975,47 +12453,128 @@ class $$NotificationsTableAnnotationComposer GeneratedColumn get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => column); - GeneratedColumnWithTypeConverter get type => - $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get amount => + $composableBuilder(column: $table.amount, builder: (column) => column); - GeneratedColumn get title => - $composableBuilder(column: $table.title, builder: (column) => column); + GeneratedColumn get fromWalletId => $composableBuilder( + column: $table.fromWalletId, builder: (column) => column); - GeneratedColumn get body => - $composableBuilder(column: $table.body, builder: (column) => column); + GeneratedColumn get toWalletId => $composableBuilder( + column: $table.toWalletId, builder: (column) => column); - GeneratedColumnWithTypeConverter, String> get data => - $composableBuilder(column: $table.data, builder: (column) => column); + GeneratedColumn get exchangeRate => $composableBuilder( + column: $table.exchangeRate, builder: (column) => column); - GeneratedColumn get readAt => - $composableBuilder(column: $table.readAt, builder: (column) => column); + GeneratedColumn get datetime => + $composableBuilder(column: $table.datetime, builder: (column) => column); + + $$WalletsTableAnnotationComposer get fromWalletClientId { + final $$WalletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.fromWalletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableAnnotationComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$WalletsTableAnnotationComposer get toWalletClientId { + final $$WalletsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.toWalletClientId, + referencedTable: $db.wallets, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$WalletsTableAnnotationComposer( + $db: $db, + $table: $db.wallets, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$TransactionsTableAnnotationComposer get expenseTransactionClientId { + final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.expenseTransactionClientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableAnnotationComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$TransactionsTableAnnotationComposer get incomeTransactionClientId { + final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.incomeTransactionClientId, + referencedTable: $db.transactions, + getReferencedColumn: (t) => t.clientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$TransactionsTableAnnotationComposer( + $db: $db, + $table: $db.transactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } -class $$NotificationsTableTableManager extends RootTableManager< +class $$TransfersTableTableManager extends RootTableManager< _$AppDatabase, - $NotificationsTable, - Notification, - $$NotificationsTableFilterComposer, - $$NotificationsTableOrderingComposer, - $$NotificationsTableAnnotationComposer, - $$NotificationsTableCreateCompanionBuilder, - $$NotificationsTableUpdateCompanionBuilder, - ( - Notification, - BaseReferences<_$AppDatabase, $NotificationsTable, Notification> - ), - Notification, - PrefetchHooks Function()> { - $$NotificationsTableTableManager(_$AppDatabase db, $NotificationsTable table) + $TransfersTable, + Transfer, + $$TransfersTableFilterComposer, + $$TransfersTableOrderingComposer, + $$TransfersTableAnnotationComposer, + $$TransfersTableCreateCompanionBuilder, + $$TransfersTableUpdateCompanionBuilder, + (Transfer, $$TransfersTableReferences), + Transfer, + PrefetchHooks Function( + {bool fromWalletClientId, + bool toWalletClientId, + bool expenseTransactionClientId, + bool incomeTransactionClientId})> { + $$TransfersTableTableManager(_$AppDatabase db, $TransfersTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$NotificationsTableFilterComposer($db: db, $table: table), + $$TransfersTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$NotificationsTableOrderingComposer($db: db, $table: table), + $$TransfersTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$NotificationsTableAnnotationComposer($db: db, $table: table), + $$TransfersTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value userId = const Value.absent(), @@ -10025,14 +12584,18 @@ class $$NotificationsTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - Value type = const Value.absent(), - Value title = const Value.absent(), - Value body = const Value.absent(), - Value> data = const Value.absent(), - Value readAt = const Value.absent(), + Value amount = const Value.absent(), + Value fromWalletId = const Value.absent(), + Value toWalletId = const Value.absent(), + Value fromWalletClientId = const Value.absent(), + Value toWalletClientId = const Value.absent(), + Value exchangeRate = const Value.absent(), + Value datetime = const Value.absent(), + Value expenseTransactionClientId = const Value.absent(), + Value incomeTransactionClientId = const Value.absent(), Value rowid = const Value.absent(), }) => - NotificationsCompanion( + TransfersCompanion( id: id, userId: userId, clientId: clientId, @@ -10041,11 +12604,15 @@ class $$NotificationsTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - type: type, - title: title, - body: body, - data: data, - readAt: readAt, + amount: amount, + fromWalletId: fromWalletId, + toWalletId: toWalletId, + fromWalletClientId: fromWalletClientId, + toWalletClientId: toWalletClientId, + exchangeRate: exchangeRate, + datetime: datetime, + expenseTransactionClientId: expenseTransactionClientId, + incomeTransactionClientId: incomeTransactionClientId, rowid: rowid, ), createCompanionCallback: ({ @@ -10057,14 +12624,18 @@ class $$NotificationsTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - required NotificationType type, - required String title, - required String body, - required Map data, - Value readAt = const Value.absent(), + required double amount, + Value fromWalletId = const Value.absent(), + Value toWalletId = const Value.absent(), + Value fromWalletClientId = const Value.absent(), + Value toWalletClientId = const Value.absent(), + Value exchangeRate = const Value.absent(), + required DateTime datetime, + Value expenseTransactionClientId = const Value.absent(), + Value incomeTransactionClientId = const Value.absent(), Value rowid = const Value.absent(), }) => - NotificationsCompanion.insert( + TransfersCompanion.insert( id: id, userId: userId, clientId: clientId, @@ -10073,434 +12644,777 @@ class $$NotificationsTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - type: type, - title: title, - body: body, - data: data, - readAt: readAt, + amount: amount, + fromWalletId: fromWalletId, + toWalletId: toWalletId, + fromWalletClientId: fromWalletClientId, + toWalletClientId: toWalletClientId, + exchangeRate: exchangeRate, + datetime: datetime, + expenseTransactionClientId: expenseTransactionClientId, + incomeTransactionClientId: incomeTransactionClientId, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .map((e) => ( + e.readTable(table), + $$TransfersTableReferences(db, table, e) + )) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ( + {fromWalletClientId = false, + toWalletClientId = false, + expenseTransactionClientId = false, + incomeTransactionClientId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (fromWalletClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.fromWalletClientId, + referencedTable: + $$TransfersTableReferences._fromWalletClientIdTable(db), + referencedColumn: $$TransfersTableReferences + ._fromWalletClientIdTable(db) + .clientId, + ) as T; + } + if (toWalletClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.toWalletClientId, + referencedTable: + $$TransfersTableReferences._toWalletClientIdTable(db), + referencedColumn: $$TransfersTableReferences + ._toWalletClientIdTable(db) + .clientId, + ) as T; + } + if (expenseTransactionClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.expenseTransactionClientId, + referencedTable: $$TransfersTableReferences + ._expenseTransactionClientIdTable(db), + referencedColumn: $$TransfersTableReferences + ._expenseTransactionClientIdTable(db) + .clientId, + ) as T; + } + if (incomeTransactionClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.incomeTransactionClientId, + referencedTable: $$TransfersTableReferences + ._incomeTransactionClientIdTable(db), + referencedColumn: $$TransfersTableReferences + ._incomeTransactionClientIdTable(db) + .clientId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, )); } -typedef $$NotificationsTableProcessedTableManager = ProcessedTableManager< +typedef $$TransfersTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $NotificationsTable, - Notification, - $$NotificationsTableFilterComposer, - $$NotificationsTableOrderingComposer, - $$NotificationsTableAnnotationComposer, - $$NotificationsTableCreateCompanionBuilder, - $$NotificationsTableUpdateCompanionBuilder, - ( - Notification, - BaseReferences<_$AppDatabase, $NotificationsTable, Notification> - ), - Notification, - PrefetchHooks Function()>; -typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ - required String path, + $TransfersTable, + Transfer, + $$TransfersTableFilterComposer, + $$TransfersTableOrderingComposer, + $$TransfersTableAnnotationComposer, + $$TransfersTableCreateCompanionBuilder, + $$TransfersTableUpdateCompanionBuilder, + (Transfer, $$TransfersTableReferences), + Transfer, + PrefetchHooks Function( + {bool fromWalletClientId, + bool toWalletClientId, + bool expenseTransactionClientId, + bool incomeTransactionClientId})>; +typedef $$BudgetsTableCreateCompanionBuilder = BudgetsCompanion Function({ Value id, - Value type, - Value fileableType, - Value fileableId, - Value localFileableType, - Value localFileableId, - Value createdAt, - Value updatedAt, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + required String name, + required String slug, + Value description, + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + Value endDate, + Value rolloverEnabled, + Value thresholdPercent, + Value forecastAlertsEnabled, + Value isActive, + Value ownerType, + Value ownerId, Value rowid, }); -typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ - Value path, +typedef $$BudgetsTableUpdateCompanionBuilder = BudgetsCompanion Function({ Value id, - Value type, - Value fileableType, - Value fileableId, - Value localFileableType, - Value localFileableId, - Value createdAt, - Value updatedAt, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + Value name, + Value slug, + Value description, + Value amount, + Value currency, + Value periodType, + Value startDate, + Value endDate, + Value rolloverEnabled, + Value thresholdPercent, + Value forecastAlertsEnabled, + Value isActive, + Value ownerType, + Value ownerId, Value rowid, }); -class $$MediaFilesTableFilterComposer - extends Composer<_$AppDatabase, $MediaFilesTable> { - $$MediaFilesTableFilterComposer({ +final class $$BudgetsTableReferences + extends BaseReferences<_$AppDatabase, $BudgetsTable, Budget> { + $$BudgetsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$BudgetablesTable, List> + _budgetablesRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable(db.budgetables, + aliasName: $_aliasNameGenerator( + db.budgets.clientId, db.budgetables.budgetClientId)); + + $$BudgetablesTableProcessedTableManager get budgetablesRefs { + final manager = $$BudgetablesTableTableManager($_db, $_db.budgetables) + .filter((f) => f.budgetClientId.clientId + .sqlEquals($_itemColumn('client_id')!)); + + final cache = $_typedResult.readTableOrNull(_budgetablesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$BudgetPeriodStatesTable, List> + _budgetPeriodStatesRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable(db.budgetPeriodStates, + aliasName: $_aliasNameGenerator( + db.budgets.clientId, db.budgetPeriodStates.budgetClientId)); + + $$BudgetPeriodStatesTableProcessedTableManager get budgetPeriodStatesRefs { + final manager = + $$BudgetPeriodStatesTableTableManager($_db, $_db.budgetPeriodStates) + .filter((f) => f.budgetClientId.clientId + .sqlEquals($_itemColumn('client_id')!)); + + final cache = + $_typedResult.readTableOrNull(_budgetPeriodStatesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$BudgetsTableFilterComposer + extends Composer<_$AppDatabase, $BudgetsTable> { + $$BudgetsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get path => $composableBuilder( - column: $table.path, builder: (column) => ColumnFilters(column)); - ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnFilters(column)); - - ColumnFilters get fileableType => $composableBuilder( - column: $table.fileableType, builder: (column) => ColumnFilters(column)); - - ColumnFilters get fileableId => $composableBuilder( - column: $table.fileableId, builder: (column) => ColumnFilters(column)); + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); - ColumnFilters get localFileableType => $composableBuilder( - column: $table.localFileableType, - builder: (column) => ColumnFilters(column)); + ColumnFilters get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnFilters(column)); - ColumnFilters get localFileableId => $composableBuilder( - column: $table.localFileableId, - builder: (column) => ColumnFilters(column)); + ColumnFilters get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); ColumnFilters get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get slug => $composableBuilder( + column: $table.slug, builder: (column) => ColumnFilters(column)); + + ColumnFilters get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnFilters(column)); + + ColumnFilters get amount => $composableBuilder( + column: $table.amount, builder: (column) => ColumnFilters(column)); + + ColumnFilters get currency => $composableBuilder( + column: $table.currency, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get periodType => $composableBuilder( + column: $table.periodType, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get startDate => $composableBuilder( + column: $table.startDate, builder: (column) => ColumnFilters(column)); + + ColumnFilters get endDate => $composableBuilder( + column: $table.endDate, builder: (column) => ColumnFilters(column)); + + ColumnFilters get rolloverEnabled => $composableBuilder( + column: $table.rolloverEnabled, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get thresholdPercent => $composableBuilder( + column: $table.thresholdPercent, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get forecastAlertsEnabled => $composableBuilder( + column: $table.forecastAlertsEnabled, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get isActive => $composableBuilder( + column: $table.isActive, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ownerType => $composableBuilder( + column: $table.ownerType, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnFilters(column)); + + Expression budgetablesRefs( + Expression Function($$BudgetablesTableFilterComposer f) f) { + final $$BudgetablesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.budgetables, + getReferencedColumn: (t) => t.budgetClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$BudgetablesTableFilterComposer( + $db: $db, + $table: $db.budgetables, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression budgetPeriodStatesRefs( + Expression Function($$BudgetPeriodStatesTableFilterComposer f) f) { + final $$BudgetPeriodStatesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.budgetPeriodStates, + getReferencedColumn: (t) => t.budgetClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$BudgetPeriodStatesTableFilterComposer( + $db: $db, + $table: $db.budgetPeriodStates, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } -class $$MediaFilesTableOrderingComposer - extends Composer<_$AppDatabase, $MediaFilesTable> { - $$MediaFilesTableOrderingComposer({ +class $$BudgetsTableOrderingComposer + extends Composer<_$AppDatabase, $BudgetsTable> { + $$BudgetsTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get path => $composableBuilder( - column: $table.path, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get fileableType => $composableBuilder( - column: $table.fileableType, + ColumnOrderings get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get fileableId => $composableBuilder( - column: $table.fileableId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localFileableType => $composableBuilder( - column: $table.localFileableType, + ColumnOrderings get slug => $composableBuilder( + column: $table.slug, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get amount => $composableBuilder( + column: $table.amount, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get currency => $composableBuilder( + column: $table.currency, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get periodType => $composableBuilder( + column: $table.periodType, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get startDate => $composableBuilder( + column: $table.startDate, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get endDate => $composableBuilder( + column: $table.endDate, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rolloverEnabled => $composableBuilder( + column: $table.rolloverEnabled, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localFileableId => $composableBuilder( - column: $table.localFileableId, + ColumnOrderings get thresholdPercent => $composableBuilder( + column: $table.thresholdPercent, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get forecastAlertsEnabled => $composableBuilder( + column: $table.forecastAlertsEnabled, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isActive => $composableBuilder( + column: $table.isActive, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ownerType => $composableBuilder( + column: $table.ownerType, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnOrderings(column)); } -class $$MediaFilesTableAnnotationComposer - extends Composer<_$AppDatabase, $MediaFilesTable> { - $$MediaFilesTableAnnotationComposer({ +class $$BudgetsTableAnnotationComposer + extends Composer<_$AppDatabase, $BudgetsTable> { + $$BudgetsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get path => - $composableBuilder(column: $table.path, builder: (column) => column); - GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get type => - $composableBuilder(column: $table.type, builder: (column) => column); - - GeneratedColumn get fileableType => $composableBuilder( - column: $table.fileableType, builder: (column) => column); - - GeneratedColumn get fileableId => $composableBuilder( - column: $table.fileableId, builder: (column) => column); + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); - GeneratedColumn get localFileableType => $composableBuilder( - column: $table.localFileableType, builder: (column) => column); + GeneratedColumn get clientId => + $composableBuilder(column: $table.clientId, builder: (column) => column); - GeneratedColumn get localFileableId => $composableBuilder( - column: $table.localFileableId, builder: (column) => column); + GeneratedColumn get rev => + $composableBuilder(column: $table.rev, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); -} -class $$MediaFilesTableTableManager extends RootTableManager< - _$AppDatabase, - $MediaFilesTable, - MediaFile, - $$MediaFilesTableFilterComposer, - $$MediaFilesTableOrderingComposer, - $$MediaFilesTableAnnotationComposer, - $$MediaFilesTableCreateCompanionBuilder, - $$MediaFilesTableUpdateCompanionBuilder, - (MediaFile, BaseReferences<_$AppDatabase, $MediaFilesTable, MediaFile>), - MediaFile, - PrefetchHooks Function()> { - $$MediaFilesTableTableManager(_$AppDatabase db, $MediaFilesTable table) + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + GeneratedColumn get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get slug => + $composableBuilder(column: $table.slug, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + GeneratedColumn get amount => + $composableBuilder(column: $table.amount, builder: (column) => column); + + GeneratedColumn get currency => + $composableBuilder(column: $table.currency, builder: (column) => column); + + GeneratedColumnWithTypeConverter get periodType => + $composableBuilder( + column: $table.periodType, builder: (column) => column); + + GeneratedColumn get startDate => + $composableBuilder(column: $table.startDate, builder: (column) => column); + + GeneratedColumn get endDate => + $composableBuilder(column: $table.endDate, builder: (column) => column); + + GeneratedColumn get rolloverEnabled => $composableBuilder( + column: $table.rolloverEnabled, builder: (column) => column); + + GeneratedColumn get thresholdPercent => $composableBuilder( + column: $table.thresholdPercent, builder: (column) => column); + + GeneratedColumn get forecastAlertsEnabled => $composableBuilder( + column: $table.forecastAlertsEnabled, builder: (column) => column); + + GeneratedColumn get isActive => + $composableBuilder(column: $table.isActive, builder: (column) => column); + + GeneratedColumn get ownerType => + $composableBuilder(column: $table.ownerType, builder: (column) => column); + + GeneratedColumn get ownerId => + $composableBuilder(column: $table.ownerId, builder: (column) => column); + + Expression budgetablesRefs( + Expression Function($$BudgetablesTableAnnotationComposer a) f) { + final $$BudgetablesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.budgetables, + getReferencedColumn: (t) => t.budgetClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$BudgetablesTableAnnotationComposer( + $db: $db, + $table: $db.budgetables, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression budgetPeriodStatesRefs( + Expression Function($$BudgetPeriodStatesTableAnnotationComposer a) f) { + final $$BudgetPeriodStatesTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.clientId, + referencedTable: $db.budgetPeriodStates, + getReferencedColumn: (t) => t.budgetClientId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$BudgetPeriodStatesTableAnnotationComposer( + $db: $db, + $table: $db.budgetPeriodStates, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$BudgetsTableTableManager extends RootTableManager< + _$AppDatabase, + $BudgetsTable, + Budget, + $$BudgetsTableFilterComposer, + $$BudgetsTableOrderingComposer, + $$BudgetsTableAnnotationComposer, + $$BudgetsTableCreateCompanionBuilder, + $$BudgetsTableUpdateCompanionBuilder, + (Budget, $$BudgetsTableReferences), + Budget, + PrefetchHooks Function( + {bool budgetablesRefs, bool budgetPeriodStatesRefs})> { + $$BudgetsTableTableManager(_$AppDatabase db, $BudgetsTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$MediaFilesTableFilterComposer($db: db, $table: table), + $$BudgetsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$MediaFilesTableOrderingComposer($db: db, $table: table), + $$BudgetsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$MediaFilesTableAnnotationComposer($db: db, $table: table), + $$BudgetsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value path = const Value.absent(), Value id = const Value.absent(), - Value type = const Value.absent(), - Value fileableType = const Value.absent(), - Value fileableId = const Value.absent(), - Value localFileableType = const Value.absent(), - Value localFileableId = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + Value name = const Value.absent(), + Value slug = const Value.absent(), + Value description = const Value.absent(), + Value amount = const Value.absent(), + Value currency = const Value.absent(), + Value periodType = const Value.absent(), + Value startDate = const Value.absent(), + Value endDate = const Value.absent(), + Value rolloverEnabled = const Value.absent(), + Value thresholdPercent = const Value.absent(), + Value forecastAlertsEnabled = const Value.absent(), + Value isActive = const Value.absent(), + Value ownerType = const Value.absent(), + Value ownerId = const Value.absent(), Value rowid = const Value.absent(), }) => - MediaFilesCompanion( - path: path, + BudgetsCompanion( id: id, - type: type, - fileableType: fileableType, - fileableId: fileableId, - localFileableType: localFileableType, - localFileableId: localFileableId, + userId: userId, + clientId: clientId, + rev: rev, createdAt: createdAt, updatedAt: updatedAt, + deletedAt: deletedAt, + lastSyncedAt: lastSyncedAt, + name: name, + slug: slug, + description: description, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + ownerType: ownerType, + ownerId: ownerId, rowid: rowid, ), createCompanionCallback: ({ - required String path, Value id = const Value.absent(), - Value type = const Value.absent(), - Value fileableType = const Value.absent(), - Value fileableId = const Value.absent(), - Value localFileableType = const Value.absent(), - Value localFileableId = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value clientId = const Value.absent(), + Value rev = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + required String name, + required String slug, + Value description = const Value.absent(), + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + Value endDate = const Value.absent(), + Value rolloverEnabled = const Value.absent(), + Value thresholdPercent = const Value.absent(), + Value forecastAlertsEnabled = const Value.absent(), + Value isActive = const Value.absent(), + Value ownerType = const Value.absent(), + Value ownerId = const Value.absent(), Value rowid = const Value.absent(), }) => - MediaFilesCompanion.insert( - path: path, + BudgetsCompanion.insert( id: id, - type: type, - fileableType: fileableType, - fileableId: fileableId, - localFileableType: localFileableType, - localFileableId: localFileableId, + userId: userId, + clientId: clientId, + rev: rev, createdAt: createdAt, updatedAt: updatedAt, + deletedAt: deletedAt, + lastSyncedAt: lastSyncedAt, + name: name, + slug: slug, + description: description, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + ownerType: ownerType, + ownerId: ownerId, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .map((e) => + (e.readTable(table), $$BudgetsTableReferences(db, table, e))) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ( + {budgetablesRefs = false, budgetPeriodStatesRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (budgetablesRefs) db.budgetables, + if (budgetPeriodStatesRefs) db.budgetPeriodStates + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (budgetablesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$BudgetsTableReferences._budgetablesRefsTable(db), + managerFromTypedResult: (p0) => + $$BudgetsTableReferences(db, table, p0) + .budgetablesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.budgetClientId == item.clientId), + typedResults: items), + if (budgetPeriodStatesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$BudgetsTableReferences + ._budgetPeriodStatesRefsTable(db), + managerFromTypedResult: (p0) => + $$BudgetsTableReferences(db, table, p0) + .budgetPeriodStatesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.budgetClientId == item.clientId), + typedResults: items) + ]; + }, + ); + }, )); } -typedef $$MediaFilesTableProcessedTableManager = ProcessedTableManager< +typedef $$BudgetsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $MediaFilesTable, - MediaFile, - $$MediaFilesTableFilterComposer, - $$MediaFilesTableOrderingComposer, - $$MediaFilesTableAnnotationComposer, - $$MediaFilesTableCreateCompanionBuilder, - $$MediaFilesTableUpdateCompanionBuilder, - (MediaFile, BaseReferences<_$AppDatabase, $MediaFilesTable, MediaFile>), - MediaFile, - PrefetchHooks Function()>; -typedef $$TransfersTableCreateCompanionBuilder = TransfersCompanion Function({ - Value id, - Value userId, - Value clientId, - Value rev, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value lastSyncedAt, - required double amount, - Value fromWalletId, - Value toWalletId, - Value fromWalletClientId, - Value toWalletClientId, - Value exchangeRate, - required DateTime datetime, - Value expenseTransactionClientId, - Value incomeTransactionClientId, + $BudgetsTable, + Budget, + $$BudgetsTableFilterComposer, + $$BudgetsTableOrderingComposer, + $$BudgetsTableAnnotationComposer, + $$BudgetsTableCreateCompanionBuilder, + $$BudgetsTableUpdateCompanionBuilder, + (Budget, $$BudgetsTableReferences), + Budget, + PrefetchHooks Function( + {bool budgetablesRefs, bool budgetPeriodStatesRefs})>; +typedef $$BudgetablesTableCreateCompanionBuilder = BudgetablesCompanion + Function({ + required String budgetClientId, + required BudgetTargetType targetType, + required String targetClientId, Value rowid, }); -typedef $$TransfersTableUpdateCompanionBuilder = TransfersCompanion Function({ - Value id, - Value userId, - Value clientId, - Value rev, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value lastSyncedAt, - Value amount, - Value fromWalletId, - Value toWalletId, - Value fromWalletClientId, - Value toWalletClientId, - Value exchangeRate, - Value datetime, - Value expenseTransactionClientId, - Value incomeTransactionClientId, +typedef $$BudgetablesTableUpdateCompanionBuilder = BudgetablesCompanion + Function({ + Value budgetClientId, + Value targetType, + Value targetClientId, Value rowid, }); -final class $$TransfersTableReferences - extends BaseReferences<_$AppDatabase, $TransfersTable, Transfer> { - $$TransfersTableReferences(super.$_db, super.$_table, super.$_typedResult); - - static $WalletsTable _fromWalletClientIdTable(_$AppDatabase db) => - db.wallets.createAlias($_aliasNameGenerator( - db.transfers.fromWalletClientId, db.wallets.clientId)); - - $$WalletsTableProcessedTableManager? get fromWalletClientId { - final $_column = $_itemColumn('from_wallet_client_id'); - if ($_column == null) return null; - final manager = $$WalletsTableTableManager($_db, $_db.wallets) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_fromWalletClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } - - static $WalletsTable _toWalletClientIdTable(_$AppDatabase db) => - db.wallets.createAlias($_aliasNameGenerator( - db.transfers.toWalletClientId, db.wallets.clientId)); - - $$WalletsTableProcessedTableManager? get toWalletClientId { - final $_column = $_itemColumn('to_wallet_client_id'); - if ($_column == null) return null; - final manager = $$WalletsTableTableManager($_db, $_db.wallets) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_toWalletClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } - - static $TransactionsTable _expenseTransactionClientIdTable( - _$AppDatabase db) => - db.transactions.createAlias($_aliasNameGenerator( - db.transfers.expenseTransactionClientId, db.transactions.clientId)); - - $$TransactionsTableProcessedTableManager? get expenseTransactionClientId { - final $_column = $_itemColumn('expense_transaction_client_id'); - if ($_column == null) return null; - final manager = $$TransactionsTableTableManager($_db, $_db.transactions) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = - $_typedResult.readTableOrNull(_expenseTransactionClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } - - static $TransactionsTable _incomeTransactionClientIdTable(_$AppDatabase db) => - db.transactions.createAlias($_aliasNameGenerator( - db.transfers.incomeTransactionClientId, db.transactions.clientId)); - - $$TransactionsTableProcessedTableManager? get incomeTransactionClientId { - final $_column = $_itemColumn('income_transaction_client_id'); - if ($_column == null) return null; - final manager = $$TransactionsTableTableManager($_db, $_db.transactions) - .filter((f) => f.clientId.sqlEquals($_column)); - final item = - $_typedResult.readTableOrNull(_incomeTransactionClientIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } -} - -class $$TransfersTableFilterComposer - extends Composer<_$AppDatabase, $TransfersTable> { - $$TransfersTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnFilters(column)); - - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); +final class $$BudgetablesTableReferences + extends BaseReferences<_$AppDatabase, $BudgetablesTable, Budgetable> { + $$BudgetablesTableReferences(super.$_db, super.$_table, super.$_typedResult); - ColumnFilters get amount => $composableBuilder( - column: $table.amount, builder: (column) => ColumnFilters(column)); + static $BudgetsTable _budgetClientIdTable(_$AppDatabase db) => + db.budgets.createAlias($_aliasNameGenerator( + db.budgetables.budgetClientId, db.budgets.clientId)); - ColumnFilters get fromWalletId => $composableBuilder( - column: $table.fromWalletId, builder: (column) => ColumnFilters(column)); + $$BudgetsTableProcessedTableManager get budgetClientId { + final $_column = $_itemColumn('budget_client_id')!; - ColumnFilters get toWalletId => $composableBuilder( - column: $table.toWalletId, builder: (column) => ColumnFilters(column)); + final manager = $$BudgetsTableTableManager($_db, $_db.budgets) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_budgetClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} - ColumnFilters get exchangeRate => $composableBuilder( - column: $table.exchangeRate, builder: (column) => ColumnFilters(column)); +class $$BudgetablesTableFilterComposer + extends Composer<_$AppDatabase, $BudgetablesTable> { + $$BudgetablesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnWithTypeConverterFilters + get targetType => $composableBuilder( + column: $table.targetType, + builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get datetime => $composableBuilder( - column: $table.datetime, builder: (column) => ColumnFilters(column)); + ColumnFilters get targetClientId => $composableBuilder( + column: $table.targetClientId, + builder: (column) => ColumnFilters(column)); - $$WalletsTableFilterComposer get fromWalletClientId { - final $$WalletsTableFilterComposer composer = $composerBuilder( + $$BudgetsTableFilterComposer get budgetClientId { + final $$BudgetsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.fromWalletClientId, - referencedTable: $db.wallets, + getCurrentColumn: (t) => t.budgetClientId, + referencedTable: $db.budgets, getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$WalletsTableFilterComposer( + $$BudgetsTableFilterComposer( $db: $db, - $table: $db.wallets, + $table: $db.budgets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10508,39 +13422,36 @@ class $$TransfersTableFilterComposer )); return composer; } +} - $$WalletsTableFilterComposer get toWalletClientId { - final $$WalletsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.toWalletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableFilterComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } +class $$BudgetablesTableOrderingComposer + extends Composer<_$AppDatabase, $BudgetablesTable> { + $$BudgetablesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get targetType => $composableBuilder( + column: $table.targetType, builder: (column) => ColumnOrderings(column)); - $$TransactionsTableFilterComposer get expenseTransactionClientId { - final $$TransactionsTableFilterComposer composer = $composerBuilder( + ColumnOrderings get targetClientId => $composableBuilder( + column: $table.targetClientId, + builder: (column) => ColumnOrderings(column)); + + $$BudgetsTableOrderingComposer get budgetClientId { + final $$BudgetsTableOrderingComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.expenseTransactionClientId, - referencedTable: $db.transactions, + getCurrentColumn: (t) => t.budgetClientId, + referencedTable: $db.budgets, getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableFilterComposer( + $$BudgetsTableOrderingComposer( $db: $db, - $table: $db.transactions, + $table: $db.budgets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10548,19 +13459,36 @@ class $$TransfersTableFilterComposer )); return composer; } +} - $$TransactionsTableFilterComposer get incomeTransactionClientId { - final $$TransactionsTableFilterComposer composer = $composerBuilder( +class $$BudgetablesTableAnnotationComposer + extends Composer<_$AppDatabase, $BudgetablesTable> { + $$BudgetablesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumnWithTypeConverter get targetType => + $composableBuilder( + column: $table.targetType, builder: (column) => column); + + GeneratedColumn get targetClientId => $composableBuilder( + column: $table.targetClientId, builder: (column) => column); + + $$BudgetsTableAnnotationComposer get budgetClientId { + final $$BudgetsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.incomeTransactionClientId, - referencedTable: $db.transactions, + getCurrentColumn: (t) => t.budgetClientId, + referencedTable: $db.budgets, getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableFilterComposer( + $$BudgetsTableAnnotationComposer( $db: $db, - $table: $db.transactions, + $table: $db.budgets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10570,109 +13498,232 @@ class $$TransfersTableFilterComposer } } -class $$TransfersTableOrderingComposer - extends Composer<_$AppDatabase, $TransfersTable> { - $$TransfersTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); +class $$BudgetablesTableTableManager extends RootTableManager< + _$AppDatabase, + $BudgetablesTable, + Budgetable, + $$BudgetablesTableFilterComposer, + $$BudgetablesTableOrderingComposer, + $$BudgetablesTableAnnotationComposer, + $$BudgetablesTableCreateCompanionBuilder, + $$BudgetablesTableUpdateCompanionBuilder, + (Budgetable, $$BudgetablesTableReferences), + Budgetable, + PrefetchHooks Function({bool budgetClientId})> { + $$BudgetablesTableTableManager(_$AppDatabase db, $BudgetablesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$BudgetablesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$BudgetablesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$BudgetablesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value budgetClientId = const Value.absent(), + Value targetType = const Value.absent(), + Value targetClientId = const Value.absent(), + Value rowid = const Value.absent(), + }) => + BudgetablesCompanion( + budgetClientId: budgetClientId, + targetType: targetType, + targetClientId: targetClientId, + rowid: rowid, + ), + createCompanionCallback: ({ + required String budgetClientId, + required BudgetTargetType targetType, + required String targetClientId, + Value rowid = const Value.absent(), + }) => + BudgetablesCompanion.insert( + budgetClientId: budgetClientId, + targetType: targetType, + targetClientId: targetClientId, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$BudgetablesTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({budgetClientId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (budgetClientId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.budgetClientId, + referencedTable: + $$BudgetablesTableReferences._budgetClientIdTable(db), + referencedColumn: $$BudgetablesTableReferences + ._budgetClientIdTable(db) + .clientId, + ) as T; + } - ColumnOrderings get clientId => $composableBuilder( - column: $table.clientId, builder: (column) => ColumnOrderings(column)); + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} - ColumnOrderings get rev => $composableBuilder( - column: $table.rev, builder: (column) => ColumnOrderings(column)); +typedef $$BudgetablesTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $BudgetablesTable, + Budgetable, + $$BudgetablesTableFilterComposer, + $$BudgetablesTableOrderingComposer, + $$BudgetablesTableAnnotationComposer, + $$BudgetablesTableCreateCompanionBuilder, + $$BudgetablesTableUpdateCompanionBuilder, + (Budgetable, $$BudgetablesTableReferences), + Budgetable, + PrefetchHooks Function({bool budgetClientId})>; +typedef $$BudgetPeriodStatesTableCreateCompanionBuilder + = BudgetPeriodStatesCompanion Function({ + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + required String budgetClientId, + required DateTime periodStart, + required DateTime periodEnd, + Value netSpent, + Value rolloverIn, + Value rolloverOut, + Value closedAt, + Value rowid, +}); +typedef $$BudgetPeriodStatesTableUpdateCompanionBuilder + = BudgetPeriodStatesCompanion Function({ + Value id, + Value userId, + Value clientId, + Value rev, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value lastSyncedAt, + Value budgetClientId, + Value periodStart, + Value periodEnd, + Value netSpent, + Value rolloverIn, + Value rolloverOut, + Value closedAt, + Value rowid, +}); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +final class $$BudgetPeriodStatesTableReferences extends BaseReferences< + _$AppDatabase, $BudgetPeriodStatesTable, BudgetPeriodState> { + $$BudgetPeriodStatesTableReferences( + super.$_db, super.$_table, super.$_typedResult); - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + static $BudgetsTable _budgetClientIdTable(_$AppDatabase db) => + db.budgets.createAlias($_aliasNameGenerator( + db.budgetPeriodStates.budgetClientId, db.budgets.clientId)); - ColumnOrderings get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + $$BudgetsTableProcessedTableManager get budgetClientId { + final $_column = $_itemColumn('budget_client_id')!; - ColumnOrderings get lastSyncedAt => $composableBuilder( - column: $table.lastSyncedAt, - builder: (column) => ColumnOrderings(column)); + final manager = $$BudgetsTableTableManager($_db, $_db.budgets) + .filter((f) => f.clientId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_budgetClientIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} - ColumnOrderings get amount => $composableBuilder( - column: $table.amount, builder: (column) => ColumnOrderings(column)); +class $$BudgetPeriodStatesTableFilterComposer + extends Composer<_$AppDatabase, $BudgetPeriodStatesTable> { + $$BudgetPeriodStatesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnOrderings get fromWalletId => $composableBuilder( - column: $table.fromWalletId, - builder: (column) => ColumnOrderings(column)); + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); - ColumnOrderings get toWalletId => $composableBuilder( - column: $table.toWalletId, builder: (column) => ColumnOrderings(column)); + ColumnFilters get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnFilters(column)); - ColumnOrderings get exchangeRate => $composableBuilder( - column: $table.exchangeRate, - builder: (column) => ColumnOrderings(column)); + ColumnFilters get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnFilters(column)); - ColumnOrderings get datetime => $composableBuilder( - column: $table.datetime, builder: (column) => ColumnOrderings(column)); + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); - $$WalletsTableOrderingComposer get fromWalletClientId { - final $$WalletsTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.fromWalletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableOrderingComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - $$WalletsTableOrderingComposer get toWalletClientId { - final $$WalletsTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.toWalletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableOrderingComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnFilters(column)); - $$TransactionsTableOrderingComposer get expenseTransactionClientId { - final $$TransactionsTableOrderingComposer composer = $composerBuilder( + ColumnFilters get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get periodStart => $composableBuilder( + column: $table.periodStart, builder: (column) => ColumnFilters(column)); + + ColumnFilters get periodEnd => $composableBuilder( + column: $table.periodEnd, builder: (column) => ColumnFilters(column)); + + ColumnFilters get netSpent => $composableBuilder( + column: $table.netSpent, builder: (column) => ColumnFilters(column)); + + ColumnFilters get rolloverIn => $composableBuilder( + column: $table.rolloverIn, builder: (column) => ColumnFilters(column)); + + ColumnFilters get rolloverOut => $composableBuilder( + column: $table.rolloverOut, builder: (column) => ColumnFilters(column)); + + ColumnFilters get closedAt => $composableBuilder( + column: $table.closedAt, builder: (column) => ColumnFilters(column)); + + $$BudgetsTableFilterComposer get budgetClientId { + final $$BudgetsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.expenseTransactionClientId, - referencedTable: $db.transactions, + getCurrentColumn: (t) => t.budgetClientId, + referencedTable: $db.budgets, getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableOrderingComposer( + $$BudgetsTableFilterComposer( $db: $db, - $table: $db.transactions, + $table: $db.budgets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10680,19 +13731,72 @@ class $$TransfersTableOrderingComposer )); return composer; } +} - $$TransactionsTableOrderingComposer get incomeTransactionClientId { - final $$TransactionsTableOrderingComposer composer = $composerBuilder( +class $$BudgetPeriodStatesTableOrderingComposer + extends Composer<_$AppDatabase, $BudgetPeriodStatesTable> { + $$BudgetPeriodStatesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get clientId => $composableBuilder( + column: $table.clientId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rev => $composableBuilder( + column: $table.rev, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastSyncedAt => $composableBuilder( + column: $table.lastSyncedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get periodStart => $composableBuilder( + column: $table.periodStart, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get periodEnd => $composableBuilder( + column: $table.periodEnd, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get netSpent => $composableBuilder( + column: $table.netSpent, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rolloverIn => $composableBuilder( + column: $table.rolloverIn, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rolloverOut => $composableBuilder( + column: $table.rolloverOut, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get closedAt => $composableBuilder( + column: $table.closedAt, builder: (column) => ColumnOrderings(column)); + + $$BudgetsTableOrderingComposer get budgetClientId { + final $$BudgetsTableOrderingComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.incomeTransactionClientId, - referencedTable: $db.transactions, + getCurrentColumn: (t) => t.budgetClientId, + referencedTable: $db.budgets, getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableOrderingComposer( + $$BudgetsTableOrderingComposer( $db: $db, - $table: $db.transactions, + $table: $db.budgets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10702,9 +13806,9 @@ class $$TransfersTableOrderingComposer } } -class $$TransfersTableAnnotationComposer - extends Composer<_$AppDatabase, $TransfersTable> { - $$TransfersTableAnnotationComposer({ +class $$BudgetPeriodStatesTableAnnotationComposer + extends Composer<_$AppDatabase, $BudgetPeriodStatesTable> { + $$BudgetPeriodStatesTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -10735,93 +13839,36 @@ class $$TransfersTableAnnotationComposer GeneratedColumn get lastSyncedAt => $composableBuilder( column: $table.lastSyncedAt, builder: (column) => column); - GeneratedColumn get amount => - $composableBuilder(column: $table.amount, builder: (column) => column); - - GeneratedColumn get fromWalletId => $composableBuilder( - column: $table.fromWalletId, builder: (column) => column); - - GeneratedColumn get toWalletId => $composableBuilder( - column: $table.toWalletId, builder: (column) => column); + GeneratedColumn get periodStart => $composableBuilder( + column: $table.periodStart, builder: (column) => column); - GeneratedColumn get exchangeRate => $composableBuilder( - column: $table.exchangeRate, builder: (column) => column); + GeneratedColumn get periodEnd => + $composableBuilder(column: $table.periodEnd, builder: (column) => column); - GeneratedColumn get datetime => - $composableBuilder(column: $table.datetime, builder: (column) => column); + GeneratedColumn get netSpent => + $composableBuilder(column: $table.netSpent, builder: (column) => column); - $$WalletsTableAnnotationComposer get fromWalletClientId { - final $$WalletsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.fromWalletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableAnnotationComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + GeneratedColumn get rolloverIn => $composableBuilder( + column: $table.rolloverIn, builder: (column) => column); - $$WalletsTableAnnotationComposer get toWalletClientId { - final $$WalletsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.toWalletClientId, - referencedTable: $db.wallets, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$WalletsTableAnnotationComposer( - $db: $db, - $table: $db.wallets, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + GeneratedColumn get rolloverOut => $composableBuilder( + column: $table.rolloverOut, builder: (column) => column); - $$TransactionsTableAnnotationComposer get expenseTransactionClientId { - final $$TransactionsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.expenseTransactionClientId, - referencedTable: $db.transactions, - getReferencedColumn: (t) => t.clientId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableAnnotationComposer( - $db: $db, - $table: $db.transactions, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } + GeneratedColumn get closedAt => + $composableBuilder(column: $table.closedAt, builder: (column) => column); - $$TransactionsTableAnnotationComposer get incomeTransactionClientId { - final $$TransactionsTableAnnotationComposer composer = $composerBuilder( + $$BudgetsTableAnnotationComposer get budgetClientId { + final $$BudgetsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.incomeTransactionClientId, - referencedTable: $db.transactions, + getCurrentColumn: (t) => t.budgetClientId, + referencedTable: $db.budgets, getReferencedColumn: (t) => t.clientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$TransactionsTableAnnotationComposer( + $$BudgetsTableAnnotationComposer( $db: $db, - $table: $db.transactions, + $table: $db.budgets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10831,32 +13878,30 @@ class $$TransfersTableAnnotationComposer } } -class $$TransfersTableTableManager extends RootTableManager< +class $$BudgetPeriodStatesTableTableManager extends RootTableManager< _$AppDatabase, - $TransfersTable, - Transfer, - $$TransfersTableFilterComposer, - $$TransfersTableOrderingComposer, - $$TransfersTableAnnotationComposer, - $$TransfersTableCreateCompanionBuilder, - $$TransfersTableUpdateCompanionBuilder, - (Transfer, $$TransfersTableReferences), - Transfer, - PrefetchHooks Function( - {bool fromWalletClientId, - bool toWalletClientId, - bool expenseTransactionClientId, - bool incomeTransactionClientId})> { - $$TransfersTableTableManager(_$AppDatabase db, $TransfersTable table) + $BudgetPeriodStatesTable, + BudgetPeriodState, + $$BudgetPeriodStatesTableFilterComposer, + $$BudgetPeriodStatesTableOrderingComposer, + $$BudgetPeriodStatesTableAnnotationComposer, + $$BudgetPeriodStatesTableCreateCompanionBuilder, + $$BudgetPeriodStatesTableUpdateCompanionBuilder, + (BudgetPeriodState, $$BudgetPeriodStatesTableReferences), + BudgetPeriodState, + PrefetchHooks Function({bool budgetClientId})> { + $$BudgetPeriodStatesTableTableManager( + _$AppDatabase db, $BudgetPeriodStatesTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$TransfersTableFilterComposer($db: db, $table: table), + $$BudgetPeriodStatesTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$TransfersTableOrderingComposer($db: db, $table: table), + $$BudgetPeriodStatesTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$TransfersTableAnnotationComposer($db: db, $table: table), + $$BudgetPeriodStatesTableAnnotationComposer( + $db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value userId = const Value.absent(), @@ -10866,18 +13911,16 @@ class $$TransfersTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - Value amount = const Value.absent(), - Value fromWalletId = const Value.absent(), - Value toWalletId = const Value.absent(), - Value fromWalletClientId = const Value.absent(), - Value toWalletClientId = const Value.absent(), - Value exchangeRate = const Value.absent(), - Value datetime = const Value.absent(), - Value expenseTransactionClientId = const Value.absent(), - Value incomeTransactionClientId = const Value.absent(), + Value budgetClientId = const Value.absent(), + Value periodStart = const Value.absent(), + Value periodEnd = const Value.absent(), + Value netSpent = const Value.absent(), + Value rolloverIn = const Value.absent(), + Value rolloverOut = const Value.absent(), + Value closedAt = const Value.absent(), Value rowid = const Value.absent(), }) => - TransfersCompanion( + BudgetPeriodStatesCompanion( id: id, userId: userId, clientId: clientId, @@ -10886,15 +13929,13 @@ class $$TransfersTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - amount: amount, - fromWalletId: fromWalletId, - toWalletId: toWalletId, - fromWalletClientId: fromWalletClientId, - toWalletClientId: toWalletClientId, - exchangeRate: exchangeRate, - datetime: datetime, - expenseTransactionClientId: expenseTransactionClientId, - incomeTransactionClientId: incomeTransactionClientId, + budgetClientId: budgetClientId, + periodStart: periodStart, + periodEnd: periodEnd, + netSpent: netSpent, + rolloverIn: rolloverIn, + rolloverOut: rolloverOut, + closedAt: closedAt, rowid: rowid, ), createCompanionCallback: ({ @@ -10906,18 +13947,16 @@ class $$TransfersTableTableManager extends RootTableManager< Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), Value lastSyncedAt = const Value.absent(), - required double amount, - Value fromWalletId = const Value.absent(), - Value toWalletId = const Value.absent(), - Value fromWalletClientId = const Value.absent(), - Value toWalletClientId = const Value.absent(), - Value exchangeRate = const Value.absent(), - required DateTime datetime, - Value expenseTransactionClientId = const Value.absent(), - Value incomeTransactionClientId = const Value.absent(), + required String budgetClientId, + required DateTime periodStart, + required DateTime periodEnd, + Value netSpent = const Value.absent(), + Value rolloverIn = const Value.absent(), + Value rolloverOut = const Value.absent(), + Value closedAt = const Value.absent(), Value rowid = const Value.absent(), }) => - TransfersCompanion.insert( + BudgetPeriodStatesCompanion.insert( id: id, userId: userId, clientId: clientId, @@ -10926,28 +13965,22 @@ class $$TransfersTableTableManager extends RootTableManager< updatedAt: updatedAt, deletedAt: deletedAt, lastSyncedAt: lastSyncedAt, - amount: amount, - fromWalletId: fromWalletId, - toWalletId: toWalletId, - fromWalletClientId: fromWalletClientId, - toWalletClientId: toWalletClientId, - exchangeRate: exchangeRate, - datetime: datetime, - expenseTransactionClientId: expenseTransactionClientId, - incomeTransactionClientId: incomeTransactionClientId, + budgetClientId: budgetClientId, + periodStart: periodStart, + periodEnd: periodEnd, + netSpent: netSpent, + rolloverIn: rolloverIn, + rolloverOut: rolloverOut, + closedAt: closedAt, rowid: rowid, ), withReferenceMapper: (p0) => p0 .map((e) => ( e.readTable(table), - $$TransfersTableReferences(db, table, e) + $$BudgetPeriodStatesTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: ( - {fromWalletClientId = false, - toWalletClientId = false, - expenseTransactionClientId = false, - incomeTransactionClientId = false}) { + prefetchHooksCallback: ({budgetClientId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -10964,47 +13997,14 @@ class $$TransfersTableTableManager extends RootTableManager< dynamic, dynamic, dynamic>>(state) { - if (fromWalletClientId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.fromWalletClientId, - referencedTable: - $$TransfersTableReferences._fromWalletClientIdTable(db), - referencedColumn: $$TransfersTableReferences - ._fromWalletClientIdTable(db) - .clientId, - ) as T; - } - if (toWalletClientId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.toWalletClientId, - referencedTable: - $$TransfersTableReferences._toWalletClientIdTable(db), - referencedColumn: $$TransfersTableReferences - ._toWalletClientIdTable(db) - .clientId, - ) as T; - } - if (expenseTransactionClientId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.expenseTransactionClientId, - referencedTable: $$TransfersTableReferences - ._expenseTransactionClientIdTable(db), - referencedColumn: $$TransfersTableReferences - ._expenseTransactionClientIdTable(db) - .clientId, - ) as T; - } - if (incomeTransactionClientId) { + if (budgetClientId) { state = state.withJoin( currentTable: table, - currentColumn: table.incomeTransactionClientId, - referencedTable: $$TransfersTableReferences - ._incomeTransactionClientIdTable(db), - referencedColumn: $$TransfersTableReferences - ._incomeTransactionClientIdTable(db) + currentColumn: table.budgetClientId, + referencedTable: $$BudgetPeriodStatesTableReferences + ._budgetClientIdTable(db), + referencedColumn: $$BudgetPeriodStatesTableReferences + ._budgetClientIdTable(db) .clientId, ) as T; } @@ -11019,22 +14019,18 @@ class $$TransfersTableTableManager extends RootTableManager< )); } -typedef $$TransfersTableProcessedTableManager = ProcessedTableManager< +typedef $$BudgetPeriodStatesTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $TransfersTable, - Transfer, - $$TransfersTableFilterComposer, - $$TransfersTableOrderingComposer, - $$TransfersTableAnnotationComposer, - $$TransfersTableCreateCompanionBuilder, - $$TransfersTableUpdateCompanionBuilder, - (Transfer, $$TransfersTableReferences), - Transfer, - PrefetchHooks Function( - {bool fromWalletClientId, - bool toWalletClientId, - bool expenseTransactionClientId, - bool incomeTransactionClientId})>; + $BudgetPeriodStatesTable, + BudgetPeriodState, + $$BudgetPeriodStatesTableFilterComposer, + $$BudgetPeriodStatesTableOrderingComposer, + $$BudgetPeriodStatesTableAnnotationComposer, + $$BudgetPeriodStatesTableCreateCompanionBuilder, + $$BudgetPeriodStatesTableUpdateCompanionBuilder, + (BudgetPeriodState, $$BudgetPeriodStatesTableReferences), + BudgetPeriodState, + PrefetchHooks Function({bool budgetClientId})>; class $AppDatabaseManager { final _$AppDatabase _db; @@ -11065,4 +14061,10 @@ class $AppDatabaseManager { $$MediaFilesTableTableManager(_db, _db.mediaFiles); $$TransfersTableTableManager get transfers => $$TransfersTableTableManager(_db, _db.transfers); + $$BudgetsTableTableManager get budgets => + $$BudgetsTableTableManager(_db, _db.budgets); + $$BudgetablesTableTableManager get budgetables => + $$BudgetablesTableTableManager(_db, _db.budgetables); + $$BudgetPeriodStatesTableTableManager get budgetPeriodStates => + $$BudgetPeriodStatesTableTableManager(_db, _db.budgetPeriodStates); } diff --git a/lib/data/database/tables/budget_period_states.dart b/lib/data/database/tables/budget_period_states.dart new file mode 100644 index 00000000..3f8301d0 --- /dev/null +++ b/lib/data/database/tables/budget_period_states.dart @@ -0,0 +1,14 @@ +import 'package:drift/drift.dart'; +import 'package:trakli/data/database/tables/budgets.dart'; +import 'package:trakli/data/database/tables/sync_table.dart'; + +@DataClassName('BudgetPeriodState') +class BudgetPeriodStates extends Table with SyncTable { + TextColumn get budgetClientId => text().references(Budgets, #clientId)(); + DateTimeColumn get periodStart => dateTime()(); + DateTimeColumn get periodEnd => dateTime()(); + RealColumn get netSpent => real().withDefault(const Constant(0.0))(); + RealColumn get rolloverIn => real().withDefault(const Constant(0.0))(); + RealColumn get rolloverOut => real().withDefault(const Constant(0.0))(); + DateTimeColumn get closedAt => dateTime().nullable()(); +} diff --git a/lib/data/database/tables/budgetables.dart b/lib/data/database/tables/budgetables.dart new file mode 100644 index 00000000..d5eb5eb7 --- /dev/null +++ b/lib/data/database/tables/budgetables.dart @@ -0,0 +1,14 @@ +import 'package:drift/drift.dart'; +import 'package:trakli/data/database/tables/budgets.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +@DataClassName('Budgetable') +class Budgetables extends Table { + TextColumn get budgetClientId => text().references(Budgets, #clientId)(); + TextColumn get targetType => textEnum()(); + TextColumn get targetClientId => text()(); + + @override + Set get primaryKey => + {budgetClientId, targetType, targetClientId}; +} diff --git a/lib/data/database/tables/budgets.dart b/lib/data/database/tables/budgets.dart new file mode 100644 index 00000000..d82d8e43 --- /dev/null +++ b/lib/data/database/tables/budgets.dart @@ -0,0 +1,24 @@ +import 'package:drift/drift.dart'; +import 'package:trakli/data/database/tables/sync_table.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +@DataClassName('Budget') +class Budgets extends Table with SyncTable { + TextColumn get name => text()(); + TextColumn get slug => text()(); + TextColumn get description => text().nullable()(); + RealColumn get amount => real()(); + TextColumn get currency => text()(); + TextColumn get periodType => textEnum()(); + DateTimeColumn get startDate => dateTime()(); + DateTimeColumn get endDate => dateTime().nullable()(); + BoolColumn get rolloverEnabled => + boolean().withDefault(const Constant(false))(); + IntColumn get thresholdPercent => integer().withDefault(const Constant(80))(); + BoolColumn get forecastAlertsEnabled => + boolean().withDefault(const Constant(false))(); + BoolColumn get isActive => boolean().withDefault(const Constant(true))(); + TextColumn get ownerType => + text().withDefault(const Constant('user'))(); + IntColumn get ownerId => integer().nullable()(); +} diff --git a/lib/data/datasources/budget/budget_local_datasource.dart b/lib/data/datasources/budget/budget_local_datasource.dart new file mode 100644 index 00000000..c1c066f9 --- /dev/null +++ b/lib/data/datasources/budget/budget_local_datasource.dart @@ -0,0 +1,342 @@ +import 'package:drift/drift.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/exceptions.dart'; +import 'package:trakli/core/utils/date_util.dart'; +import 'package:trakli/core/utils/id_helper.dart'; +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class BudgetTargetInput { + final BudgetTargetType type; + final String clientId; + const BudgetTargetInput({required this.type, required this.clientId}); +} + +class ResolvedBudgetTarget { + final BudgetTargetType type; + final String clientId; + final int? id; + final String? name; + const ResolvedBudgetTarget({ + required this.type, + required this.clientId, + this.id, + this.name, + }); +} + +abstract class BudgetLocalDataSource { + Future> getAllBudgets({bool? active}); + Future getBudgetByClientId(String clientId); + Future> getTargetsForBudget(String budgetClientId); + Future> getPeriodStatesForBudget( + String budgetClientId); + + Stream> watchAllBudgets({bool? active}); + Stream> watchTargetsForBudget(String budgetClientId); + Stream> watchPeriodStatesForBudget( + String budgetClientId); + + Future insertBudget({ + required String name, + required String slug, + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + DateTime? endDate, + String? description, + bool rolloverEnabled = false, + int thresholdPercent = 80, + bool forecastAlertsEnabled = false, + bool isActive = true, + List targets = const [], + }); + + Future updateBudget( + String clientId, { + String? name, + String? slug, + double? amount, + String? currency, + BudgetPeriodType? periodType, + DateTime? startDate, + DateTime? endDate, + String? description, + bool? rolloverEnabled, + int? thresholdPercent, + bool? forecastAlertsEnabled, + bool? isActive, + List? targets, + }); + + Future deleteBudget(String clientId); + + Future> getResolvedTargetsForBudget( + String budgetClientId); +} + +@Injectable(as: BudgetLocalDataSource) +class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { + BudgetLocalDataSourceImpl(this.database); + final AppDatabase database; + + @override + Future> getAllBudgets({bool? active}) async { + final query = database.select(database.budgets) + ..orderBy([(b) => OrderingTerm.desc(b.createdAt)]); + if (active != null) { + query.where((b) => b.isActive.equals(active)); + } + return query.get(); + } + + @override + Future getBudgetByClientId(String clientId) { + return (database.select(database.budgets) + ..where((b) => b.clientId.equals(clientId))) + .getSingleOrNull(); + } + + @override + Future> getTargetsForBudget(String budgetClientId) { + return (database.select(database.budgetables) + ..where((bt) => bt.budgetClientId.equals(budgetClientId))) + .get(); + } + + @override + Future> getPeriodStatesForBudget( + String budgetClientId) { + return (database.select(database.budgetPeriodStates) + ..where((ps) => ps.budgetClientId.equals(budgetClientId)) + ..orderBy([(ps) => OrderingTerm.desc(ps.periodStart)])) + .get(); + } + + @override + Stream> watchAllBudgets({bool? active}) { + final query = database.select(database.budgets) + ..orderBy([(b) => OrderingTerm.desc(b.createdAt)]); + if (active != null) { + query.where((b) => b.isActive.equals(active)); + } + return query.watch(); + } + + @override + Stream> watchTargetsForBudget(String budgetClientId) { + return (database.select(database.budgetables) + ..where((bt) => bt.budgetClientId.equals(budgetClientId))) + .watch(); + } + + @override + Stream> watchPeriodStatesForBudget( + String budgetClientId) { + return (database.select(database.budgetPeriodStates) + ..where((ps) => ps.budgetClientId.equals(budgetClientId)) + ..orderBy([(ps) => OrderingTerm.desc(ps.periodStart)])) + .watch(); + } + + @override + Future insertBudget({ + required String name, + required String slug, + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + DateTime? endDate, + String? description, + bool rolloverEnabled = false, + int thresholdPercent = 80, + bool forecastAlertsEnabled = false, + bool isActive = true, + List targets = const [], + }) async { + final existing = await (database.select(database.budgets) + ..where((b) => b.name.equals(name))) + .getSingleOrNull(); + if (existing != null) { + throw DuplicateException('Budget with name "$name" already exists'); + } + + final now = getNewFormattedUtcDateTime(); + final clientId = await generateDeviceScopedId(); + + return database.transaction(() async { + final inserted = await database.into(database.budgets).insertReturning( + BudgetsCompanion.insert( + clientId: Value(clientId), + name: name, + slug: slug, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: Value(endDate), + description: Value(description), + rolloverEnabled: Value(rolloverEnabled), + thresholdPercent: Value(thresholdPercent), + forecastAlertsEnabled: Value(forecastAlertsEnabled), + isActive: Value(isActive), + createdAt: Value(now), + updatedAt: Value(now), + ), + ); + + for (final t in targets) { + await database.into(database.budgetables).insert( + BudgetablesCompanion.insert( + budgetClientId: clientId, + targetType: t.type, + targetClientId: t.clientId, + ), + mode: InsertMode.insertOrReplace, + ); + } + + return inserted; + }); + } + + @override + Future updateBudget( + String clientId, { + String? name, + String? slug, + double? amount, + String? currency, + BudgetPeriodType? periodType, + DateTime? startDate, + DateTime? endDate, + String? description, + bool? rolloverEnabled, + int? thresholdPercent, + bool? forecastAlertsEnabled, + bool? isActive, + List? targets, + }) async { + if (name != null) { + final dupe = await (database.select(database.budgets) + ..where((b) => + b.name.equals(name) & b.clientId.isNotValue(clientId))) + .getSingleOrNull(); + if (dupe != null) { + throw DuplicateException('Budget with name "$name" already exists'); + } + } + + final now = getNewFormattedUtcDateTime(); + + return database.transaction(() async { + final updated = await (database.update(database.budgets) + ..where((b) => b.clientId.equals(clientId))) + .writeReturning( + BudgetsCompanion( + name: name != null ? Value(name) : const Value.absent(), + slug: slug != null ? Value(slug) : const Value.absent(), + amount: amount != null ? Value(amount) : const Value.absent(), + currency: currency != null ? Value(currency) : const Value.absent(), + periodType: periodType != null + ? Value(periodType) + : const Value.absent(), + startDate: + startDate != null ? Value(startDate) : const Value.absent(), + endDate: endDate != null ? Value(endDate) : const Value.absent(), + description: + description != null ? Value(description) : const Value.absent(), + rolloverEnabled: rolloverEnabled != null + ? Value(rolloverEnabled) + : const Value.absent(), + thresholdPercent: thresholdPercent != null + ? Value(thresholdPercent) + : const Value.absent(), + forecastAlertsEnabled: forecastAlertsEnabled != null + ? Value(forecastAlertsEnabled) + : const Value.absent(), + isActive: isActive != null ? Value(isActive) : const Value.absent(), + updatedAt: Value(now), + ), + ); + + if (targets != null) { + await (database.delete(database.budgetables) + ..where((bt) => bt.budgetClientId.equals(clientId))) + .go(); + for (final t in targets) { + await database.into(database.budgetables).insert( + BudgetablesCompanion.insert( + budgetClientId: clientId, + targetType: t.type, + targetClientId: t.clientId, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + + return updated.first; + }); + } + + @override + Future> getResolvedTargetsForBudget( + String budgetClientId) async { + final targetRows = await getTargetsForBudget(budgetClientId); + final out = []; + for (final row in targetRows) { + int? id; + String? name; + switch (row.targetType) { + case BudgetTargetType.category: + final c = await (database.select(database.categories) + ..where((t) => t.clientId.equals(row.targetClientId))) + .getSingleOrNull(); + id = c?.id; + name = c?.name; + break; + case BudgetTargetType.wallet: + final w = await (database.select(database.wallets) + ..where((t) => t.clientId.equals(row.targetClientId))) + .getSingleOrNull(); + id = w?.id; + name = w?.name; + break; + case BudgetTargetType.group: + final g = await (database.select(database.groups) + ..where((t) => t.clientId.equals(row.targetClientId))) + .getSingleOrNull(); + id = g?.id; + name = g?.name; + break; + } + out.add(ResolvedBudgetTarget( + type: row.targetType, + clientId: row.targetClientId, + id: id, + name: name, + )); + } + return out; + } + + @override + Future deleteBudget(String clientId) async { + final row = await (database.select(database.budgets) + ..where((b) => b.clientId.equals(clientId))) + .getSingle(); + await database.transaction(() async { + await (database.delete(database.budgetables) + ..where((bt) => bt.budgetClientId.equals(clientId))) + .go(); + await (database.delete(database.budgetPeriodStates) + ..where((ps) => ps.budgetClientId.equals(clientId))) + .go(); + await database.delete(database.budgets).delete(row); + }); + return row; + } +} diff --git a/lib/data/datasources/budget/budget_remote_datasource.dart b/lib/data/datasources/budget/budget_remote_datasource.dart new file mode 100644 index 00000000..31e8ed80 --- /dev/null +++ b/lib/data/datasources/budget/budget_remote_datasource.dart @@ -0,0 +1,191 @@ +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/utils/date_util.dart'; +import 'package:trakli/core/utils/json_defaults.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_period_state_dto.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; +import 'package:trakli/data/datasources/core/api_response.dart'; +import 'package:trakli/data/datasources/core/pagination_response.dart'; + +abstract class BudgetRemoteDataSource { + Future> getAllBudgets({ + DateTime? syncedSince, + bool? noClientId, + bool? active, + }); + Future getBudget(int id); + Future insertBudget(BudgetCompleteDto dto); + Future updateBudget(BudgetCompleteDto dto); + Future deleteBudget(int id); + Future getBudgetProgress(int id); + Future getBudgetTransactions( + int id, { + int limit = 50, + }); + Future closeBudgetPeriod(int id); + Future> getAllPeriodStates({ + DateTime? syncedSince, + bool? noClientId, + }); +} + +@Injectable(as: BudgetRemoteDataSource) +class BudgetRemoteDataSourceImpl implements BudgetRemoteDataSource { + final Dio dio; + + BudgetRemoteDataSourceImpl({required this.dio}); + + @override + Future> getAllBudgets({ + DateTime? syncedSince, + bool? noClientId, + bool? active, + }) async { + final allItems = []; + int currentPage = 1; + + while (true) { + final queryParams = {'page': currentPage}; + if (syncedSince != null) { + queryParams['synced_since'] = + formatServerIsoDateTimeString(syncedSince); + } + if (noClientId != null) { + queryParams['no_client_id'] = noClientId; + } + if (active != null) { + queryParams['active'] = active; + } + + final response = await dio.get('budgets', queryParameters: queryParams); + final apiResponse = ApiResponse.fromJson(response.data); + + final paginated = PaginationResponse.fromJson( + apiResponse.data as Map, + (Object? json) => + BudgetCompleteDto.fromServerJson(json! as Map), + ); + + allItems.addAll(paginated.data); + + if (!paginated.hasMore) break; + currentPage++; + } + + return allItems; + } + + @override + Future getBudget(int id) async { + final response = await dio.get('budgets/$id'); + if (response.data == null) return null; + + final apiResponse = ApiResponse.fromJson(response.data); + return BudgetCompleteDto.fromServerJson( + apiResponse.data as Map, + ); + } + + @override + Future insertBudget(BudgetCompleteDto dto) async { + final response = await dio.post('budgets', data: dto.toServerJson()); + final apiResponse = ApiResponse.fromJson(response.data); + return BudgetCompleteDto.fromServerJson( + apiResponse.data as Map, + ); + } + + @override + Future updateBudget(BudgetCompleteDto dto) async { + final response = await dio.put( + 'budgets/${dto.budget.id}', + data: dto.toServerJson(), + ); + final apiResponse = ApiResponse.fromJson(response.data); + return BudgetCompleteDto.fromServerJson( + apiResponse.data as Map, + ); + } + + @override + Future deleteBudget(int id) async { + await dio.delete('budgets/$id'); + } + + @override + Future getBudgetProgress(int id) async { + final response = await dio.get('budgets/$id/progress'); + if (response.data == null) return null; + final apiResponse = ApiResponse.fromJson(response.data); + return BudgetProgressDto.fromJson( + apiResponse.data as Map, + ); + } + + @override + Future getBudgetTransactions( + int id, { + int limit = 50, + }) async { + final response = await dio.get( + 'budgets/$id/transactions', + queryParameters: {'limit': limit}, + ); + if (response.data == null) return null; + final apiResponse = ApiResponse.fromJson(response.data); + return BudgetTransactionsResponse.fromJson( + apiResponse.data as Map, + ); + } + + @override + Future closeBudgetPeriod(int id) async { + final response = await dio.post('budgets/$id/close-period'); + if (response.data == null) return null; + final apiResponse = ApiResponse.fromJson(response.data); + return BudgetCompleteDto.fromServerJson( + apiResponse.data as Map, + ); + } + + @override + Future> getAllPeriodStates({ + DateTime? syncedSince, + bool? noClientId, + }) async { + final allItems = []; + int currentPage = 1; + + while (true) { + final queryParams = {'page': currentPage}; + if (syncedSince != null) { + queryParams['synced_since'] = + formatServerIsoDateTimeString(syncedSince); + } + if (noClientId != null) { + queryParams['no_client_id'] = noClientId; + } + + final response = await dio.get( + 'budget-period-states', + queryParameters: queryParams, + ); + final apiResponse = ApiResponse.fromJson(response.data); + + final paginated = PaginationResponse.fromJson( + apiResponse.data as Map, + (Object? json) => BudgetPeriodStateDto.fromJson( + JsonDefaultsHelper.addDefaults(json! as Map), + ), + ); + + allItems.addAll(paginated.data); + if (!paginated.hasMore) break; + currentPage++; + } + + return allItems; + } +} diff --git a/lib/data/datasources/budget/dtos/budget_complete_dto.dart b/lib/data/datasources/budget/dtos/budget_complete_dto.dart new file mode 100644 index 00000000..1edd2305 --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_complete_dto.dart @@ -0,0 +1,67 @@ +import 'package:trakli/core/utils/date_util.dart'; +import 'package:trakli/core/utils/json_defaults.dart'; +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_target_dto.dart'; + +class BudgetCompleteDto { + final Budget budget; + final List targets; + final BudgetProgressDto? progress; + + const BudgetCompleteDto({ + required this.budget, + this.targets = const [], + this.progress, + }); + + factory BudgetCompleteDto.fromServerJson(Map json) { + final budget = Budget.fromJson(JsonDefaultsHelper.addDefaults(json)); + + final rawTargets = json['targets']; + final targets = (rawTargets is List) + ? rawTargets + .whereType>() + .map(BudgetTargetDto.fromJson) + .toList() + : []; + + final rawProgress = json['progress']; + final progress = rawProgress is Map + ? BudgetProgressDto.fromJson(rawProgress) + : null; + + return BudgetCompleteDto( + budget: budget, + targets: targets, + progress: progress, + ); + } + + Map toServerJson() { + return { + if (budget.clientId.isNotEmpty) 'client_id': budget.clientId, + 'name': budget.name, + if (budget.description != null && budget.description!.trim().isNotEmpty) + 'description': budget.description, + 'amount': budget.amount, + 'currency': budget.currency, + 'period_type': budget.periodType.serverKey, + 'start_date': formatServerIsoDateTimeString(budget.startDate), + if (budget.endDate != null) + 'end_date': formatServerIsoDateTimeString(budget.endDate!), + 'rollover_enabled': budget.rolloverEnabled, + 'threshold_percent': budget.thresholdPercent, + 'forecast_alerts_enabled': budget.forecastAlertsEnabled, + 'is_active': budget.isActive, + 'targets': targets + .map((t) => { + 'type': t.type.serverKey, + if (t.id != null) 'id': t.id, + if (t.clientId != null && t.clientId!.isNotEmpty) + 'client_id': t.clientId, + }) + .toList(), + }; + } +} diff --git a/lib/data/datasources/budget/dtos/budget_period_state_dto.dart b/lib/data/datasources/budget/dtos/budget_period_state_dto.dart new file mode 100644 index 00000000..784d34ab --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_period_state_dto.dart @@ -0,0 +1,35 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/data/datasources/core/util.dart'; +import 'package:trakli/data/database/tables/sync_table.dart'; + +part 'budget_period_state_dto.freezed.dart'; +part 'budget_period_state_dto.g.dart'; + +@freezed +class BudgetPeriodStateDto with _$BudgetPeriodStateDto { + const factory BudgetPeriodStateDto({ + int? id, + @JsonKey(name: 'budget_id') int? budgetId, + @JsonKey(name: 'budget_client_generated_id') + String? budgetClientGeneratedId, + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + required String clientId, + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + required DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + required DateTime periodEnd, + @JsonKey(name: 'net_spent') @Default(0.0) double netSpent, + @JsonKey(name: 'rollover_in') @Default(0.0) double rolloverIn, + @JsonKey(name: 'rollover_out') @Default(0.0) double rolloverOut, + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) DateTime? closedAt, + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + DateTime? lastSyncedAt, + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + DateTime? updatedAt, + }) = _BudgetPeriodStateDto; + + factory BudgetPeriodStateDto.fromJson(Map json) => + _$BudgetPeriodStateDtoFromJson(json); +} diff --git a/lib/data/datasources/budget/dtos/budget_period_state_dto.freezed.dart b/lib/data/datasources/budget/dtos/budget_period_state_dto.freezed.dart new file mode 100644 index 00000000..c3069ba6 --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_period_state_dto.freezed.dart @@ -0,0 +1,501 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_period_state_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +BudgetPeriodStateDto _$BudgetPeriodStateDtoFromJson(Map json) { + return _BudgetPeriodStateDto.fromJson(json); +} + +/// @nodoc +mixin _$BudgetPeriodStateDto { + int? get id => throw _privateConstructorUsedError; + @JsonKey(name: 'budget_id') + int? get budgetId => throw _privateConstructorUsedError; + @JsonKey(name: 'budget_client_generated_id') + String? get budgetClientGeneratedId => throw _privateConstructorUsedError; + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + String get clientId => throw _privateConstructorUsedError; + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime get periodStart => throw _privateConstructorUsedError; + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + DateTime get periodEnd => throw _privateConstructorUsedError; + @JsonKey(name: 'net_spent') + double get netSpent => throw _privateConstructorUsedError; + @JsonKey(name: 'rollover_in') + double get rolloverIn => throw _privateConstructorUsedError; + @JsonKey(name: 'rollover_out') + double get rolloverOut => throw _privateConstructorUsedError; + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) + DateTime? get closedAt => throw _privateConstructorUsedError; + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + DateTime? get lastSyncedAt => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) + DateTime? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this BudgetPeriodStateDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of BudgetPeriodStateDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetPeriodStateDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetPeriodStateDtoCopyWith<$Res> { + factory $BudgetPeriodStateDtoCopyWith(BudgetPeriodStateDto value, + $Res Function(BudgetPeriodStateDto) then) = + _$BudgetPeriodStateDtoCopyWithImpl<$Res, BudgetPeriodStateDto>; + @useResult + $Res call( + {int? id, + @JsonKey(name: 'budget_id') int? budgetId, + @JsonKey(name: 'budget_client_generated_id') + String? budgetClientGeneratedId, + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + String clientId, + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) DateTime periodEnd, + @JsonKey(name: 'net_spent') double netSpent, + @JsonKey(name: 'rollover_in') double rolloverIn, + @JsonKey(name: 'rollover_out') double rolloverOut, + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) + DateTime? closedAt, + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + DateTime? lastSyncedAt, + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + DateTime? updatedAt}); +} + +/// @nodoc +class _$BudgetPeriodStateDtoCopyWithImpl<$Res, + $Val extends BudgetPeriodStateDto> + implements $BudgetPeriodStateDtoCopyWith<$Res> { + _$BudgetPeriodStateDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetPeriodStateDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? budgetId = freezed, + Object? budgetClientGeneratedId = freezed, + Object? clientId = null, + Object? periodStart = null, + Object? periodEnd = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? rolloverOut = null, + Object? closedAt = freezed, + Object? lastSyncedAt = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then(_value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + budgetId: freezed == budgetId + ? _value.budgetId + : budgetId // ignore: cast_nullable_to_non_nullable + as int?, + budgetClientGeneratedId: freezed == budgetClientGeneratedId + ? _value.budgetClientGeneratedId + : budgetClientGeneratedId // ignore: cast_nullable_to_non_nullable + as String?, + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + rolloverOut: null == rolloverOut + ? _value.rolloverOut + : rolloverOut // ignore: cast_nullable_to_non_nullable + as double, + closedAt: freezed == closedAt + ? _value.closedAt + : closedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastSyncedAt: freezed == lastSyncedAt + ? _value.lastSyncedAt + : lastSyncedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BudgetPeriodStateDtoImplCopyWith<$Res> + implements $BudgetPeriodStateDtoCopyWith<$Res> { + factory _$$BudgetPeriodStateDtoImplCopyWith(_$BudgetPeriodStateDtoImpl value, + $Res Function(_$BudgetPeriodStateDtoImpl) then) = + __$$BudgetPeriodStateDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int? id, + @JsonKey(name: 'budget_id') int? budgetId, + @JsonKey(name: 'budget_client_generated_id') + String? budgetClientGeneratedId, + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + String clientId, + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) DateTime periodEnd, + @JsonKey(name: 'net_spent') double netSpent, + @JsonKey(name: 'rollover_in') double rolloverIn, + @JsonKey(name: 'rollover_out') double rolloverOut, + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) + DateTime? closedAt, + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + DateTime? lastSyncedAt, + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + DateTime? updatedAt}); +} + +/// @nodoc +class __$$BudgetPeriodStateDtoImplCopyWithImpl<$Res> + extends _$BudgetPeriodStateDtoCopyWithImpl<$Res, _$BudgetPeriodStateDtoImpl> + implements _$$BudgetPeriodStateDtoImplCopyWith<$Res> { + __$$BudgetPeriodStateDtoImplCopyWithImpl(_$BudgetPeriodStateDtoImpl _value, + $Res Function(_$BudgetPeriodStateDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetPeriodStateDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? budgetId = freezed, + Object? budgetClientGeneratedId = freezed, + Object? clientId = null, + Object? periodStart = null, + Object? periodEnd = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? rolloverOut = null, + Object? closedAt = freezed, + Object? lastSyncedAt = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then(_$BudgetPeriodStateDtoImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + budgetId: freezed == budgetId + ? _value.budgetId + : budgetId // ignore: cast_nullable_to_non_nullable + as int?, + budgetClientGeneratedId: freezed == budgetClientGeneratedId + ? _value.budgetClientGeneratedId + : budgetClientGeneratedId // ignore: cast_nullable_to_non_nullable + as String?, + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + rolloverOut: null == rolloverOut + ? _value.rolloverOut + : rolloverOut // ignore: cast_nullable_to_non_nullable + as double, + closedAt: freezed == closedAt + ? _value.closedAt + : closedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastSyncedAt: freezed == lastSyncedAt + ? _value.lastSyncedAt + : lastSyncedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$BudgetPeriodStateDtoImpl implements _BudgetPeriodStateDto { + const _$BudgetPeriodStateDtoImpl( + {this.id, + @JsonKey(name: 'budget_id') this.budgetId, + @JsonKey(name: 'budget_client_generated_id') this.budgetClientGeneratedId, + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + required this.clientId, + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + required this.periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + required this.periodEnd, + @JsonKey(name: 'net_spent') this.netSpent = 0.0, + @JsonKey(name: 'rollover_in') this.rolloverIn = 0.0, + @JsonKey(name: 'rollover_out') this.rolloverOut = 0.0, + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) this.closedAt, + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + this.lastSyncedAt, + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) this.createdAt, + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + this.updatedAt}); + + factory _$BudgetPeriodStateDtoImpl.fromJson(Map json) => + _$$BudgetPeriodStateDtoImplFromJson(json); + + @override + final int? id; + @override + @JsonKey(name: 'budget_id') + final int? budgetId; + @override + @JsonKey(name: 'budget_client_generated_id') + final String? budgetClientGeneratedId; + @override + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + final String clientId; + @override + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + final DateTime periodStart; + @override + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + final DateTime periodEnd; + @override + @JsonKey(name: 'net_spent') + final double netSpent; + @override + @JsonKey(name: 'rollover_in') + final double rolloverIn; + @override + @JsonKey(name: 'rollover_out') + final double rolloverOut; + @override + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) + final DateTime? closedAt; + @override + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + final DateTime? lastSyncedAt; + @override + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) + final DateTime? createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + final DateTime? updatedAt; + + @override + String toString() { + return 'BudgetPeriodStateDto(id: $id, budgetId: $budgetId, budgetClientGeneratedId: $budgetClientGeneratedId, clientId: $clientId, periodStart: $periodStart, periodEnd: $periodEnd, netSpent: $netSpent, rolloverIn: $rolloverIn, rolloverOut: $rolloverOut, closedAt: $closedAt, lastSyncedAt: $lastSyncedAt, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetPeriodStateDtoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.budgetId, budgetId) || + other.budgetId == budgetId) && + (identical( + other.budgetClientGeneratedId, budgetClientGeneratedId) || + other.budgetClientGeneratedId == budgetClientGeneratedId) && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.periodStart, periodStart) || + other.periodStart == periodStart) && + (identical(other.periodEnd, periodEnd) || + other.periodEnd == periodEnd) && + (identical(other.netSpent, netSpent) || + other.netSpent == netSpent) && + (identical(other.rolloverIn, rolloverIn) || + other.rolloverIn == rolloverIn) && + (identical(other.rolloverOut, rolloverOut) || + other.rolloverOut == rolloverOut) && + (identical(other.closedAt, closedAt) || + other.closedAt == closedAt) && + (identical(other.lastSyncedAt, lastSyncedAt) || + other.lastSyncedAt == lastSyncedAt) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + budgetId, + budgetClientGeneratedId, + clientId, + periodStart, + periodEnd, + netSpent, + rolloverIn, + rolloverOut, + closedAt, + lastSyncedAt, + createdAt, + updatedAt); + + /// Create a copy of BudgetPeriodStateDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetPeriodStateDtoImplCopyWith<_$BudgetPeriodStateDtoImpl> + get copyWith => + __$$BudgetPeriodStateDtoImplCopyWithImpl<_$BudgetPeriodStateDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$BudgetPeriodStateDtoImplToJson( + this, + ); + } +} + +abstract class _BudgetPeriodStateDto implements BudgetPeriodStateDto { + const factory _BudgetPeriodStateDto( + {final int? id, + @JsonKey(name: 'budget_id') final int? budgetId, + @JsonKey(name: 'budget_client_generated_id') + final String? budgetClientGeneratedId, + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + required final String clientId, + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + required final DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + required final DateTime periodEnd, + @JsonKey(name: 'net_spent') final double netSpent, + @JsonKey(name: 'rollover_in') final double rolloverIn, + @JsonKey(name: 'rollover_out') final double rolloverOut, + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) + final DateTime? closedAt, + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + final DateTime? lastSyncedAt, + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) + final DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + final DateTime? updatedAt}) = _$BudgetPeriodStateDtoImpl; + + factory _BudgetPeriodStateDto.fromJson(Map json) = + _$BudgetPeriodStateDtoImpl.fromJson; + + @override + int? get id; + @override + @JsonKey(name: 'budget_id') + int? get budgetId; + @override + @JsonKey(name: 'budget_client_generated_id') + String? get budgetClientGeneratedId; + @override + @JsonKey(name: 'client_generated_id', defaultValue: defaultClientId) + String get clientId; + @override + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime get periodStart; + @override + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + DateTime get periodEnd; + @override + @JsonKey(name: 'net_spent') + double get netSpent; + @override + @JsonKey(name: 'rollover_in') + double get rolloverIn; + @override + @JsonKey(name: 'rollover_out') + double get rolloverOut; + @override + @JsonKey(name: 'closed_at', fromJson: safeParseDateTime) + DateTime? get closedAt; + @override + @JsonKey(name: 'last_synced_at', fromJson: safeParseDateTime) + DateTime? get lastSyncedAt; + @override + @JsonKey(name: 'created_at', fromJson: safeParseDateTime) + DateTime? get createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: safeParseDateTime) + DateTime? get updatedAt; + + /// Create a copy of BudgetPeriodStateDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetPeriodStateDtoImplCopyWith<_$BudgetPeriodStateDtoImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/data/datasources/budget/dtos/budget_period_state_dto.g.dart b/lib/data/datasources/budget/dtos/budget_period_state_dto.g.dart new file mode 100644 index 00000000..25a83948 --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_period_state_dto.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'budget_period_state_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$BudgetPeriodStateDtoImpl _$$BudgetPeriodStateDtoImplFromJson( + Map json) => + _$BudgetPeriodStateDtoImpl( + id: (json['id'] as num?)?.toInt(), + budgetId: (json['budget_id'] as num?)?.toInt(), + budgetClientGeneratedId: json['budget_client_generated_id'] as String?, + clientId: json['client_generated_id'] as String? ?? '', + periodStart: DateTime.parse(json['period_start'] as String), + periodEnd: DateTime.parse(json['period_end'] as String), + netSpent: (json['net_spent'] as num?)?.toDouble() ?? 0.0, + rolloverIn: (json['rollover_in'] as num?)?.toDouble() ?? 0.0, + rolloverOut: (json['rollover_out'] as num?)?.toDouble() ?? 0.0, + closedAt: safeParseDateTime(json['closed_at']), + lastSyncedAt: safeParseDateTime(json['last_synced_at']), + createdAt: safeParseDateTime(json['created_at']), + updatedAt: safeParseDateTime(json['updated_at']), + ); + +Map _$$BudgetPeriodStateDtoImplToJson( + _$BudgetPeriodStateDtoImpl instance) => + { + 'id': instance.id, + 'budget_id': instance.budgetId, + 'budget_client_generated_id': instance.budgetClientGeneratedId, + 'client_generated_id': instance.clientId, + 'period_start': instance.periodStart.toIso8601String(), + 'period_end': instance.periodEnd.toIso8601String(), + 'net_spent': instance.netSpent, + 'rollover_in': instance.rolloverIn, + 'rollover_out': instance.rolloverOut, + 'closed_at': instance.closedAt?.toIso8601String(), + 'last_synced_at': instance.lastSyncedAt?.toIso8601String(), + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + }; diff --git a/lib/data/datasources/budget/dtos/budget_progress_dto.dart b/lib/data/datasources/budget/dtos/budget_progress_dto.dart new file mode 100644 index 00000000..2464ec28 --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_progress_dto.dart @@ -0,0 +1,30 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +part 'budget_progress_dto.freezed.dart'; +part 'budget_progress_dto.g.dart'; + +@freezed +class BudgetProgressDto with _$BudgetProgressDto { + const factory BudgetProgressDto({ + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + required DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + required DateTime periodEnd, + required double limit, + @JsonKey(name: 'gross_spent') required double grossSpent, + required double refunds, + @JsonKey(name: 'net_spent') required double netSpent, + @JsonKey(name: 'rollover_in') required double rolloverIn, + @JsonKey(name: 'effective_limit') required double effectiveLimit, + required double remaining, + @JsonKey(name: 'percent_used') required double percentUsed, + @JsonKey(name: 'projected_spend') required double projectedSpend, + required BudgetStatus status, + @JsonKey(name: 'is_threshold_crossed') required bool isThresholdCrossed, + @JsonKey(name: 'is_forecast_breach') required bool isForecastBreach, + }) = _BudgetProgressDto; + + factory BudgetProgressDto.fromJson(Map json) => + _$BudgetProgressDtoFromJson(json); +} diff --git a/lib/data/datasources/budget/dtos/budget_progress_dto.freezed.dart b/lib/data/datasources/budget/dtos/budget_progress_dto.freezed.dart new file mode 100644 index 00000000..faad2a65 --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_progress_dto.freezed.dart @@ -0,0 +1,493 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_progress_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +BudgetProgressDto _$BudgetProgressDtoFromJson(Map json) { + return _BudgetProgressDto.fromJson(json); +} + +/// @nodoc +mixin _$BudgetProgressDto { + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime get periodStart => throw _privateConstructorUsedError; + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + DateTime get periodEnd => throw _privateConstructorUsedError; + double get limit => throw _privateConstructorUsedError; + @JsonKey(name: 'gross_spent') + double get grossSpent => throw _privateConstructorUsedError; + double get refunds => throw _privateConstructorUsedError; + @JsonKey(name: 'net_spent') + double get netSpent => throw _privateConstructorUsedError; + @JsonKey(name: 'rollover_in') + double get rolloverIn => throw _privateConstructorUsedError; + @JsonKey(name: 'effective_limit') + double get effectiveLimit => throw _privateConstructorUsedError; + double get remaining => throw _privateConstructorUsedError; + @JsonKey(name: 'percent_used') + double get percentUsed => throw _privateConstructorUsedError; + @JsonKey(name: 'projected_spend') + double get projectedSpend => throw _privateConstructorUsedError; + BudgetStatus get status => throw _privateConstructorUsedError; + @JsonKey(name: 'is_threshold_crossed') + bool get isThresholdCrossed => throw _privateConstructorUsedError; + @JsonKey(name: 'is_forecast_breach') + bool get isForecastBreach => throw _privateConstructorUsedError; + + /// Serializes this BudgetProgressDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of BudgetProgressDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetProgressDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetProgressDtoCopyWith<$Res> { + factory $BudgetProgressDtoCopyWith( + BudgetProgressDto value, $Res Function(BudgetProgressDto) then) = + _$BudgetProgressDtoCopyWithImpl<$Res, BudgetProgressDto>; + @useResult + $Res call( + {@JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) DateTime periodEnd, + double limit, + @JsonKey(name: 'gross_spent') double grossSpent, + double refunds, + @JsonKey(name: 'net_spent') double netSpent, + @JsonKey(name: 'rollover_in') double rolloverIn, + @JsonKey(name: 'effective_limit') double effectiveLimit, + double remaining, + @JsonKey(name: 'percent_used') double percentUsed, + @JsonKey(name: 'projected_spend') double projectedSpend, + BudgetStatus status, + @JsonKey(name: 'is_threshold_crossed') bool isThresholdCrossed, + @JsonKey(name: 'is_forecast_breach') bool isForecastBreach}); +} + +/// @nodoc +class _$BudgetProgressDtoCopyWithImpl<$Res, $Val extends BudgetProgressDto> + implements $BudgetProgressDtoCopyWith<$Res> { + _$BudgetProgressDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetProgressDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? periodStart = null, + Object? periodEnd = null, + Object? limit = null, + Object? grossSpent = null, + Object? refunds = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? effectiveLimit = null, + Object? remaining = null, + Object? percentUsed = null, + Object? projectedSpend = null, + Object? status = null, + Object? isThresholdCrossed = null, + Object? isForecastBreach = null, + }) { + return _then(_value.copyWith( + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as double, + grossSpent: null == grossSpent + ? _value.grossSpent + : grossSpent // ignore: cast_nullable_to_non_nullable + as double, + refunds: null == refunds + ? _value.refunds + : refunds // ignore: cast_nullable_to_non_nullable + as double, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + effectiveLimit: null == effectiveLimit + ? _value.effectiveLimit + : effectiveLimit // ignore: cast_nullable_to_non_nullable + as double, + remaining: null == remaining + ? _value.remaining + : remaining // ignore: cast_nullable_to_non_nullable + as double, + percentUsed: null == percentUsed + ? _value.percentUsed + : percentUsed // ignore: cast_nullable_to_non_nullable + as double, + projectedSpend: null == projectedSpend + ? _value.projectedSpend + : projectedSpend // ignore: cast_nullable_to_non_nullable + as double, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as BudgetStatus, + isThresholdCrossed: null == isThresholdCrossed + ? _value.isThresholdCrossed + : isThresholdCrossed // ignore: cast_nullable_to_non_nullable + as bool, + isForecastBreach: null == isForecastBreach + ? _value.isForecastBreach + : isForecastBreach // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BudgetProgressDtoImplCopyWith<$Res> + implements $BudgetProgressDtoCopyWith<$Res> { + factory _$$BudgetProgressDtoImplCopyWith(_$BudgetProgressDtoImpl value, + $Res Function(_$BudgetProgressDtoImpl) then) = + __$$BudgetProgressDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) DateTime periodEnd, + double limit, + @JsonKey(name: 'gross_spent') double grossSpent, + double refunds, + @JsonKey(name: 'net_spent') double netSpent, + @JsonKey(name: 'rollover_in') double rolloverIn, + @JsonKey(name: 'effective_limit') double effectiveLimit, + double remaining, + @JsonKey(name: 'percent_used') double percentUsed, + @JsonKey(name: 'projected_spend') double projectedSpend, + BudgetStatus status, + @JsonKey(name: 'is_threshold_crossed') bool isThresholdCrossed, + @JsonKey(name: 'is_forecast_breach') bool isForecastBreach}); +} + +/// @nodoc +class __$$BudgetProgressDtoImplCopyWithImpl<$Res> + extends _$BudgetProgressDtoCopyWithImpl<$Res, _$BudgetProgressDtoImpl> + implements _$$BudgetProgressDtoImplCopyWith<$Res> { + __$$BudgetProgressDtoImplCopyWithImpl(_$BudgetProgressDtoImpl _value, + $Res Function(_$BudgetProgressDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetProgressDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? periodStart = null, + Object? periodEnd = null, + Object? limit = null, + Object? grossSpent = null, + Object? refunds = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? effectiveLimit = null, + Object? remaining = null, + Object? percentUsed = null, + Object? projectedSpend = null, + Object? status = null, + Object? isThresholdCrossed = null, + Object? isForecastBreach = null, + }) { + return _then(_$BudgetProgressDtoImpl( + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as double, + grossSpent: null == grossSpent + ? _value.grossSpent + : grossSpent // ignore: cast_nullable_to_non_nullable + as double, + refunds: null == refunds + ? _value.refunds + : refunds // ignore: cast_nullable_to_non_nullable + as double, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + effectiveLimit: null == effectiveLimit + ? _value.effectiveLimit + : effectiveLimit // ignore: cast_nullable_to_non_nullable + as double, + remaining: null == remaining + ? _value.remaining + : remaining // ignore: cast_nullable_to_non_nullable + as double, + percentUsed: null == percentUsed + ? _value.percentUsed + : percentUsed // ignore: cast_nullable_to_non_nullable + as double, + projectedSpend: null == projectedSpend + ? _value.projectedSpend + : projectedSpend // ignore: cast_nullable_to_non_nullable + as double, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as BudgetStatus, + isThresholdCrossed: null == isThresholdCrossed + ? _value.isThresholdCrossed + : isThresholdCrossed // ignore: cast_nullable_to_non_nullable + as bool, + isForecastBreach: null == isForecastBreach + ? _value.isForecastBreach + : isForecastBreach // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$BudgetProgressDtoImpl implements _BudgetProgressDto { + const _$BudgetProgressDtoImpl( + {@JsonKey(name: 'period_start', fromJson: DateTime.parse) + required this.periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + required this.periodEnd, + required this.limit, + @JsonKey(name: 'gross_spent') required this.grossSpent, + required this.refunds, + @JsonKey(name: 'net_spent') required this.netSpent, + @JsonKey(name: 'rollover_in') required this.rolloverIn, + @JsonKey(name: 'effective_limit') required this.effectiveLimit, + required this.remaining, + @JsonKey(name: 'percent_used') required this.percentUsed, + @JsonKey(name: 'projected_spend') required this.projectedSpend, + required this.status, + @JsonKey(name: 'is_threshold_crossed') required this.isThresholdCrossed, + @JsonKey(name: 'is_forecast_breach') required this.isForecastBreach}); + + factory _$BudgetProgressDtoImpl.fromJson(Map json) => + _$$BudgetProgressDtoImplFromJson(json); + + @override + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + final DateTime periodStart; + @override + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + final DateTime periodEnd; + @override + final double limit; + @override + @JsonKey(name: 'gross_spent') + final double grossSpent; + @override + final double refunds; + @override + @JsonKey(name: 'net_spent') + final double netSpent; + @override + @JsonKey(name: 'rollover_in') + final double rolloverIn; + @override + @JsonKey(name: 'effective_limit') + final double effectiveLimit; + @override + final double remaining; + @override + @JsonKey(name: 'percent_used') + final double percentUsed; + @override + @JsonKey(name: 'projected_spend') + final double projectedSpend; + @override + final BudgetStatus status; + @override + @JsonKey(name: 'is_threshold_crossed') + final bool isThresholdCrossed; + @override + @JsonKey(name: 'is_forecast_breach') + final bool isForecastBreach; + + @override + String toString() { + return 'BudgetProgressDto(periodStart: $periodStart, periodEnd: $periodEnd, limit: $limit, grossSpent: $grossSpent, refunds: $refunds, netSpent: $netSpent, rolloverIn: $rolloverIn, effectiveLimit: $effectiveLimit, remaining: $remaining, percentUsed: $percentUsed, projectedSpend: $projectedSpend, status: $status, isThresholdCrossed: $isThresholdCrossed, isForecastBreach: $isForecastBreach)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetProgressDtoImpl && + (identical(other.periodStart, periodStart) || + other.periodStart == periodStart) && + (identical(other.periodEnd, periodEnd) || + other.periodEnd == periodEnd) && + (identical(other.limit, limit) || other.limit == limit) && + (identical(other.grossSpent, grossSpent) || + other.grossSpent == grossSpent) && + (identical(other.refunds, refunds) || other.refunds == refunds) && + (identical(other.netSpent, netSpent) || + other.netSpent == netSpent) && + (identical(other.rolloverIn, rolloverIn) || + other.rolloverIn == rolloverIn) && + (identical(other.effectiveLimit, effectiveLimit) || + other.effectiveLimit == effectiveLimit) && + (identical(other.remaining, remaining) || + other.remaining == remaining) && + (identical(other.percentUsed, percentUsed) || + other.percentUsed == percentUsed) && + (identical(other.projectedSpend, projectedSpend) || + other.projectedSpend == projectedSpend) && + (identical(other.status, status) || other.status == status) && + (identical(other.isThresholdCrossed, isThresholdCrossed) || + other.isThresholdCrossed == isThresholdCrossed) && + (identical(other.isForecastBreach, isForecastBreach) || + other.isForecastBreach == isForecastBreach)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + periodStart, + periodEnd, + limit, + grossSpent, + refunds, + netSpent, + rolloverIn, + effectiveLimit, + remaining, + percentUsed, + projectedSpend, + status, + isThresholdCrossed, + isForecastBreach); + + /// Create a copy of BudgetProgressDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetProgressDtoImplCopyWith<_$BudgetProgressDtoImpl> get copyWith => + __$$BudgetProgressDtoImplCopyWithImpl<_$BudgetProgressDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$BudgetProgressDtoImplToJson( + this, + ); + } +} + +abstract class _BudgetProgressDto implements BudgetProgressDto { + const factory _BudgetProgressDto( + {@JsonKey(name: 'period_start', fromJson: DateTime.parse) + required final DateTime periodStart, + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + required final DateTime periodEnd, + required final double limit, + @JsonKey(name: 'gross_spent') required final double grossSpent, + required final double refunds, + @JsonKey(name: 'net_spent') required final double netSpent, + @JsonKey(name: 'rollover_in') required final double rolloverIn, + @JsonKey(name: 'effective_limit') required final double effectiveLimit, + required final double remaining, + @JsonKey(name: 'percent_used') required final double percentUsed, + @JsonKey(name: 'projected_spend') required final double projectedSpend, + required final BudgetStatus status, + @JsonKey(name: 'is_threshold_crossed') + required final bool isThresholdCrossed, + @JsonKey(name: 'is_forecast_breach') + required final bool isForecastBreach}) = _$BudgetProgressDtoImpl; + + factory _BudgetProgressDto.fromJson(Map json) = + _$BudgetProgressDtoImpl.fromJson; + + @override + @JsonKey(name: 'period_start', fromJson: DateTime.parse) + DateTime get periodStart; + @override + @JsonKey(name: 'period_end', fromJson: DateTime.parse) + DateTime get periodEnd; + @override + double get limit; + @override + @JsonKey(name: 'gross_spent') + double get grossSpent; + @override + double get refunds; + @override + @JsonKey(name: 'net_spent') + double get netSpent; + @override + @JsonKey(name: 'rollover_in') + double get rolloverIn; + @override + @JsonKey(name: 'effective_limit') + double get effectiveLimit; + @override + double get remaining; + @override + @JsonKey(name: 'percent_used') + double get percentUsed; + @override + @JsonKey(name: 'projected_spend') + double get projectedSpend; + @override + BudgetStatus get status; + @override + @JsonKey(name: 'is_threshold_crossed') + bool get isThresholdCrossed; + @override + @JsonKey(name: 'is_forecast_breach') + bool get isForecastBreach; + + /// Create a copy of BudgetProgressDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetProgressDtoImplCopyWith<_$BudgetProgressDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/datasources/budget/dtos/budget_progress_dto.g.dart b/lib/data/datasources/budget/dtos/budget_progress_dto.g.dart new file mode 100644 index 00000000..4a95335a --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_progress_dto.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'budget_progress_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$BudgetProgressDtoImpl _$$BudgetProgressDtoImplFromJson( + Map json) => + _$BudgetProgressDtoImpl( + periodStart: DateTime.parse(json['period_start'] as String), + periodEnd: DateTime.parse(json['period_end'] as String), + limit: (json['limit'] as num).toDouble(), + grossSpent: (json['gross_spent'] as num).toDouble(), + refunds: (json['refunds'] as num).toDouble(), + netSpent: (json['net_spent'] as num).toDouble(), + rolloverIn: (json['rollover_in'] as num).toDouble(), + effectiveLimit: (json['effective_limit'] as num).toDouble(), + remaining: (json['remaining'] as num).toDouble(), + percentUsed: (json['percent_used'] as num).toDouble(), + projectedSpend: (json['projected_spend'] as num).toDouble(), + status: $enumDecode(_$BudgetStatusEnumMap, json['status']), + isThresholdCrossed: json['is_threshold_crossed'] as bool, + isForecastBreach: json['is_forecast_breach'] as bool, + ); + +Map _$$BudgetProgressDtoImplToJson( + _$BudgetProgressDtoImpl instance) => + { + 'period_start': instance.periodStart.toIso8601String(), + 'period_end': instance.periodEnd.toIso8601String(), + 'limit': instance.limit, + 'gross_spent': instance.grossSpent, + 'refunds': instance.refunds, + 'net_spent': instance.netSpent, + 'rollover_in': instance.rolloverIn, + 'effective_limit': instance.effectiveLimit, + 'remaining': instance.remaining, + 'percent_used': instance.percentUsed, + 'projected_spend': instance.projectedSpend, + 'status': _$BudgetStatusEnumMap[instance.status]!, + 'is_threshold_crossed': instance.isThresholdCrossed, + 'is_forecast_breach': instance.isForecastBreach, + }; + +const _$BudgetStatusEnumMap = { + BudgetStatus.onTrack: 'on_track', + BudgetStatus.nearLimit: 'near_limit', + BudgetStatus.overBudget: 'over_budget', + BudgetStatus.forecastBreach: 'forecast_breach', +}; diff --git a/lib/data/datasources/budget/dtos/budget_target_dto.dart b/lib/data/datasources/budget/dtos/budget_target_dto.dart new file mode 100644 index 00000000..1505dc5b --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_target_dto.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +part 'budget_target_dto.freezed.dart'; +part 'budget_target_dto.g.dart'; + +@freezed +class BudgetTargetDto with _$BudgetTargetDto { + const factory BudgetTargetDto({ + required BudgetTargetType type, + int? id, + @JsonKey(name: 'client_generated_id') String? clientId, + String? name, + }) = _BudgetTargetDto; + + factory BudgetTargetDto.fromJson(Map json) => + _$BudgetTargetDtoFromJson(json); +} diff --git a/lib/data/datasources/budget/dtos/budget_target_dto.freezed.dart b/lib/data/datasources/budget/dtos/budget_target_dto.freezed.dart new file mode 100644 index 00000000..59d9ac96 --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_target_dto.freezed.dart @@ -0,0 +1,233 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_target_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +BudgetTargetDto _$BudgetTargetDtoFromJson(Map json) { + return _BudgetTargetDto.fromJson(json); +} + +/// @nodoc +mixin _$BudgetTargetDto { + BudgetTargetType get type => throw _privateConstructorUsedError; + int? get id => throw _privateConstructorUsedError; + @JsonKey(name: 'client_generated_id') + String? get clientId => throw _privateConstructorUsedError; + String? get name => throw _privateConstructorUsedError; + + /// Serializes this BudgetTargetDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of BudgetTargetDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetTargetDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetTargetDtoCopyWith<$Res> { + factory $BudgetTargetDtoCopyWith( + BudgetTargetDto value, $Res Function(BudgetTargetDto) then) = + _$BudgetTargetDtoCopyWithImpl<$Res, BudgetTargetDto>; + @useResult + $Res call( + {BudgetTargetType type, + int? id, + @JsonKey(name: 'client_generated_id') String? clientId, + String? name}); +} + +/// @nodoc +class _$BudgetTargetDtoCopyWithImpl<$Res, $Val extends BudgetTargetDto> + implements $BudgetTargetDtoCopyWith<$Res> { + _$BudgetTargetDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetTargetDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? id = freezed, + Object? clientId = freezed, + Object? name = freezed, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as BudgetTargetType, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BudgetTargetDtoImplCopyWith<$Res> + implements $BudgetTargetDtoCopyWith<$Res> { + factory _$$BudgetTargetDtoImplCopyWith(_$BudgetTargetDtoImpl value, + $Res Function(_$BudgetTargetDtoImpl) then) = + __$$BudgetTargetDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {BudgetTargetType type, + int? id, + @JsonKey(name: 'client_generated_id') String? clientId, + String? name}); +} + +/// @nodoc +class __$$BudgetTargetDtoImplCopyWithImpl<$Res> + extends _$BudgetTargetDtoCopyWithImpl<$Res, _$BudgetTargetDtoImpl> + implements _$$BudgetTargetDtoImplCopyWith<$Res> { + __$$BudgetTargetDtoImplCopyWithImpl( + _$BudgetTargetDtoImpl _value, $Res Function(_$BudgetTargetDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetTargetDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? id = freezed, + Object? clientId = freezed, + Object? name = freezed, + }) { + return _then(_$BudgetTargetDtoImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as BudgetTargetType, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$BudgetTargetDtoImpl implements _BudgetTargetDto { + const _$BudgetTargetDtoImpl( + {required this.type, + this.id, + @JsonKey(name: 'client_generated_id') this.clientId, + this.name}); + + factory _$BudgetTargetDtoImpl.fromJson(Map json) => + _$$BudgetTargetDtoImplFromJson(json); + + @override + final BudgetTargetType type; + @override + final int? id; + @override + @JsonKey(name: 'client_generated_id') + final String? clientId; + @override + final String? name; + + @override + String toString() { + return 'BudgetTargetDto(type: $type, id: $id, clientId: $clientId, name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetTargetDtoImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.id, id) || other.id == id) && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, type, id, clientId, name); + + /// Create a copy of BudgetTargetDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetTargetDtoImplCopyWith<_$BudgetTargetDtoImpl> get copyWith => + __$$BudgetTargetDtoImplCopyWithImpl<_$BudgetTargetDtoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$BudgetTargetDtoImplToJson( + this, + ); + } +} + +abstract class _BudgetTargetDto implements BudgetTargetDto { + const factory _BudgetTargetDto( + {required final BudgetTargetType type, + final int? id, + @JsonKey(name: 'client_generated_id') final String? clientId, + final String? name}) = _$BudgetTargetDtoImpl; + + factory _BudgetTargetDto.fromJson(Map json) = + _$BudgetTargetDtoImpl.fromJson; + + @override + BudgetTargetType get type; + @override + int? get id; + @override + @JsonKey(name: 'client_generated_id') + String? get clientId; + @override + String? get name; + + /// Create a copy of BudgetTargetDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetTargetDtoImplCopyWith<_$BudgetTargetDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/datasources/budget/dtos/budget_target_dto.g.dart b/lib/data/datasources/budget/dtos/budget_target_dto.g.dart new file mode 100644 index 00000000..35708ebf --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_target_dto.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'budget_target_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$BudgetTargetDtoImpl _$$BudgetTargetDtoImplFromJson( + Map json) => + _$BudgetTargetDtoImpl( + type: $enumDecode(_$BudgetTargetTypeEnumMap, json['type']), + id: (json['id'] as num?)?.toInt(), + clientId: json['client_generated_id'] as String?, + name: json['name'] as String?, + ); + +Map _$$BudgetTargetDtoImplToJson( + _$BudgetTargetDtoImpl instance) => + { + 'type': _$BudgetTargetTypeEnumMap[instance.type]!, + 'id': instance.id, + 'client_generated_id': instance.clientId, + 'name': instance.name, + }; + +const _$BudgetTargetTypeEnumMap = { + BudgetTargetType.category: 'category', + BudgetTargetType.group: 'group', + BudgetTargetType.wallet: 'wallet', +}; diff --git a/lib/data/datasources/budget/dtos/budget_transactions_response.dart b/lib/data/datasources/budget/dtos/budget_transactions_response.dart new file mode 100644 index 00000000..bb8cded5 --- /dev/null +++ b/lib/data/datasources/budget/dtos/budget_transactions_response.dart @@ -0,0 +1,30 @@ +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/core/utils/json_defaults.dart'; + +class BudgetTransactionsResponse { + final DateTime periodStart; + final DateTime periodEnd; + final List data; + + const BudgetTransactionsResponse({ + required this.periodStart, + required this.periodEnd, + required this.data, + }); + + factory BudgetTransactionsResponse.fromJson(Map json) { + final rawData = json['data']; + final txs = (rawData is List) + ? rawData + .whereType>() + .map((j) => Transaction.fromJson(JsonDefaultsHelper.addDefaults(j))) + .toList() + : []; + + return BudgetTransactionsResponse( + periodStart: DateTime.parse(json['period_start'] as String), + periodEnd: DateTime.parse(json['period_end'] as String), + data: txs, + ); + } +} diff --git a/lib/data/mappers/budget_mapper.dart b/lib/data/mappers/budget_mapper.dart new file mode 100644 index 00000000..69f660e5 --- /dev/null +++ b/lib/data/mappers/budget_mapper.dart @@ -0,0 +1,91 @@ +import 'package:trakli/data/database/app_database.dart' as db; +import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_target_dto.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/entities/budget_period_state_entity.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/domain/entities/budget_target_entity.dart'; + +class BudgetMapper { + static BudgetEntity toDomain( + db.Budget budget, { + List targets = const [], + BudgetProgressEntity? progress, + }) { + return BudgetEntity( + clientId: budget.clientId, + id: budget.id, + userId: budget.userId, + name: budget.name, + slug: budget.slug, + description: budget.description, + amount: budget.amount, + currency: budget.currency, + periodType: budget.periodType, + startDate: budget.startDate, + endDate: budget.endDate, + rolloverEnabled: budget.rolloverEnabled, + thresholdPercent: budget.thresholdPercent, + forecastAlertsEnabled: budget.forecastAlertsEnabled, + isActive: budget.isActive, + ownerType: budget.ownerType, + ownerId: budget.ownerId, + targets: targets, + progress: progress, + createdAt: budget.createdAt, + updatedAt: budget.updatedAt, + lastSyncedAt: budget.lastSyncedAt, + ); + } + + static BudgetTargetEntity targetFromDb(db.Budgetable row, {String? name}) { + return BudgetTargetEntity( + type: row.targetType, + clientId: row.targetClientId, + name: name, + ); + } + + static BudgetTargetEntity targetFromDto(BudgetTargetDto dto) { + return BudgetTargetEntity( + type: dto.type, + id: dto.id, + clientId: dto.clientId, + name: dto.name, + ); + } + + static BudgetProgressEntity progressFromDto(BudgetProgressDto dto) { + return BudgetProgressEntity( + periodStart: dto.periodStart, + periodEnd: dto.periodEnd, + limit: dto.limit, + grossSpent: dto.grossSpent, + refunds: dto.refunds, + netSpent: dto.netSpent, + rolloverIn: dto.rolloverIn, + effectiveLimit: dto.effectiveLimit, + remaining: dto.remaining, + percentUsed: dto.percentUsed, + projectedSpend: dto.projectedSpend, + status: dto.status, + isThresholdCrossed: dto.isThresholdCrossed, + isForecastBreach: dto.isForecastBreach, + ); + } + + static BudgetPeriodStateEntity periodStateToDomain(db.BudgetPeriodState row) { + return BudgetPeriodStateEntity( + clientId: row.clientId, + id: row.id, + budgetClientId: row.budgetClientId, + periodStart: row.periodStart, + periodEnd: row.periodEnd, + netSpent: row.netSpent, + rolloverIn: row.rolloverIn, + rolloverOut: row.rolloverOut, + closedAt: row.closedAt, + lastSyncedAt: row.lastSyncedAt, + ); + } +} diff --git a/lib/data/repositories/budget_repository_impl.dart b/lib/data/repositories/budget_repository_impl.dart new file mode 100644 index 00000000..ea716439 --- /dev/null +++ b/lib/data/repositories/budget_repository_impl.dart @@ -0,0 +1,312 @@ +import 'dart:async'; +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:drift/drift.dart'; +import 'package:drift_sync_core/drift_sync_core.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/error/repository_error_handler.dart'; +import 'package:trakli/data/database/app_database.dart' as db; +import 'package:trakli/data/datasources/budget/budget_local_datasource.dart'; +import 'package:trakli/data/datasources/budget/budget_remote_datasource.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_target_dto.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; +import 'package:trakli/data/mappers/budget_mapper.dart'; +import 'package:trakli/data/sync/budget_sync_handler.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/entities/budget_period_state_entity.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/domain/entities/budget_target_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +@LazySingleton(as: BudgetRepository) +class BudgetRepositoryImpl + extends SyncEntityRepository + implements BudgetRepository { + final BudgetLocalDataSource localDataSource; + final BudgetRemoteDataSource remoteDataSource; + + BudgetRepositoryImpl({ + required BudgetSyncHandler syncHandler, + required this.localDataSource, + required this.remoteDataSource, + required super.db, + required super.requestAuthorizationService, + }) : super(syncHandler: syncHandler); + + @override + Future>> getAllBudgets({bool? active}) { + return RepositoryErrorHandler.handleApiCall(() async { + final rows = await localDataSource.getAllBudgets(active: active); + final out = []; + for (final row in rows) { + final targets = await _domainTargets(row.clientId); + out.add(BudgetMapper.toDomain(row, targets: targets)); + } + return out; + }); + } + + @override + Future> getBudget(String clientId) { + return RepositoryErrorHandler.handleApiCall(() async { + final row = await localDataSource.getBudgetByClientId(clientId); + if (row == null) return null; + final targets = await _domainTargets(clientId); + return BudgetMapper.toDomain(row, targets: targets); + }); + } + + @override + Future> insertBudget({ + required String name, + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + DateTime? endDate, + String? description, + bool rolloverEnabled = false, + int thresholdPercent = 80, + bool forecastAlertsEnabled = false, + bool isActive = true, + List targets = const [], + }) { + return RepositoryErrorHandler.handleApiCall(() async { + final budget = await localDataSource.insertBudget( + name: name, + slug: _slugify(name), + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + description: description, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + targets: targets + .map((t) => BudgetTargetInput(type: t.type, clientId: t.clientId)) + .toList(), + ); + + final dto = await _composeDto(budget.clientId); + if (dto != null) { + unawaited(post(dto)); + } + return unit; + }); + } + + @override + Future> updateBudget( + String clientId, { + String? name, + double? amount, + String? currency, + BudgetPeriodType? periodType, + DateTime? startDate, + DateTime? endDate, + String? description, + bool? rolloverEnabled, + int? thresholdPercent, + bool? forecastAlertsEnabled, + bool? isActive, + List? targets, + }) { + return RepositoryErrorHandler.handleApiCall(() async { + await localDataSource.updateBudget( + clientId, + name: name, + slug: name != null ? _slugify(name) : null, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + description: description, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + targets: targets + ?.map((t) => BudgetTargetInput(type: t.type, clientId: t.clientId)) + .toList(), + ); + + final dto = await _composeDto(clientId); + if (dto != null) { + unawaited(put(dto)); + } + return unit; + }); + } + + @override + Future> deleteBudget(String clientId) { + return RepositoryErrorHandler.handleApiCall(() async { + final dto = await _composeDto(clientId); + await localDataSource.deleteBudget(clientId); + if (dto != null) { + unawaited(delete(dto)); + } + return unit; + }); + } + + @override + Stream>> listenToBudgets({bool? active}) { + return localDataSource + .watchAllBudgets(active: active) + .asyncMap((rows) async { + try { + final out = []; + for (final row in rows) { + final targets = await _domainTargets(row.clientId); + out.add(BudgetMapper.toDomain(row, targets: targets)); + } + return Right>(out); + } catch (_) { + return const Left>(UnknownFailure()); + } + }); + } + + @override + Stream>> listenToTargetsForBudget( + String budgetClientId) { + return localDataSource + .watchTargetsForBudget(budgetClientId) + .asyncMap((_) async { + try { + return Right>( + await _domainTargets(budgetClientId), + ); + } catch (_) { + return const Left>(UnknownFailure()); + } + }); + } + + @override + Stream>> listenToPeriodStates( + String budgetClientId) { + return localDataSource + .watchPeriodStatesForBudget(budgetClientId) + .map((rows) { + return Right>( + rows.map(BudgetMapper.periodStateToDomain).toList(), + ); + }); + } + + @override + Future> fetchBudgetProgress(int id) { + return RepositoryErrorHandler.handleApiCall(() async { + final dto = await remoteDataSource.getBudgetProgress(id); + if (dto == null) return null; + return BudgetMapper.progressFromDto(dto); + }); + } + + @override + Future> + fetchBudgetTransactions(int id, {int limit = 50}) { + return RepositoryErrorHandler.handleApiCall(() async { + return remoteDataSource.getBudgetTransactions(id, limit: limit); + }); + } + + @override + Future> closeBudgetPeriod(int id) { + return RepositoryErrorHandler.handleApiCall(() async { + await remoteDataSource.closeBudgetPeriod(id); + return unit; + }); + } + + @override + Future> refreshPeriodStates() { + return RepositoryErrorHandler.handleApiCall(() async { + final dtos = await remoteDataSource.getAllPeriodStates(); + final localBudgets = await localDataSource.getAllBudgets(); + final byServerId = { + for (final b in localBudgets) + if (b.id != null) b.id!: b.clientId, + }; + final byClientId = {for (final b in localBudgets) b.clientId: b}; + + for (final dto in dtos) { + String? budgetClientId; + if (dto.budgetClientGeneratedId != null && + byClientId.containsKey(dto.budgetClientGeneratedId)) { + budgetClientId = dto.budgetClientGeneratedId; + } else if (dto.budgetId != null) { + budgetClientId = byServerId[dto.budgetId]; + } + if (budgetClientId == null) continue; + + await super.db.into(super.db.budgetPeriodStates).insert( + db.BudgetPeriodStatesCompanion( + id: Value(dto.id), + clientId: Value(dto.clientId), + budgetClientId: Value(budgetClientId), + periodStart: Value(dto.periodStart), + periodEnd: Value(dto.periodEnd), + netSpent: Value(dto.netSpent), + rolloverIn: Value(dto.rolloverIn), + rolloverOut: Value(dto.rolloverOut), + closedAt: Value(dto.closedAt), + lastSyncedAt: Value(dto.lastSyncedAt), + createdAt: dto.createdAt != null + ? Value(dto.createdAt!) + : const Value.absent(), + updatedAt: dto.updatedAt != null + ? Value(dto.updatedAt!) + : const Value.absent(), + ), + mode: InsertMode.insertOrReplace, + ); + } + return unit; + }); + } + + Future> _domainTargets(String budgetClientId) async { + final resolved = + await localDataSource.getResolvedTargetsForBudget(budgetClientId); + return resolved + .map((r) => BudgetTargetEntity( + type: r.type, + id: r.id, + clientId: r.clientId, + name: r.name, + )) + .toList(); + } + + Future _composeDto(String clientId) async { + final row = await localDataSource.getBudgetByClientId(clientId); + if (row == null) return null; + final resolved = + await localDataSource.getResolvedTargetsForBudget(clientId); + final targets = resolved + .map((r) => BudgetTargetDto( + type: r.type, + id: r.id, + clientId: r.clientId, + name: r.name, + )) + .toList(); + return BudgetCompleteDto(budget: row, targets: targets); + } + + String _slugify(String name) { + final lowered = name.trim().toLowerCase(); + return lowered + .replaceAll(RegExp(r'[^a-z0-9]+'), '-') + .replaceAll(RegExp(r'^-+|-+$'), ''); + } +} diff --git a/lib/data/sync/budget_sync_handler.dart b/lib/data/sync/budget_sync_handler.dart new file mode 100644 index 00000000..9f819076 --- /dev/null +++ b/lib/data/sync/budget_sync_handler.dart @@ -0,0 +1,223 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sync_core/drift_sync_core.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/utils/id_helper.dart'; +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/data/database/tables/budgets.dart'; +import 'package:trakli/data/database/tables/sync_table.dart'; +import 'package:trakli/data/datasources/budget/budget_remote_datasource.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart'; + +@lazySingleton +class BudgetSyncHandler + extends SyncTypeHandler + with RestSyncTypeHandler { + static const String entity = 'budget'; + + BudgetSyncHandler( + this.db, + this.remoteDataSource, + ); + + final AppDatabase db; + final BudgetRemoteDataSource remoteDataSource; + + TableInfo get table => db.budgets; + + @override + String get entityType => BudgetSyncHandler.entity; + + @override + String getClientId(BudgetCompleteDto entity) => entity.budget.clientId; + + @override + int? getServerId(BudgetCompleteDto entity) => entity.budget.id; + + @override + String getRev(BudgetCompleteDto entity) => entity.budget.rev ?? '1'; + + @override + DateTime? getLastSyncedAt(BudgetCompleteDto entity) => + entity.budget.lastSyncedAt; + + @override + Future unmarshal(Map entityJson) async { + return BudgetCompleteDto.fromServerJson(entityJson); + } + + @override + Map marshal(BudgetCompleteDto entity) { + return entity.toServerJson(); + } + + @override + Future shouldPersistRemote(BudgetCompleteDto entity) async => true; + + @override + Future> restGetAllRemote({ + bool? noClientId, + DateTime? syncedSince, + }) { + return remoteDataSource.getAllBudgets( + noClientId: noClientId, + syncedSince: syncedSince, + ); + } + + @override + Future restGetRemote(int id) { + return remoteDataSource.getBudget(id); + } + + @override + Future restPutRemote(BudgetCompleteDto entity) async { + if (entity.budget.id == null) { + return remoteDataSource.insertBudget(entity); + } else { + return remoteDataSource.updateBudget(entity); + } + } + + @override + Future restDeleteRemote(BudgetCompleteDto entity) async { + if (entity.budget.id != null) { + await remoteDataSource.deleteBudget(entity.budget.id!); + } + } + + @override + Future deleteAllLocal() async { + await db.budgetables.deleteAll(); + await db.budgetPeriodStates.deleteAll(); + await table.deleteAll(); + } + + @override + Future deleteLocalNotIn(Set clientIds) async { + if (clientIds.isEmpty) return; + await (db.delete(table)..where((t) => t.clientId.isNotIn(clientIds))).go(); + } + + @override + Future deleteLocal(BudgetCompleteDto entity) async { + final clientId = entity.budget.clientId; + await (db.delete(db.budgetables) + ..where((t) => t.budgetClientId.equals(clientId))) + .go(); + await (db.delete(db.budgetPeriodStates) + ..where((t) => t.budgetClientId.equals(clientId))) + .go(); + await table.deleteWhere((t) => t.clientId.equals(clientId)); + } + + @override + Future upsertLocal(BudgetCompleteDto entity) async { + await _upsertBudget(entity); + } + + @override + Future upsertAllLocal(List list) async { + for (final entity in list) { + if (entity.budget.clientId.isEmpty) { + continue; + } + if (entity.budget.deletedAt != null) { + await deleteLocal(entity); + continue; + } + await _upsertBudget(entity); + } + } + + @override + Future getLocalByClientId(String clientId) async { + final row = await (db.select(table) + ..where((t) => t.clientId.equals(clientId))) + .getSingleOrNull(); + if (row == null) { + throw Exception('Budget not found'); + } + return BudgetCompleteDto(budget: row); + } + + @override + Future getLocalByServerId(int serverId) async { + try { + final row = await (db.select(table)..where((t) => t.id.equals(serverId))) + .getSingle(); + return BudgetCompleteDto(budget: row); + } catch (_) { + return null; + } + } + + @override + Future assignClientId(BudgetCompleteDto item) async { + if (item.budget.clientId.isEmpty || + item.budget.clientId == defaultClientId) { + final newClientId = await generateDeviceScopedId(); + return BudgetCompleteDto( + budget: item.budget.copyWith(clientId: newClientId), + targets: item.targets, + progress: item.progress, + ); + } + return item; + } + + Future _upsertBudget(BudgetCompleteDto entity) async { + final budget = entity.budget; + final companion = BudgetsCompanion( + id: Value(budget.id), + userId: Value(budget.userId), + clientId: Value(budget.clientId), + name: Value(budget.name), + slug: Value(budget.slug), + description: Value(budget.description), + amount: Value(budget.amount), + currency: Value(budget.currency), + periodType: Value(budget.periodType), + startDate: Value(budget.startDate), + endDate: Value(budget.endDate), + rolloverEnabled: Value(budget.rolloverEnabled), + thresholdPercent: Value(budget.thresholdPercent), + forecastAlertsEnabled: Value(budget.forecastAlertsEnabled), + isActive: Value(budget.isActive), + ownerType: Value(budget.ownerType), + ownerId: Value(budget.ownerId), + createdAt: Value(budget.createdAt), + updatedAt: Value(budget.updatedAt), + lastSyncedAt: Value(budget.lastSyncedAt), + deletedAt: Value(budget.deletedAt), + rev: Value(budget.rev), + ); + await table.insertOnConflictUpdate(companion); + + if (entity.targets.isNotEmpty || _shouldResetTargets(entity)) { + await (db.delete(db.budgetables) + ..where((t) => t.budgetClientId.equals(budget.clientId))) + .go(); + for (final t in entity.targets) { + final targetClientId = t.clientId; + if (targetClientId == null || targetClientId.isEmpty) { + continue; + } + await db.into(db.budgetables).insert( + BudgetablesCompanion.insert( + budgetClientId: budget.clientId, + targetType: t.type, + targetClientId: targetClientId, + ), + mode: InsertMode.insertOrReplace, + ); + } + } + } + + bool _shouldResetTargets(BudgetCompleteDto entity) { + // Server payloads always include the canonical target list (possibly empty). + // Locally-composed DTOs always populate it from the DB. Either way, the + // server-state of targets is authoritative. + return true; + } +} diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index 164b64f5..d0328abf 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -43,6 +43,8 @@ import '../data/datasources/auth/preference_manager.dart' as _i683; import '../data/datasources/auth/token_manager.dart' as _i483; import '../data/datasources/benefits/cloud_benefit_remote_data_source.dart' as _i61; +import '../data/datasources/budget/budget_local_datasource.dart' as _i293; +import '../data/datasources/budget/budget_remote_datasource.dart' as _i760; import '../data/datasources/category/category_local_datasource.dart' as _i148; import '../data/datasources/category/category_remote_datasource.dart' as _i558; import '../data/datasources/configuration/config_local_datasource.dart' @@ -78,6 +80,7 @@ import '../data/datasources/wallet/wallet_local_datasource.dart' as _i849; import '../data/datasources/wallet/wallet_remote_datasource.dart' as _i624; import '../data/repositories/ai_repository_impl.dart' as _i841; import '../data/repositories/auth_repository_imp.dart' as _i135; +import '../data/repositories/budget_repository_impl.dart' as _i81; import '../data/repositories/category_repository_impl.dart' as _i324; import '../data/repositories/cloud_benefit_repository_imp.dart' as _i415; import '../data/repositories/config_repository_impl.dart' as _i379; @@ -91,6 +94,7 @@ import '../data/repositories/subscription_repository_imp.dart' as _i1047; import '../data/repositories/transaction_repository_impl.dart' as _i114; import '../data/repositories/transfer_repository_impl.dart' as _i268; import '../data/repositories/wallet_repository_impl.dart' as _i305; +import '../data/sync/budget_sync_handler.dart' as _i918; import '../data/sync/category_sync_handler.dart' as _i463; import '../data/sync/config_sync_handler.dart' as _i480; import '../data/sync/group_sync_handler.dart' as _i235; @@ -102,6 +106,7 @@ import '../data/sync/transfer_sync_handler.dart' as _i225; import '../data/sync/wallet_sync_handler.dart' as _i849; import '../domain/repositories/ai_repository.dart' as _i542; import '../domain/repositories/auth_repository.dart' as _i800; +import '../domain/repositories/budget_repository.dart' as _i340; import '../domain/repositories/category_repository.dart' as _i410; import '../domain/repositories/cloud_benefit_repository.dart' as _i11; import '../domain/repositories/config_repository.dart' as _i899; @@ -210,6 +215,7 @@ import '../presentation/auth/cubits/login/login_cubit.dart' as _i15; import '../presentation/auth/cubits/oauth/oauth_cubit.dart' as _i91; import '../presentation/auth/cubits/register/register_cubit.dart' as _i831; import '../presentation/benefits/cubit/benefits_cubit.dart' as _i88; +import '../presentation/budget/cubit/budget_cubit.dart' as _i1064; import '../presentation/category/cubit/category_cubit.dart' as _i455; import '../presentation/config/cubit/config_cubit.dart' as _i408; import '../presentation/config/theme_cubit/theme_cubit.dart' as _i627; @@ -297,6 +303,8 @@ _i174.GetIt $initGetIt( () => _i849.WalletLocalDataSourceImpl(database: gh<_i704.AppDatabase>())); gh.factory<_i514.ConfigLocalDataSource>( () => _i514.ConfigLocalDataSourceImpl(database: gh<_i704.AppDatabase>())); + gh.factory<_i293.BudgetLocalDataSource>( + () => _i293.BudgetLocalDataSourceImpl(gh<_i704.AppDatabase>())); gh.factory<_i148.CategoryLocalDataSource>( () => _i148.CategoryLocalDataSourceImpl(gh<_i704.AppDatabase>())); gh.lazySingleton<_i361.Dio>( @@ -372,6 +380,8 @@ _i174.GetIt $initGetIt( )); gh.factory<_i478.GroupRemoteDataSource>( () => _i478.GroupRemoteDataSourceImpl(dio: gh<_i361.Dio>())); + gh.factory<_i760.BudgetRemoteDataSource>( + () => _i760.BudgetRemoteDataSourceImpl(dio: gh<_i361.Dio>())); gh.factory<_i961.GetCategoriesUseCase>( () => _i961.GetCategoriesUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i292.DeleteCategoryUseCase>( @@ -476,6 +486,10 @@ _i174.GetIt $initGetIt( () => _i498.LoginByPhoneUsecase(gh<_i800.AuthRepository>())); gh.factory<_i828.IsOnboardingCompleted>( () => _i828.IsOnboardingCompleted(gh<_i800.AuthRepository>())); + gh.lazySingleton<_i918.BudgetSyncHandler>(() => _i918.BudgetSyncHandler( + gh<_i704.AppDatabase>(), + gh<_i760.BudgetRemoteDataSource>(), + )); gh.lazySingleton<_i118.TransactionRepository>(() => _i114.TransactionRepositoryImpl( syncHandler: gh<_i893.TransactionSyncHandler>(), @@ -491,6 +505,13 @@ _i174.GetIt $initGetIt( () => _i36.ConfirmSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i929.GetImportSessionsUseCase>( () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); + gh.lazySingleton<_i340.BudgetRepository>(() => _i81.BudgetRepositoryImpl( + syncHandler: gh<_i918.BudgetSyncHandler>(), + localDataSource: gh<_i293.BudgetLocalDataSource>(), + remoteDataSource: gh<_i760.BudgetRemoteDataSource>(), + db: gh<_i704.AppDatabase>(), + requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), + )); gh.factory<_i911.UpdatePartyUseCase>( () => _i911.UpdatePartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i84.AddPartyUseCase>( @@ -619,6 +640,8 @@ _i174.GetIt $initGetIt( gh<_i542.PasswordResetCodeUseCase>(), gh<_i494.PasswordResetUseCase>(), )); + gh.factory<_i1064.BudgetCubit>( + () => _i1064.BudgetCubit(gh<_i340.BudgetRepository>())); gh.factory<_i831.RegisterCubit>(() => _i831.RegisterCubit( gh<_i705.RegisterUseCase>(), gh<_i402.GetOtpCodeUseCase>(), @@ -674,6 +697,19 @@ _i174.GetIt $initGetIt( gh<_i1004.CreateTransferWithTransactionsUseCase>(), listenToTransfersUseCase: gh<_i744.ListenToTransfersUseCase>(), )); + gh.lazySingleton>>( + () => syncModule.provideSyncTypeHandlers( + gh<_i463.CategorySyncHandler>(), + gh<_i480.ConfigSyncHandler>(), + gh<_i849.WalletSyncHandler>(), + gh<_i280.PartySyncHandler>(), + gh<_i235.GroupSyncHandler>(), + gh<_i217.NotificationSyncHandler>(), + gh<_i893.TransactionSyncHandler>(), + gh<_i225.TransferSyncHandler>(), + gh<_i382.MediaSyncHandler>(), + gh<_i918.BudgetSyncHandler>(), + )); gh.factory<_i408.ConfigCubit>(() => _i408.ConfigCubit( gh<_i132.GetConfigsUseCase>(), gh<_i933.GetConfigUseCase>(), @@ -689,18 +725,6 @@ _i174.GetIt $initGetIt( )); gh.factory<_i977.PlansCubit>( () => _i977.PlansCubit(gh<_i314.FetchSubscriptionPlans>())); - gh.lazySingleton>>( - () => syncModule.provideSyncTypeHandlers( - gh<_i463.CategorySyncHandler>(), - gh<_i480.ConfigSyncHandler>(), - gh<_i849.WalletSyncHandler>(), - gh<_i280.PartySyncHandler>(), - gh<_i235.GroupSyncHandler>(), - gh<_i217.NotificationSyncHandler>(), - gh<_i893.TransactionSyncHandler>(), - gh<_i225.TransferSyncHandler>(), - gh<_i382.MediaSyncHandler>(), - )); gh.factory<_i798.UpdateDefaultCurrencyUseCase>(() => _i798.UpdateDefaultCurrencyUseCase(gh<_i1057.ExchangeRateRepository>())); gh.factory<_i676.GroupCubit>(() => _i676.GroupCubit( diff --git a/lib/domain/entities/budget_entity.dart b/lib/domain/entities/budget_entity.dart new file mode 100644 index 00000000..94ec17bc --- /dev/null +++ b/lib/domain/entities/budget_entity.dart @@ -0,0 +1,34 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/domain/entities/budget_target_entity.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +part 'budget_entity.freezed.dart'; + +@freezed +class BudgetEntity with _$BudgetEntity { + const factory BudgetEntity({ + required String clientId, + int? id, + int? userId, + required String name, + String? slug, + String? description, + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + DateTime? endDate, + @Default(false) bool rolloverEnabled, + @Default(80) int thresholdPercent, + @Default(false) bool forecastAlertsEnabled, + @Default(true) bool isActive, + @Default('user') String ownerType, + int? ownerId, + @Default([]) List targets, + BudgetProgressEntity? progress, + required DateTime createdAt, + DateTime? updatedAt, + DateTime? lastSyncedAt, + }) = _BudgetEntity; +} diff --git a/lib/domain/entities/budget_entity.freezed.dart b/lib/domain/entities/budget_entity.freezed.dart new file mode 100644 index 00000000..df442187 --- /dev/null +++ b/lib/domain/entities/budget_entity.freezed.dart @@ -0,0 +1,637 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$BudgetEntity { + String get clientId => throw _privateConstructorUsedError; + int? get id => throw _privateConstructorUsedError; + int? get userId => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String? get slug => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + double get amount => throw _privateConstructorUsedError; + String get currency => throw _privateConstructorUsedError; + BudgetPeriodType get periodType => throw _privateConstructorUsedError; + DateTime get startDate => throw _privateConstructorUsedError; + DateTime? get endDate => throw _privateConstructorUsedError; + bool get rolloverEnabled => throw _privateConstructorUsedError; + int get thresholdPercent => throw _privateConstructorUsedError; + bool get forecastAlertsEnabled => throw _privateConstructorUsedError; + bool get isActive => throw _privateConstructorUsedError; + String get ownerType => throw _privateConstructorUsedError; + int? get ownerId => throw _privateConstructorUsedError; + List get targets => throw _privateConstructorUsedError; + BudgetProgressEntity? get progress => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + DateTime? get lastSyncedAt => throw _privateConstructorUsedError; + + /// Create a copy of BudgetEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetEntityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetEntityCopyWith<$Res> { + factory $BudgetEntityCopyWith( + BudgetEntity value, $Res Function(BudgetEntity) then) = + _$BudgetEntityCopyWithImpl<$Res, BudgetEntity>; + @useResult + $Res call( + {String clientId, + int? id, + int? userId, + String name, + String? slug, + String? description, + double amount, + String currency, + BudgetPeriodType periodType, + DateTime startDate, + DateTime? endDate, + bool rolloverEnabled, + int thresholdPercent, + bool forecastAlertsEnabled, + bool isActive, + String ownerType, + int? ownerId, + List targets, + BudgetProgressEntity? progress, + DateTime createdAt, + DateTime? updatedAt, + DateTime? lastSyncedAt}); + + $BudgetProgressEntityCopyWith<$Res>? get progress; +} + +/// @nodoc +class _$BudgetEntityCopyWithImpl<$Res, $Val extends BudgetEntity> + implements $BudgetEntityCopyWith<$Res> { + _$BudgetEntityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? clientId = null, + Object? id = freezed, + Object? userId = freezed, + Object? name = null, + Object? slug = freezed, + Object? description = freezed, + Object? amount = null, + Object? currency = null, + Object? periodType = null, + Object? startDate = null, + Object? endDate = freezed, + Object? rolloverEnabled = null, + Object? thresholdPercent = null, + Object? forecastAlertsEnabled = null, + Object? isActive = null, + Object? ownerType = null, + Object? ownerId = freezed, + Object? targets = null, + Object? progress = freezed, + Object? createdAt = null, + Object? updatedAt = freezed, + Object? lastSyncedAt = freezed, + }) { + return _then(_value.copyWith( + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + userId: freezed == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as int?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: freezed == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String?, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as double, + currency: null == currency + ? _value.currency + : currency // ignore: cast_nullable_to_non_nullable + as String, + periodType: null == periodType + ? _value.periodType + : periodType // ignore: cast_nullable_to_non_nullable + as BudgetPeriodType, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: freezed == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + rolloverEnabled: null == rolloverEnabled + ? _value.rolloverEnabled + : rolloverEnabled // ignore: cast_nullable_to_non_nullable + as bool, + thresholdPercent: null == thresholdPercent + ? _value.thresholdPercent + : thresholdPercent // ignore: cast_nullable_to_non_nullable + as int, + forecastAlertsEnabled: null == forecastAlertsEnabled + ? _value.forecastAlertsEnabled + : forecastAlertsEnabled // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ownerType: null == ownerType + ? _value.ownerType + : ownerType // ignore: cast_nullable_to_non_nullable + as String, + ownerId: freezed == ownerId + ? _value.ownerId + : ownerId // ignore: cast_nullable_to_non_nullable + as int?, + targets: null == targets + ? _value.targets + : targets // ignore: cast_nullable_to_non_nullable + as List, + progress: freezed == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as BudgetProgressEntity?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastSyncedAt: freezed == lastSyncedAt + ? _value.lastSyncedAt + : lastSyncedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) as $Val); + } + + /// Create a copy of BudgetEntity + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $BudgetProgressEntityCopyWith<$Res>? get progress { + if (_value.progress == null) { + return null; + } + + return $BudgetProgressEntityCopyWith<$Res>(_value.progress!, (value) { + return _then(_value.copyWith(progress: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$BudgetEntityImplCopyWith<$Res> + implements $BudgetEntityCopyWith<$Res> { + factory _$$BudgetEntityImplCopyWith( + _$BudgetEntityImpl value, $Res Function(_$BudgetEntityImpl) then) = + __$$BudgetEntityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String clientId, + int? id, + int? userId, + String name, + String? slug, + String? description, + double amount, + String currency, + BudgetPeriodType periodType, + DateTime startDate, + DateTime? endDate, + bool rolloverEnabled, + int thresholdPercent, + bool forecastAlertsEnabled, + bool isActive, + String ownerType, + int? ownerId, + List targets, + BudgetProgressEntity? progress, + DateTime createdAt, + DateTime? updatedAt, + DateTime? lastSyncedAt}); + + @override + $BudgetProgressEntityCopyWith<$Res>? get progress; +} + +/// @nodoc +class __$$BudgetEntityImplCopyWithImpl<$Res> + extends _$BudgetEntityCopyWithImpl<$Res, _$BudgetEntityImpl> + implements _$$BudgetEntityImplCopyWith<$Res> { + __$$BudgetEntityImplCopyWithImpl( + _$BudgetEntityImpl _value, $Res Function(_$BudgetEntityImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? clientId = null, + Object? id = freezed, + Object? userId = freezed, + Object? name = null, + Object? slug = freezed, + Object? description = freezed, + Object? amount = null, + Object? currency = null, + Object? periodType = null, + Object? startDate = null, + Object? endDate = freezed, + Object? rolloverEnabled = null, + Object? thresholdPercent = null, + Object? forecastAlertsEnabled = null, + Object? isActive = null, + Object? ownerType = null, + Object? ownerId = freezed, + Object? targets = null, + Object? progress = freezed, + Object? createdAt = null, + Object? updatedAt = freezed, + Object? lastSyncedAt = freezed, + }) { + return _then(_$BudgetEntityImpl( + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + userId: freezed == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as int?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: freezed == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String?, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as double, + currency: null == currency + ? _value.currency + : currency // ignore: cast_nullable_to_non_nullable + as String, + periodType: null == periodType + ? _value.periodType + : periodType // ignore: cast_nullable_to_non_nullable + as BudgetPeriodType, + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: freezed == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + rolloverEnabled: null == rolloverEnabled + ? _value.rolloverEnabled + : rolloverEnabled // ignore: cast_nullable_to_non_nullable + as bool, + thresholdPercent: null == thresholdPercent + ? _value.thresholdPercent + : thresholdPercent // ignore: cast_nullable_to_non_nullable + as int, + forecastAlertsEnabled: null == forecastAlertsEnabled + ? _value.forecastAlertsEnabled + : forecastAlertsEnabled // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ownerType: null == ownerType + ? _value.ownerType + : ownerType // ignore: cast_nullable_to_non_nullable + as String, + ownerId: freezed == ownerId + ? _value.ownerId + : ownerId // ignore: cast_nullable_to_non_nullable + as int?, + targets: null == targets + ? _value._targets + : targets // ignore: cast_nullable_to_non_nullable + as List, + progress: freezed == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as BudgetProgressEntity?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastSyncedAt: freezed == lastSyncedAt + ? _value.lastSyncedAt + : lastSyncedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + )); + } +} + +/// @nodoc + +class _$BudgetEntityImpl implements _BudgetEntity { + const _$BudgetEntityImpl( + {required this.clientId, + this.id, + this.userId, + required this.name, + this.slug, + this.description, + required this.amount, + required this.currency, + required this.periodType, + required this.startDate, + this.endDate, + this.rolloverEnabled = false, + this.thresholdPercent = 80, + this.forecastAlertsEnabled = false, + this.isActive = true, + this.ownerType = 'user', + this.ownerId, + final List targets = const [], + this.progress, + required this.createdAt, + this.updatedAt, + this.lastSyncedAt}) + : _targets = targets; + + @override + final String clientId; + @override + final int? id; + @override + final int? userId; + @override + final String name; + @override + final String? slug; + @override + final String? description; + @override + final double amount; + @override + final String currency; + @override + final BudgetPeriodType periodType; + @override + final DateTime startDate; + @override + final DateTime? endDate; + @override + @JsonKey() + final bool rolloverEnabled; + @override + @JsonKey() + final int thresholdPercent; + @override + @JsonKey() + final bool forecastAlertsEnabled; + @override + @JsonKey() + final bool isActive; + @override + @JsonKey() + final String ownerType; + @override + final int? ownerId; + final List _targets; + @override + @JsonKey() + List get targets { + if (_targets is EqualUnmodifiableListView) return _targets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_targets); + } + + @override + final BudgetProgressEntity? progress; + @override + final DateTime createdAt; + @override + final DateTime? updatedAt; + @override + final DateTime? lastSyncedAt; + + @override + String toString() { + return 'BudgetEntity(clientId: $clientId, id: $id, userId: $userId, name: $name, slug: $slug, description: $description, amount: $amount, currency: $currency, periodType: $periodType, startDate: $startDate, endDate: $endDate, rolloverEnabled: $rolloverEnabled, thresholdPercent: $thresholdPercent, forecastAlertsEnabled: $forecastAlertsEnabled, isActive: $isActive, ownerType: $ownerType, ownerId: $ownerId, targets: $targets, progress: $progress, createdAt: $createdAt, updatedAt: $updatedAt, lastSyncedAt: $lastSyncedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetEntityImpl && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.id, id) || other.id == id) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.name, name) || other.name == name) && + (identical(other.slug, slug) || other.slug == slug) && + (identical(other.description, description) || + other.description == description) && + (identical(other.amount, amount) || other.amount == amount) && + (identical(other.currency, currency) || + other.currency == currency) && + (identical(other.periodType, periodType) || + other.periodType == periodType) && + (identical(other.startDate, startDate) || + other.startDate == startDate) && + (identical(other.endDate, endDate) || other.endDate == endDate) && + (identical(other.rolloverEnabled, rolloverEnabled) || + other.rolloverEnabled == rolloverEnabled) && + (identical(other.thresholdPercent, thresholdPercent) || + other.thresholdPercent == thresholdPercent) && + (identical(other.forecastAlertsEnabled, forecastAlertsEnabled) || + other.forecastAlertsEnabled == forecastAlertsEnabled) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + (identical(other.ownerType, ownerType) || + other.ownerType == ownerType) && + (identical(other.ownerId, ownerId) || other.ownerId == ownerId) && + const DeepCollectionEquality().equals(other._targets, _targets) && + (identical(other.progress, progress) || + other.progress == progress) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.lastSyncedAt, lastSyncedAt) || + other.lastSyncedAt == lastSyncedAt)); + } + + @override + int get hashCode => Object.hashAll([ + runtimeType, + clientId, + id, + userId, + name, + slug, + description, + amount, + currency, + periodType, + startDate, + endDate, + rolloverEnabled, + thresholdPercent, + forecastAlertsEnabled, + isActive, + ownerType, + ownerId, + const DeepCollectionEquality().hash(_targets), + progress, + createdAt, + updatedAt, + lastSyncedAt + ]); + + /// Create a copy of BudgetEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetEntityImplCopyWith<_$BudgetEntityImpl> get copyWith => + __$$BudgetEntityImplCopyWithImpl<_$BudgetEntityImpl>(this, _$identity); +} + +abstract class _BudgetEntity implements BudgetEntity { + const factory _BudgetEntity( + {required final String clientId, + final int? id, + final int? userId, + required final String name, + final String? slug, + final String? description, + required final double amount, + required final String currency, + required final BudgetPeriodType periodType, + required 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 List targets, + final BudgetProgressEntity? progress, + required final DateTime createdAt, + final DateTime? updatedAt, + final DateTime? lastSyncedAt}) = _$BudgetEntityImpl; + + @override + String get clientId; + @override + int? get id; + @override + int? get userId; + @override + String get name; + @override + String? get slug; + @override + String? get description; + @override + double get amount; + @override + String get currency; + @override + BudgetPeriodType get periodType; + @override + DateTime get startDate; + @override + DateTime? get endDate; + @override + bool get rolloverEnabled; + @override + int get thresholdPercent; + @override + bool get forecastAlertsEnabled; + @override + bool get isActive; + @override + String get ownerType; + @override + int? get ownerId; + @override + List get targets; + @override + BudgetProgressEntity? get progress; + @override + DateTime get createdAt; + @override + DateTime? get updatedAt; + @override + DateTime? get lastSyncedAt; + + /// Create a copy of BudgetEntity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetEntityImplCopyWith<_$BudgetEntityImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/domain/entities/budget_period_state_entity.dart b/lib/domain/entities/budget_period_state_entity.dart new file mode 100644 index 00000000..d5a3d589 --- /dev/null +++ b/lib/domain/entities/budget_period_state_entity.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'budget_period_state_entity.freezed.dart'; + +@freezed +class BudgetPeriodStateEntity with _$BudgetPeriodStateEntity { + const factory BudgetPeriodStateEntity({ + required String clientId, + int? id, + required String budgetClientId, + required DateTime periodStart, + required DateTime periodEnd, + required double netSpent, + required double rolloverIn, + required double rolloverOut, + DateTime? closedAt, + DateTime? lastSyncedAt, + }) = _BudgetPeriodStateEntity; +} diff --git a/lib/domain/entities/budget_period_state_entity.freezed.dart b/lib/domain/entities/budget_period_state_entity.freezed.dart new file mode 100644 index 00000000..22ede9b6 --- /dev/null +++ b/lib/domain/entities/budget_period_state_entity.freezed.dart @@ -0,0 +1,352 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_period_state_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$BudgetPeriodStateEntity { + String get clientId => throw _privateConstructorUsedError; + int? get id => throw _privateConstructorUsedError; + String get budgetClientId => throw _privateConstructorUsedError; + DateTime get periodStart => throw _privateConstructorUsedError; + DateTime get periodEnd => throw _privateConstructorUsedError; + double get netSpent => throw _privateConstructorUsedError; + double get rolloverIn => throw _privateConstructorUsedError; + double get rolloverOut => throw _privateConstructorUsedError; + DateTime? get closedAt => throw _privateConstructorUsedError; + DateTime? get lastSyncedAt => throw _privateConstructorUsedError; + + /// Create a copy of BudgetPeriodStateEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetPeriodStateEntityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetPeriodStateEntityCopyWith<$Res> { + factory $BudgetPeriodStateEntityCopyWith(BudgetPeriodStateEntity value, + $Res Function(BudgetPeriodStateEntity) then) = + _$BudgetPeriodStateEntityCopyWithImpl<$Res, BudgetPeriodStateEntity>; + @useResult + $Res call( + {String clientId, + int? id, + String budgetClientId, + DateTime periodStart, + DateTime periodEnd, + double netSpent, + double rolloverIn, + double rolloverOut, + DateTime? closedAt, + DateTime? lastSyncedAt}); +} + +/// @nodoc +class _$BudgetPeriodStateEntityCopyWithImpl<$Res, + $Val extends BudgetPeriodStateEntity> + implements $BudgetPeriodStateEntityCopyWith<$Res> { + _$BudgetPeriodStateEntityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetPeriodStateEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? clientId = null, + Object? id = freezed, + Object? budgetClientId = null, + Object? periodStart = null, + Object? periodEnd = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? rolloverOut = null, + Object? closedAt = freezed, + Object? lastSyncedAt = freezed, + }) { + return _then(_value.copyWith( + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + budgetClientId: null == budgetClientId + ? _value.budgetClientId + : budgetClientId // ignore: cast_nullable_to_non_nullable + as String, + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + rolloverOut: null == rolloverOut + ? _value.rolloverOut + : rolloverOut // ignore: cast_nullable_to_non_nullable + as double, + closedAt: freezed == closedAt + ? _value.closedAt + : closedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastSyncedAt: freezed == lastSyncedAt + ? _value.lastSyncedAt + : lastSyncedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BudgetPeriodStateEntityImplCopyWith<$Res> + implements $BudgetPeriodStateEntityCopyWith<$Res> { + factory _$$BudgetPeriodStateEntityImplCopyWith( + _$BudgetPeriodStateEntityImpl value, + $Res Function(_$BudgetPeriodStateEntityImpl) then) = + __$$BudgetPeriodStateEntityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String clientId, + int? id, + String budgetClientId, + DateTime periodStart, + DateTime periodEnd, + double netSpent, + double rolloverIn, + double rolloverOut, + DateTime? closedAt, + DateTime? lastSyncedAt}); +} + +/// @nodoc +class __$$BudgetPeriodStateEntityImplCopyWithImpl<$Res> + extends _$BudgetPeriodStateEntityCopyWithImpl<$Res, + _$BudgetPeriodStateEntityImpl> + implements _$$BudgetPeriodStateEntityImplCopyWith<$Res> { + __$$BudgetPeriodStateEntityImplCopyWithImpl( + _$BudgetPeriodStateEntityImpl _value, + $Res Function(_$BudgetPeriodStateEntityImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetPeriodStateEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? clientId = null, + Object? id = freezed, + Object? budgetClientId = null, + Object? periodStart = null, + Object? periodEnd = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? rolloverOut = null, + Object? closedAt = freezed, + Object? lastSyncedAt = freezed, + }) { + return _then(_$BudgetPeriodStateEntityImpl( + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + budgetClientId: null == budgetClientId + ? _value.budgetClientId + : budgetClientId // ignore: cast_nullable_to_non_nullable + as String, + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + rolloverOut: null == rolloverOut + ? _value.rolloverOut + : rolloverOut // ignore: cast_nullable_to_non_nullable + as double, + closedAt: freezed == closedAt + ? _value.closedAt + : closedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastSyncedAt: freezed == lastSyncedAt + ? _value.lastSyncedAt + : lastSyncedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + )); + } +} + +/// @nodoc + +class _$BudgetPeriodStateEntityImpl implements _BudgetPeriodStateEntity { + const _$BudgetPeriodStateEntityImpl( + {required this.clientId, + this.id, + required this.budgetClientId, + required this.periodStart, + required this.periodEnd, + required this.netSpent, + required this.rolloverIn, + required this.rolloverOut, + this.closedAt, + this.lastSyncedAt}); + + @override + final String clientId; + @override + final int? id; + @override + final String budgetClientId; + @override + final DateTime periodStart; + @override + final DateTime periodEnd; + @override + final double netSpent; + @override + final double rolloverIn; + @override + final double rolloverOut; + @override + final DateTime? closedAt; + @override + final DateTime? lastSyncedAt; + + @override + String toString() { + return 'BudgetPeriodStateEntity(clientId: $clientId, id: $id, budgetClientId: $budgetClientId, periodStart: $periodStart, periodEnd: $periodEnd, netSpent: $netSpent, rolloverIn: $rolloverIn, rolloverOut: $rolloverOut, closedAt: $closedAt, lastSyncedAt: $lastSyncedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetPeriodStateEntityImpl && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.id, id) || other.id == id) && + (identical(other.budgetClientId, budgetClientId) || + other.budgetClientId == budgetClientId) && + (identical(other.periodStart, periodStart) || + other.periodStart == periodStart) && + (identical(other.periodEnd, periodEnd) || + other.periodEnd == periodEnd) && + (identical(other.netSpent, netSpent) || + other.netSpent == netSpent) && + (identical(other.rolloverIn, rolloverIn) || + other.rolloverIn == rolloverIn) && + (identical(other.rolloverOut, rolloverOut) || + other.rolloverOut == rolloverOut) && + (identical(other.closedAt, closedAt) || + other.closedAt == closedAt) && + (identical(other.lastSyncedAt, lastSyncedAt) || + other.lastSyncedAt == lastSyncedAt)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + clientId, + id, + budgetClientId, + periodStart, + periodEnd, + netSpent, + rolloverIn, + rolloverOut, + closedAt, + lastSyncedAt); + + /// Create a copy of BudgetPeriodStateEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetPeriodStateEntityImplCopyWith<_$BudgetPeriodStateEntityImpl> + get copyWith => __$$BudgetPeriodStateEntityImplCopyWithImpl< + _$BudgetPeriodStateEntityImpl>(this, _$identity); +} + +abstract class _BudgetPeriodStateEntity implements BudgetPeriodStateEntity { + const factory _BudgetPeriodStateEntity( + {required final String clientId, + final int? id, + required final String budgetClientId, + required final DateTime periodStart, + required final DateTime periodEnd, + required final double netSpent, + required final double rolloverIn, + required final double rolloverOut, + final DateTime? closedAt, + final DateTime? lastSyncedAt}) = _$BudgetPeriodStateEntityImpl; + + @override + String get clientId; + @override + int? get id; + @override + String get budgetClientId; + @override + DateTime get periodStart; + @override + DateTime get periodEnd; + @override + double get netSpent; + @override + double get rolloverIn; + @override + double get rolloverOut; + @override + DateTime? get closedAt; + @override + DateTime? get lastSyncedAt; + + /// Create a copy of BudgetPeriodStateEntity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetPeriodStateEntityImplCopyWith<_$BudgetPeriodStateEntityImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/domain/entities/budget_progress_entity.dart b/lib/domain/entities/budget_progress_entity.dart new file mode 100644 index 00000000..5d0916b5 --- /dev/null +++ b/lib/domain/entities/budget_progress_entity.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +part 'budget_progress_entity.freezed.dart'; + +@freezed +class BudgetProgressEntity with _$BudgetProgressEntity { + const factory BudgetProgressEntity({ + required DateTime periodStart, + required DateTime periodEnd, + required double limit, + required double grossSpent, + required double refunds, + required double netSpent, + required double rolloverIn, + required double effectiveLimit, + required double remaining, + required double percentUsed, + required double projectedSpend, + required BudgetStatus status, + required bool isThresholdCrossed, + required bool isForecastBreach, + }) = _BudgetProgressEntity; +} diff --git a/lib/domain/entities/budget_progress_entity.freezed.dart b/lib/domain/entities/budget_progress_entity.freezed.dart new file mode 100644 index 00000000..5dcc7c82 --- /dev/null +++ b/lib/domain/entities/budget_progress_entity.freezed.dart @@ -0,0 +1,436 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_progress_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$BudgetProgressEntity { + DateTime get periodStart => throw _privateConstructorUsedError; + DateTime get periodEnd => throw _privateConstructorUsedError; + double get limit => throw _privateConstructorUsedError; + double get grossSpent => throw _privateConstructorUsedError; + double get refunds => throw _privateConstructorUsedError; + double get netSpent => throw _privateConstructorUsedError; + double get rolloverIn => throw _privateConstructorUsedError; + double get effectiveLimit => throw _privateConstructorUsedError; + double get remaining => throw _privateConstructorUsedError; + double get percentUsed => throw _privateConstructorUsedError; + double get projectedSpend => throw _privateConstructorUsedError; + BudgetStatus get status => throw _privateConstructorUsedError; + bool get isThresholdCrossed => throw _privateConstructorUsedError; + bool get isForecastBreach => throw _privateConstructorUsedError; + + /// Create a copy of BudgetProgressEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetProgressEntityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetProgressEntityCopyWith<$Res> { + factory $BudgetProgressEntityCopyWith(BudgetProgressEntity value, + $Res Function(BudgetProgressEntity) then) = + _$BudgetProgressEntityCopyWithImpl<$Res, BudgetProgressEntity>; + @useResult + $Res call( + {DateTime periodStart, + DateTime periodEnd, + double limit, + double grossSpent, + double refunds, + double netSpent, + double rolloverIn, + double effectiveLimit, + double remaining, + double percentUsed, + double projectedSpend, + BudgetStatus status, + bool isThresholdCrossed, + bool isForecastBreach}); +} + +/// @nodoc +class _$BudgetProgressEntityCopyWithImpl<$Res, + $Val extends BudgetProgressEntity> + implements $BudgetProgressEntityCopyWith<$Res> { + _$BudgetProgressEntityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetProgressEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? periodStart = null, + Object? periodEnd = null, + Object? limit = null, + Object? grossSpent = null, + Object? refunds = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? effectiveLimit = null, + Object? remaining = null, + Object? percentUsed = null, + Object? projectedSpend = null, + Object? status = null, + Object? isThresholdCrossed = null, + Object? isForecastBreach = null, + }) { + return _then(_value.copyWith( + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as double, + grossSpent: null == grossSpent + ? _value.grossSpent + : grossSpent // ignore: cast_nullable_to_non_nullable + as double, + refunds: null == refunds + ? _value.refunds + : refunds // ignore: cast_nullable_to_non_nullable + as double, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + effectiveLimit: null == effectiveLimit + ? _value.effectiveLimit + : effectiveLimit // ignore: cast_nullable_to_non_nullable + as double, + remaining: null == remaining + ? _value.remaining + : remaining // ignore: cast_nullable_to_non_nullable + as double, + percentUsed: null == percentUsed + ? _value.percentUsed + : percentUsed // ignore: cast_nullable_to_non_nullable + as double, + projectedSpend: null == projectedSpend + ? _value.projectedSpend + : projectedSpend // ignore: cast_nullable_to_non_nullable + as double, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as BudgetStatus, + isThresholdCrossed: null == isThresholdCrossed + ? _value.isThresholdCrossed + : isThresholdCrossed // ignore: cast_nullable_to_non_nullable + as bool, + isForecastBreach: null == isForecastBreach + ? _value.isForecastBreach + : isForecastBreach // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BudgetProgressEntityImplCopyWith<$Res> + implements $BudgetProgressEntityCopyWith<$Res> { + factory _$$BudgetProgressEntityImplCopyWith(_$BudgetProgressEntityImpl value, + $Res Function(_$BudgetProgressEntityImpl) then) = + __$$BudgetProgressEntityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {DateTime periodStart, + DateTime periodEnd, + double limit, + double grossSpent, + double refunds, + double netSpent, + double rolloverIn, + double effectiveLimit, + double remaining, + double percentUsed, + double projectedSpend, + BudgetStatus status, + bool isThresholdCrossed, + bool isForecastBreach}); +} + +/// @nodoc +class __$$BudgetProgressEntityImplCopyWithImpl<$Res> + extends _$BudgetProgressEntityCopyWithImpl<$Res, _$BudgetProgressEntityImpl> + implements _$$BudgetProgressEntityImplCopyWith<$Res> { + __$$BudgetProgressEntityImplCopyWithImpl(_$BudgetProgressEntityImpl _value, + $Res Function(_$BudgetProgressEntityImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetProgressEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? periodStart = null, + Object? periodEnd = null, + Object? limit = null, + Object? grossSpent = null, + Object? refunds = null, + Object? netSpent = null, + Object? rolloverIn = null, + Object? effectiveLimit = null, + Object? remaining = null, + Object? percentUsed = null, + Object? projectedSpend = null, + Object? status = null, + Object? isThresholdCrossed = null, + Object? isForecastBreach = null, + }) { + return _then(_$BudgetProgressEntityImpl( + periodStart: null == periodStart + ? _value.periodStart + : periodStart // ignore: cast_nullable_to_non_nullable + as DateTime, + periodEnd: null == periodEnd + ? _value.periodEnd + : periodEnd // ignore: cast_nullable_to_non_nullable + as DateTime, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as double, + grossSpent: null == grossSpent + ? _value.grossSpent + : grossSpent // ignore: cast_nullable_to_non_nullable + as double, + refunds: null == refunds + ? _value.refunds + : refunds // ignore: cast_nullable_to_non_nullable + as double, + netSpent: null == netSpent + ? _value.netSpent + : netSpent // ignore: cast_nullable_to_non_nullable + as double, + rolloverIn: null == rolloverIn + ? _value.rolloverIn + : rolloverIn // ignore: cast_nullable_to_non_nullable + as double, + effectiveLimit: null == effectiveLimit + ? _value.effectiveLimit + : effectiveLimit // ignore: cast_nullable_to_non_nullable + as double, + remaining: null == remaining + ? _value.remaining + : remaining // ignore: cast_nullable_to_non_nullable + as double, + percentUsed: null == percentUsed + ? _value.percentUsed + : percentUsed // ignore: cast_nullable_to_non_nullable + as double, + projectedSpend: null == projectedSpend + ? _value.projectedSpend + : projectedSpend // ignore: cast_nullable_to_non_nullable + as double, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as BudgetStatus, + isThresholdCrossed: null == isThresholdCrossed + ? _value.isThresholdCrossed + : isThresholdCrossed // ignore: cast_nullable_to_non_nullable + as bool, + isForecastBreach: null == isForecastBreach + ? _value.isForecastBreach + : isForecastBreach // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$BudgetProgressEntityImpl implements _BudgetProgressEntity { + const _$BudgetProgressEntityImpl( + {required this.periodStart, + required this.periodEnd, + required this.limit, + required this.grossSpent, + required this.refunds, + required this.netSpent, + required this.rolloverIn, + required this.effectiveLimit, + required this.remaining, + required this.percentUsed, + required this.projectedSpend, + required this.status, + required this.isThresholdCrossed, + required this.isForecastBreach}); + + @override + final DateTime periodStart; + @override + final DateTime periodEnd; + @override + final double limit; + @override + final double grossSpent; + @override + final double refunds; + @override + final double netSpent; + @override + final double rolloverIn; + @override + final double effectiveLimit; + @override + final double remaining; + @override + final double percentUsed; + @override + final double projectedSpend; + @override + final BudgetStatus status; + @override + final bool isThresholdCrossed; + @override + final bool isForecastBreach; + + @override + String toString() { + return 'BudgetProgressEntity(periodStart: $periodStart, periodEnd: $periodEnd, limit: $limit, grossSpent: $grossSpent, refunds: $refunds, netSpent: $netSpent, rolloverIn: $rolloverIn, effectiveLimit: $effectiveLimit, remaining: $remaining, percentUsed: $percentUsed, projectedSpend: $projectedSpend, status: $status, isThresholdCrossed: $isThresholdCrossed, isForecastBreach: $isForecastBreach)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetProgressEntityImpl && + (identical(other.periodStart, periodStart) || + other.periodStart == periodStart) && + (identical(other.periodEnd, periodEnd) || + other.periodEnd == periodEnd) && + (identical(other.limit, limit) || other.limit == limit) && + (identical(other.grossSpent, grossSpent) || + other.grossSpent == grossSpent) && + (identical(other.refunds, refunds) || other.refunds == refunds) && + (identical(other.netSpent, netSpent) || + other.netSpent == netSpent) && + (identical(other.rolloverIn, rolloverIn) || + other.rolloverIn == rolloverIn) && + (identical(other.effectiveLimit, effectiveLimit) || + other.effectiveLimit == effectiveLimit) && + (identical(other.remaining, remaining) || + other.remaining == remaining) && + (identical(other.percentUsed, percentUsed) || + other.percentUsed == percentUsed) && + (identical(other.projectedSpend, projectedSpend) || + other.projectedSpend == projectedSpend) && + (identical(other.status, status) || other.status == status) && + (identical(other.isThresholdCrossed, isThresholdCrossed) || + other.isThresholdCrossed == isThresholdCrossed) && + (identical(other.isForecastBreach, isForecastBreach) || + other.isForecastBreach == isForecastBreach)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + periodStart, + periodEnd, + limit, + grossSpent, + refunds, + netSpent, + rolloverIn, + effectiveLimit, + remaining, + percentUsed, + projectedSpend, + status, + isThresholdCrossed, + isForecastBreach); + + /// Create a copy of BudgetProgressEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetProgressEntityImplCopyWith<_$BudgetProgressEntityImpl> + get copyWith => + __$$BudgetProgressEntityImplCopyWithImpl<_$BudgetProgressEntityImpl>( + this, _$identity); +} + +abstract class _BudgetProgressEntity implements BudgetProgressEntity { + const factory _BudgetProgressEntity( + {required final DateTime periodStart, + required final DateTime periodEnd, + required final double limit, + required final double grossSpent, + required final double refunds, + required final double netSpent, + required final double rolloverIn, + required final double effectiveLimit, + required final double remaining, + required final double percentUsed, + required final double projectedSpend, + required final BudgetStatus status, + required final bool isThresholdCrossed, + required final bool isForecastBreach}) = _$BudgetProgressEntityImpl; + + @override + DateTime get periodStart; + @override + DateTime get periodEnd; + @override + double get limit; + @override + double get grossSpent; + @override + double get refunds; + @override + double get netSpent; + @override + double get rolloverIn; + @override + double get effectiveLimit; + @override + double get remaining; + @override + double get percentUsed; + @override + double get projectedSpend; + @override + BudgetStatus get status; + @override + bool get isThresholdCrossed; + @override + bool get isForecastBreach; + + /// Create a copy of BudgetProgressEntity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetProgressEntityImplCopyWith<_$BudgetProgressEntityImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/domain/entities/budget_target_entity.dart b/lib/domain/entities/budget_target_entity.dart new file mode 100644 index 00000000..7b2a3d21 --- /dev/null +++ b/lib/domain/entities/budget_target_entity.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +part 'budget_target_entity.freezed.dart'; + +@freezed +class BudgetTargetEntity with _$BudgetTargetEntity { + const factory BudgetTargetEntity({ + required BudgetTargetType type, + int? id, + String? clientId, + String? name, + }) = _BudgetTargetEntity; +} diff --git a/lib/domain/entities/budget_target_entity.freezed.dart b/lib/domain/entities/budget_target_entity.freezed.dart new file mode 100644 index 00000000..3589303f --- /dev/null +++ b/lib/domain/entities/budget_target_entity.freezed.dart @@ -0,0 +1,198 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_target_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$BudgetTargetEntity { + BudgetTargetType get type => throw _privateConstructorUsedError; + int? get id => throw _privateConstructorUsedError; + String? get clientId => throw _privateConstructorUsedError; + String? get name => throw _privateConstructorUsedError; + + /// Create a copy of BudgetTargetEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetTargetEntityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetTargetEntityCopyWith<$Res> { + factory $BudgetTargetEntityCopyWith( + BudgetTargetEntity value, $Res Function(BudgetTargetEntity) then) = + _$BudgetTargetEntityCopyWithImpl<$Res, BudgetTargetEntity>; + @useResult + $Res call({BudgetTargetType type, int? id, String? clientId, String? name}); +} + +/// @nodoc +class _$BudgetTargetEntityCopyWithImpl<$Res, $Val extends BudgetTargetEntity> + implements $BudgetTargetEntityCopyWith<$Res> { + _$BudgetTargetEntityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetTargetEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? id = freezed, + Object? clientId = freezed, + Object? name = freezed, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as BudgetTargetType, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BudgetTargetEntityImplCopyWith<$Res> + implements $BudgetTargetEntityCopyWith<$Res> { + factory _$$BudgetTargetEntityImplCopyWith(_$BudgetTargetEntityImpl value, + $Res Function(_$BudgetTargetEntityImpl) then) = + __$$BudgetTargetEntityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({BudgetTargetType type, int? id, String? clientId, String? name}); +} + +/// @nodoc +class __$$BudgetTargetEntityImplCopyWithImpl<$Res> + extends _$BudgetTargetEntityCopyWithImpl<$Res, _$BudgetTargetEntityImpl> + implements _$$BudgetTargetEntityImplCopyWith<$Res> { + __$$BudgetTargetEntityImplCopyWithImpl(_$BudgetTargetEntityImpl _value, + $Res Function(_$BudgetTargetEntityImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetTargetEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? id = freezed, + Object? clientId = freezed, + Object? name = freezed, + }) { + return _then(_$BudgetTargetEntityImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as BudgetTargetType, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$BudgetTargetEntityImpl implements _BudgetTargetEntity { + const _$BudgetTargetEntityImpl( + {required this.type, this.id, this.clientId, this.name}); + + @override + final BudgetTargetType type; + @override + final int? id; + @override + final String? clientId; + @override + final String? name; + + @override + String toString() { + return 'BudgetTargetEntity(type: $type, id: $id, clientId: $clientId, name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetTargetEntityImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.id, id) || other.id == id) && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.name, name) || other.name == name)); + } + + @override + int get hashCode => Object.hash(runtimeType, type, id, clientId, name); + + /// Create a copy of BudgetTargetEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetTargetEntityImplCopyWith<_$BudgetTargetEntityImpl> get copyWith => + __$$BudgetTargetEntityImplCopyWithImpl<_$BudgetTargetEntityImpl>( + this, _$identity); +} + +abstract class _BudgetTargetEntity implements BudgetTargetEntity { + const factory _BudgetTargetEntity( + {required final BudgetTargetType type, + final int? id, + final String? clientId, + final String? name}) = _$BudgetTargetEntityImpl; + + @override + BudgetTargetType get type; + @override + int? get id; + @override + String? get clientId; + @override + String? get name; + + /// Create a copy of BudgetTargetEntity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetTargetEntityImplCopyWith<_$BudgetTargetEntityImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/domain/repositories/budget_repository.dart b/lib/domain/repositories/budget_repository.dart new file mode 100644 index 00000000..5be3d8c3 --- /dev/null +++ b/lib/domain/repositories/budget_repository.dart @@ -0,0 +1,66 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/entities/budget_period_state_entity.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/domain/entities/budget_target_entity.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class BudgetTargetSelection { + final BudgetTargetType type; + final String clientId; + const BudgetTargetSelection({required this.type, required this.clientId}); +} + +abstract class BudgetRepository { + Future>> getAllBudgets({bool? active}); + Future> getBudget(String clientId); + + Future> insertBudget({ + required String name, + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + DateTime? endDate, + String? description, + bool rolloverEnabled = false, + int thresholdPercent = 80, + bool forecastAlertsEnabled = false, + bool isActive = true, + List targets = const [], + }); + + Future> updateBudget( + String clientId, { + String? name, + double? amount, + String? currency, + BudgetPeriodType? periodType, + DateTime? startDate, + DateTime? endDate, + String? description, + bool? rolloverEnabled, + int? thresholdPercent, + bool? forecastAlertsEnabled, + bool? isActive, + List? targets, + }); + + Future> deleteBudget(String clientId); + + Stream>> listenToBudgets({bool? active}); + Stream>> listenToTargetsForBudget( + String budgetClientId); + Stream>> listenToPeriodStates( + String budgetClientId); + + Future> fetchBudgetProgress(int id); + Future> fetchBudgetTransactions( + int id, + {int limit = 50}); + Future> closeBudgetPeriod(int id); + + Future> refreshPeriodStates(); +} diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index af8ea80e..f3f8277d 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -26,6 +26,7 @@ import 'package:trakli/presentation/auth/cubits/login/login_cubit.dart'; import 'package:trakli/presentation/auth/cubits/oauth/oauth_cubit.dart'; import 'package:trakli/presentation/auth/cubits/register/register_cubit.dart'; import 'package:trakli/presentation/benefits/cubit/benefits_cubit.dart'; +import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; import 'package:trakli/presentation/category/cubit/category_cubit.dart'; import 'package:trakli/presentation/config/cubit/config_cubit.dart'; import 'package:trakli/presentation/config/theme_cubit/theme_cubit.dart'; @@ -128,6 +129,9 @@ class AppWidget extends StatelessWidget { BlocProvider( create: (_) => getIt(), ), + BlocProvider( + create: (_) => getIt(), + ), ], child: const AppView(), ); diff --git a/lib/presentation/budget/add_budget_screen.dart b/lib/presentation/budget/add_budget_screen.dart new file mode 100644 index 00000000..f42f01d1 --- /dev/null +++ b/lib/presentation/budget/add_budget_screen.dart @@ -0,0 +1,722 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/entities/category_entity.dart'; +import 'package:trakli/domain/entities/group_entity.dart'; +import 'package:trakli/domain/entities/wallet_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; +import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; +import 'package:trakli/presentation/category/cubit/category_cubit.dart'; +import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/wallets/cubit/wallet_cubit.dart'; + +class AddBudgetScreen extends StatefulWidget { + final BudgetEntity? budget; + const AddBudgetScreen({super.key, this.budget}); + + @override + State createState() => _AddBudgetScreenState(); +} + +class _AddBudgetScreenState extends State { + late final TextEditingController _name; + late final TextEditingController _amount; + late final TextEditingController _currency; + late final TextEditingController _description; + + late BudgetPeriodType _periodType; + late DateTime _startDate; + DateTime? _endDate; + late int _threshold; + late bool _rolloverEnabled; + late bool _forecastAlertsEnabled; + late bool _isActive; + late List _targets; + + bool get _isEdit => widget.budget != null; + + @override + void initState() { + super.initState(); + final b = widget.budget; + _name = TextEditingController(text: b?.name ?? ''); + _amount = TextEditingController(text: b?.amount.toStringAsFixed(2) ?? ''); + _currency = TextEditingController(text: b?.currency ?? 'USD'); + _description = TextEditingController(text: b?.description ?? ''); + _periodType = b?.periodType ?? BudgetPeriodType.monthly; + _startDate = b?.startDate ?? DateTime.now(); + _endDate = b?.endDate; + _threshold = b?.thresholdPercent ?? 80; + _rolloverEnabled = b?.rolloverEnabled ?? false; + _forecastAlertsEnabled = b?.forecastAlertsEnabled ?? false; + _isActive = b?.isActive ?? true; + _targets = b?.targets + .where((t) => t.clientId != null) + .map((t) => + BudgetTargetSelection(type: t.type, clientId: t.clientId!)) + .toList() ?? + []; + } + + @override + void dispose() { + _name.dispose(); + _amount.dispose(); + _currency.dispose(); + _description.dispose(); + super.dispose(); + } + + Future _pickDate({required bool isStart}) async { + final initial = isStart ? _startDate : (_endDate ?? _startDate); + final picked = await showDatePicker( + context: context, + initialDate: initial, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + if (picked == null) return; + setState(() { + if (isStart) { + _startDate = picked; + } else { + _endDate = picked; + } + }); + } + + Future _pickTargets() async { + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + builder: (_) => _TargetPickerSheet(initial: _targets), + ); + if (result != null) { + setState(() => _targets = result); + } + } + + Future _submit() async { + final name = _name.text.trim(); + if (name.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please enter a name')), + ); + return; + } + final amount = double.tryParse(_amount.text.trim()); + if (amount == null || amount <= 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please enter a valid amount')), + ); + return; + } + final currency = _currency.text.trim().toUpperCase(); + if (currency.length != 3) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Currency must be 3 letters')), + ); + return; + } + + final cubit = context.read(); + if (_isEdit) { + await cubit.updateBudget( + widget.budget!.clientId, + name: name, + amount: amount, + currency: currency, + periodType: _periodType, + startDate: _startDate, + endDate: _endDate, + description: _description.text.trim().isEmpty + ? null + : _description.text.trim(), + rolloverEnabled: _rolloverEnabled, + thresholdPercent: _threshold, + forecastAlertsEnabled: _forecastAlertsEnabled, + isActive: _isActive, + targets: _targets, + ); + } else { + await cubit.addBudget( + name: name, + amount: amount, + currency: currency, + periodType: _periodType, + startDate: _startDate, + endDate: _endDate, + description: _description.text.trim().isEmpty + ? null + : _description.text.trim(), + rolloverEnabled: _rolloverEnabled, + thresholdPercent: _threshold, + forecastAlertsEnabled: _forecastAlertsEnabled, + isActive: _isActive, + targets: _targets, + ); + } + if (!mounted) return; + AppNavigator.pop(context); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Scaffold( + backgroundColor: tones.bgPage, + appBar: AppBar( + title: Text(_isEdit ? 'Edit budget' : 'New budget'), + ), + body: BlocBuilder( + builder: (context, state) { + return Stack( + children: [ + SingleChildScrollView( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 96.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SectionLabel(text: 'Name'), + TextField( + controller: _name, + decoration: const InputDecoration( + hintText: 'e.g. Groceries', + border: OutlineInputBorder(), + ), + ), + SizedBox(height: 14.h), + _SectionLabel(text: 'Amount'), + Row( + children: [ + Expanded( + flex: 2, + child: TextField( + controller: _amount, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'[0-9.]'), + ), + ], + decoration: const InputDecoration( + hintText: '0.00', + border: OutlineInputBorder(), + ), + ), + ), + SizedBox(width: 10.w), + Expanded( + child: TextField( + controller: _currency, + textCapitalization: TextCapitalization.characters, + maxLength: 3, + decoration: const InputDecoration( + counterText: '', + border: OutlineInputBorder(), + ), + ), + ), + ], + ), + SizedBox(height: 14.h), + _SectionLabel(text: 'Period'), + _PeriodChips( + selected: _periodType, + onChange: (p) => setState(() => _periodType = p), + ), + SizedBox(height: 14.h), + _SectionLabel(text: 'Start date'), + _DateRow( + date: _startDate, + onTap: () => _pickDate(isStart: true), + ), + if (_periodType == BudgetPeriodType.custom) ...[ + SizedBox(height: 14.h), + _SectionLabel(text: 'End date'), + _DateRow( + date: _endDate, + onTap: () => _pickDate(isStart: false), + placeholder: 'Select end date', + ), + ], + SizedBox(height: 14.h), + _SectionLabel(text: 'Targets'), + InkWell( + onTap: _pickTargets, + borderRadius: BorderRadius.circular(AppRadii.md), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + border: Border.all(color: tones.borderLight), + borderRadius: BorderRadius.circular(AppRadii.md), + ), + child: Row( + children: [ + Expanded( + child: Text( + _targets.isEmpty + ? 'Apply to all transactions' + : '${_targets.length} ${_targets.length == 1 ? 'target' : 'targets'} selected', + style: TextStyle( + fontSize: 14.sp, + color: _targets.isEmpty + ? tones.textMuted + : tones.textPrimary, + ), + ), + ), + Icon( + Icons.chevron_right, + size: 20.sp, + color: tones.textMuted, + ), + ], + ), + ), + ), + SizedBox(height: 14.h), + _SectionLabel(text: 'Alert at $_threshold% used'), + Slider( + value: _threshold.toDouble(), + min: 50, + max: 100, + divisions: 10, + label: '$_threshold%', + onChanged: (v) => setState(() => _threshold = v.toInt()), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Roll over unused amount'), + subtitle: const Text( + 'Carry leftover budget into next period', + ), + value: _rolloverEnabled, + onChanged: (v) => setState(() => _rolloverEnabled = v), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Forecast alerts'), + subtitle: const Text( + 'Warn me if I\'m projected to overspend', + ), + value: _forecastAlertsEnabled, + onChanged: (v) => + setState(() => _forecastAlertsEnabled = v), + ), + if (_isEdit) + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Active'), + value: _isActive, + onChanged: (v) => setState(() => _isActive = v), + ), + SizedBox(height: 14.h), + _SectionLabel(text: 'Description (optional)'), + TextField( + controller: _description, + maxLines: 3, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 18.h), + decoration: BoxDecoration( + color: tones.bgPage, + border: Border( + top: BorderSide(color: tones.borderLight), + ), + ), + child: SafeArea( + top: false, + child: FilledButton( + onPressed: state.isSaving ? null : _submit, + style: FilledButton.styleFrom( + minimumSize: Size(double.infinity, 48.h), + ), + child: state.isSaving + ? const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(_isEdit ? 'Save changes' : 'Create budget'), + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} + +class _SectionLabel extends StatelessWidget { + final String text; + const _SectionLabel({required this.text}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 6.h), + child: Text( + text, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w700, + letterSpacing: 0.4, + color: context.tones.textSecondary, + ), + ), + ); + } +} + +class _PeriodChips extends StatelessWidget { + final BudgetPeriodType selected; + final ValueChanged onChange; + const _PeriodChips({required this.selected, required this.onChange}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + Widget chip(BudgetPeriodType type, String label) { + final active = type == selected; + return GestureDetector( + onTap: () => onChange(type), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 9.h), + margin: EdgeInsets.only(right: 8.w), + decoration: BoxDecoration( + color: active ? tones.brand.accent : tones.bgSurface, + border: Border.all( + color: active ? tones.brand.accent : tones.borderLight, + ), + borderRadius: BorderRadius.circular(AppRadii.pill), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: active ? Colors.white : tones.textSecondary, + ), + ), + ), + ); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + chip(BudgetPeriodType.weekly, 'Weekly'), + chip(BudgetPeriodType.monthly, 'Monthly'), + chip(BudgetPeriodType.yearly, 'Yearly'), + chip(BudgetPeriodType.custom, 'Custom'), + ], + ), + ); + } +} + +class _DateRow extends StatelessWidget { + final DateTime? date; + final VoidCallback onTap; + final String placeholder; + const _DateRow({ + required this.date, + required this.onTap, + this.placeholder = 'Select date', + }); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final label = date == null + ? placeholder + : '${date!.year}-${date!.month.toString().padLeft(2, '0')}-${date!.day.toString().padLeft(2, '0')}'; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(AppRadii.md), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + border: Border.all(color: tones.borderLight), + borderRadius: BorderRadius.circular(AppRadii.md), + ), + child: Row( + children: [ + Icon( + Icons.calendar_today_outlined, + size: 18.sp, + color: tones.textMuted, + ), + SizedBox(width: 10.w), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: date == null + ? tones.textMuted + : tones.textPrimary, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _TargetPickerSheet extends StatefulWidget { + final List initial; + const _TargetPickerSheet({required this.initial}); + + @override + State<_TargetPickerSheet> createState() => _TargetPickerSheetState(); +} + +class _TargetPickerSheetState extends State<_TargetPickerSheet> + with SingleTickerProviderStateMixin { + late TabController _tabs; + late Set _selectedKeys; + + String _key(BudgetTargetType t, String clientId) => '${t.name}::$clientId'; + + @override + void initState() { + super.initState(); + _tabs = TabController(length: 3, vsync: this); + _selectedKeys = widget.initial.map((t) => _key(t.type, t.clientId)).toSet(); + } + + @override + void dispose() { + _tabs.dispose(); + super.dispose(); + } + + void _toggle(BudgetTargetType type, String clientId) { + final key = _key(type, clientId); + setState(() { + if (_selectedKeys.contains(key)) { + _selectedKeys.remove(key); + } else { + _selectedKeys.add(key); + } + }); + } + + void _confirm() { + final selections = _selectedKeys.map((k) { + final parts = k.split('::'); + final type = + BudgetTargetType.tryParse(parts[0]) ?? BudgetTargetType.category; + return BudgetTargetSelection(type: type, clientId: parts[1]); + }).toList(); + Navigator.of(context).pop(selections); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.7, + maxChildSize: 0.95, + builder: (_, scrollController) { + return Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 6.h), + child: Row( + children: [ + Expanded( + child: Text( + 'Select targets', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + ), + ), + TextButton(onPressed: _confirm, child: const Text('Done')), + ], + ), + ), + TabBar( + controller: _tabs, + tabs: const [ + Tab(text: 'Categories'), + Tab(text: 'Wallets'), + Tab(text: 'Groups'), + ], + ), + Expanded( + child: TabBarView( + controller: _tabs, + children: [ + _CategoryList( + selectedKeys: _selectedKeys, + onToggle: (id) => _toggle(BudgetTargetType.category, id), + scrollController: scrollController, + ), + _WalletList( + selectedKeys: _selectedKeys, + onToggle: (id) => _toggle(BudgetTargetType.wallet, id), + scrollController: scrollController, + ), + _GroupList( + selectedKeys: _selectedKeys, + onToggle: (id) => _toggle(BudgetTargetType.group, id), + scrollController: scrollController, + ), + ], + ), + ), + ], + ); + }, + ); + } +} + +class _CategoryList extends StatelessWidget { + final Set selectedKeys; + final ValueChanged onToggle; + final ScrollController scrollController; + const _CategoryList({ + required this.selectedKeys, + required this.onToggle, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final items = state.categories; + if (items.isEmpty) { + return const Center(child: Text('No categories yet')); + } + return ListView.builder( + controller: scrollController, + itemCount: items.length, + itemBuilder: (_, i) => _TargetTile( + label: items[i].name, + selected: + selectedKeys.contains('category::${items[i].clientId}'), + onToggle: () => onToggle(items[i].clientId), + ), + ); + }, + ); + } +} + +class _WalletList extends StatelessWidget { + final Set selectedKeys; + final ValueChanged onToggle; + final ScrollController scrollController; + const _WalletList({ + required this.selectedKeys, + required this.onToggle, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final items = state.wallets; + if (items.isEmpty) { + return const Center(child: Text('No wallets yet')); + } + return ListView.builder( + controller: scrollController, + itemCount: items.length, + itemBuilder: (_, i) => _TargetTile( + label: items[i].name, + selected: selectedKeys.contains('wallet::${items[i].clientId}'), + onToggle: () => onToggle(items[i].clientId), + ), + ); + }, + ); + } +} + +class _GroupList extends StatelessWidget { + final Set selectedKeys; + final ValueChanged onToggle; + final ScrollController scrollController; + const _GroupList({ + required this.selectedKeys, + required this.onToggle, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final items = state.groups; + if (items.isEmpty) { + return const Center(child: Text('No groups yet')); + } + return ListView.builder( + controller: scrollController, + itemCount: items.length, + itemBuilder: (_, i) => _TargetTile( + label: items[i].name, + selected: selectedKeys.contains('group::${items[i].clientId}'), + onToggle: () => onToggle(items[i].clientId), + ), + ); + }, + ); + } +} + +class _TargetTile extends StatelessWidget { + final String label; + final bool selected; + final VoidCallback onToggle; + const _TargetTile({ + required this.label, + required this.selected, + required this.onToggle, + }); + + @override + Widget build(BuildContext context) { + return CheckboxListTile( + value: selected, + onChanged: (_) => onToggle(), + title: Text(label), + controlAffinity: ListTileControlAffinity.leading, + ); + } +} diff --git a/lib/presentation/budget/budget_detail_screen.dart b/lib/presentation/budget/budget_detail_screen.dart new file mode 100644 index 00000000..acf4e04a --- /dev/null +++ b/lib/presentation/budget/budget_detail_screen.dart @@ -0,0 +1,581 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/entities/budget_period_state_entity.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/presentation/budget/add_budget_screen.dart'; +import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class BudgetDetailScreen extends StatefulWidget { + final BudgetEntity budget; + const BudgetDetailScreen({super.key, required this.budget}); + + @override + State createState() => _BudgetDetailScreenState(); +} + +class _BudgetDetailScreenState extends State { + @override + void initState() { + super.initState(); + final cubit = context.read(); + cubit.watchBudget(widget.budget.clientId); + final id = widget.budget.id; + if (id != null) { + cubit.fetchProgress(id); + cubit.refreshPeriodStates(); + } + } + + BudgetEntity _currentBudget(BudgetState state) { + return state.budgets.firstWhere( + (b) => b.clientId == widget.budget.clientId, + orElse: () => widget.budget, + ); + } + + Future _confirmDelete(BudgetEntity budget) async { + final confirm = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Delete budget?'), + content: Text( + 'This will remove "${budget.name}" from your budgets. This cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + style: FilledButton.styleFrom(backgroundColor: Colors.redAccent), + child: const Text('Delete'), + ), + ], + ), + ); + if (confirm != true || !mounted) return; + await context.read().deleteBudget(budget.clientId); + if (mounted) AppNavigator.pop(context); + } + + Future _confirmClosePeriod(BudgetEntity budget) async { + final id = budget.id; + if (id == null) return; + final confirm = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Close this period?'), + content: const Text( + 'This will lock the current period\'s spending and start a new one. ' + 'Unused amount will roll into the next period.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Close period'), + ), + ], + ), + ); + if (confirm != true || !mounted) return; + await context.read().closeBudgetPeriod(id); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Scaffold( + backgroundColor: tones.bgPage, + appBar: AppBar( + title: const Text('Budget'), + actions: [ + BlocBuilder( + builder: (context, state) { + final budget = _currentBudget(state); + return Row( + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => AppNavigator.push( + context, + AddBudgetScreen(budget: budget), + ), + ), + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () => _confirmDelete(budget), + ), + ], + ); + }, + ), + ], + ), + body: BlocBuilder( + builder: (context, state) { + final budget = _currentBudget(state); + final progress = state.selectedBudgetProgress; + final targets = state.selectedBudgetTargets; + final periods = state.selectedBudgetPeriodStates; + return RefreshIndicator( + onRefresh: () async { + final id = budget.id; + if (id != null) { + await context.read().fetchProgress(id); + await context.read().refreshPeriodStates(); + } + }, + child: ListView( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 24.h), + children: [ + _HeaderCard(budget: budget, progress: progress), + SizedBox(height: 14.h), + if (state.isProgressLoading && progress == null) + const Center(child: CircularProgressIndicator()) + else if (progress != null) + _KpiGrid(budget: budget, progress: progress) + else if (budget.id == null) + _InfoBanner( + text: + 'Progress will be available once this budget syncs with the server.', + ), + SizedBox(height: 20.h), + _SectionHeader(text: 'Targets'), + _TargetsBlock(budget: budget, targets: targets), + SizedBox(height: 20.h), + _SectionHeader(text: 'Period history'), + _PeriodHistoryBlock(periods: periods), + if (budget.rolloverEnabled && budget.id != null) ...[ + SizedBox(height: 24.h), + FilledButton.icon( + onPressed: state.isClosingPeriod + ? null + : () => _confirmClosePeriod(budget), + icon: state.isClosingPeriod + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.lock_outline), + label: const Text('Close current period'), + ), + ], + ], + ), + ); + }, + ), + ); + } +} + +class _HeaderCard extends StatelessWidget { + final BudgetEntity budget; + final BudgetProgressEntity? progress; + const _HeaderCard({required this.budget, this.progress}); + + Color _statusColor(BuildContext context, BudgetStatus? status) { + final tones = context.tones; + return switch (status) { + BudgetStatus.overBudget => Colors.redAccent, + BudgetStatus.forecastBreach => Colors.orangeAccent, + BudgetStatus.nearLimit => Colors.amber.shade700, + BudgetStatus.onTrack || null => tones.brand.accent, + }; + } + + String _statusLabel(BudgetStatus? status) { + return switch (status) { + BudgetStatus.overBudget => 'OVER BUDGET', + BudgetStatus.forecastBreach => 'FORECAST BREACH', + BudgetStatus.nearLimit => 'NEAR LIMIT', + BudgetStatus.onTrack => 'ON TRACK', + null => 'AWAITING SYNC', + }; + } + + String _periodLabel() { + final start = budget.startDate; + final fmt = (DateTime d) => + '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; + if (budget.periodType == BudgetPeriodType.custom && + budget.endDate != null) { + return '${fmt(start)} → ${fmt(budget.endDate!)}'; + } + return switch (budget.periodType) { + BudgetPeriodType.weekly => 'Weekly · starting ${fmt(start)}', + BudgetPeriodType.monthly => 'Monthly · starting ${fmt(start)}', + BudgetPeriodType.yearly => 'Yearly · starting ${fmt(start)}', + BudgetPeriodType.custom => fmt(start), + }; + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + final statusColor = _statusColor(context, progress?.status); + final percent = progress?.percentUsed ?? 0; + final clampedPercent = (percent / 100).clamp(0.0, 1.0); + + return Container( + padding: EdgeInsets.all(16.r), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 8.w), + Text( + _statusLabel(progress?.status), + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w800, + letterSpacing: 1.0, + color: statusColor, + ), + ), + ], + ), + SizedBox(height: 8.h), + Text( + budget.name, + style: TextStyle( + fontSize: 22.sp, + fontWeight: FontWeight.w800, + color: tones.textPrimary, + ), + ), + SizedBox(height: 4.h), + Text( + _periodLabel(), + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + ), + ), + if (budget.description != null && + budget.description!.isNotEmpty) ...[ + SizedBox(height: 8.h), + Text( + budget.description!, + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, + ), + ), + ], + SizedBox(height: 16.h), + ClipRRect( + borderRadius: BorderRadius.circular(AppRadii.pill), + child: LinearProgressIndicator( + value: clampedPercent, + minHeight: 14, + backgroundColor: tones.bgPage, + valueColor: AlwaysStoppedAnimation(statusColor), + ), + ), + SizedBox(height: 10.h), + if (progress != null) + Text( + '${budget.currency} ${progress!.netSpent.toStringAsFixed(2)}' + ' of ' + '${budget.currency} ${progress!.effectiveLimit.toStringAsFixed(2)}' + ' · ${progress!.percentUsed.toStringAsFixed(0)}%', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: tones.textPrimary, + ), + ) + else + Text( + '${budget.currency} ${budget.amount.toStringAsFixed(2)} budget', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: tones.textPrimary, + ), + ), + ], + ), + ); + } +} + +class _KpiGrid extends StatelessWidget { + final BudgetEntity budget; + final BudgetProgressEntity progress; + const _KpiGrid({required this.budget, required this.progress}); + + @override + Widget build(BuildContext context) { + final cells = <_Kpi>[ + _Kpi('Remaining', + '${budget.currency} ${progress.remaining.toStringAsFixed(2)}'), + _Kpi('Projected', + '${budget.currency} ${progress.projectedSpend.toStringAsFixed(2)}'), + _Kpi('Refunds', + '${budget.currency} ${progress.refunds.toStringAsFixed(2)}'), + _Kpi('Rollover in', + '${budget.currency} ${progress.rolloverIn.toStringAsFixed(2)}'), + ]; + + return GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 10.h, + crossAxisSpacing: 10.w, + childAspectRatio: 2.4, + children: cells.map((c) => _KpiCard(kpi: c)).toList(), + ); + } +} + +class _Kpi { + final String label; + final String value; + const _Kpi(this.label, this.value); +} + +class _KpiCard extends StatelessWidget { + final _Kpi kpi; + const _KpiCard({required this.kpi}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + padding: EdgeInsets.all(12.r), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.md), + border: Border.all(color: tones.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + kpi.label, + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: tones.textMuted, + ), + ), + SizedBox(height: 4.h), + Text( + kpi.value, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w800, + color: tones.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + final String text; + const _SectionHeader({required this.text}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: Text( + text, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w800, + letterSpacing: 0.4, + color: context.tones.textSecondary, + ), + ), + ); + } +} + +class _TargetsBlock extends StatelessWidget { + final BudgetEntity budget; + final List targets; + const _TargetsBlock({required this.budget, required this.targets}); + + String _typeLabel(BudgetTargetType type) { + return switch (type) { + BudgetTargetType.category => 'Category', + BudgetTargetType.wallet => 'Wallet', + BudgetTargetType.group => 'Group', + }; + } + + IconData _iconFor(BudgetTargetType type) { + return switch (type) { + BudgetTargetType.category => Icons.label_outline, + BudgetTargetType.wallet => Icons.account_balance_wallet_outlined, + BudgetTargetType.group => Icons.group_outlined, + }; + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + if (budget.targets.isEmpty) { + return Container( + padding: EdgeInsets.all(14.r), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.md), + border: Border.all(color: tones.borderLight), + ), + child: Text( + 'Applies to all transactions in this period.', + style: TextStyle(fontSize: 13.sp, color: tones.textSecondary), + ), + ); + } + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.md), + border: Border.all(color: tones.borderLight), + ), + child: Column( + children: [ + for (var i = 0; i < budget.targets.length; i++) ...[ + ListTile( + leading: Icon( + _iconFor(budget.targets[i].type), + color: tones.textMuted, + ), + title: Text(budget.targets[i].name ?? '(unnamed)'), + subtitle: Text(_typeLabel(budget.targets[i].type)), + dense: true, + ), + if (i != budget.targets.length - 1) + Divider(height: 1, color: tones.borderLight), + ], + ], + ), + ); + } +} + +class _PeriodHistoryBlock extends StatelessWidget { + final List periods; + const _PeriodHistoryBlock({required this.periods}); + + String _fmtDate(DateTime d) => + '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; + + @override + Widget build(BuildContext context) { + final tones = context.tones; + if (periods.isEmpty) { + return Container( + padding: EdgeInsets.all(14.r), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.md), + border: Border.all(color: tones.borderLight), + ), + child: Text( + 'No closed periods yet.', + style: TextStyle(fontSize: 13.sp, color: tones.textSecondary), + ), + ); + } + return Container( + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.md), + border: Border.all(color: tones.borderLight), + ), + child: Column( + children: [ + for (var i = 0; i < periods.length; i++) ...[ + ListTile( + dense: true, + title: Text( + '${_fmtDate(periods[i].periodStart)} → ${_fmtDate(periods[i].periodEnd)}', + ), + subtitle: Text( + 'Spent ${periods[i].netSpent.toStringAsFixed(2)}' + ' · rollover in ${periods[i].rolloverIn.toStringAsFixed(2)}' + ' · rollover out ${periods[i].rolloverOut.toStringAsFixed(2)}', + ), + trailing: periods[i].closedAt == null + ? const Text('Open', + style: TextStyle(fontWeight: FontWeight.w600)) + : null, + ), + if (i != periods.length - 1) + Divider(height: 1, color: tones.borderLight), + ], + ], + ), + ); + } +} + +class _InfoBanner extends StatelessWidget { + final String text; + const _InfoBanner({required this.text}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + padding: EdgeInsets.all(12.r), + decoration: BoxDecoration( + color: tones.brand.accent.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(AppRadii.md), + ), + child: Row( + children: [ + Icon(Icons.info_outline, + color: tones.brand.accent, size: 18.sp), + SizedBox(width: 8.w), + Expanded( + child: Text( + text, + style: TextStyle(fontSize: 13.sp, color: tones.textPrimary), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/budget/budget_screen.dart b/lib/presentation/budget/budget_screen.dart new file mode 100644 index 00000000..4165b281 --- /dev/null +++ b/lib/presentation/budget/budget_screen.dart @@ -0,0 +1,382 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/presentation/budget/add_budget_screen.dart'; +import 'package:trakli/presentation/budget/budget_detail_screen.dart'; +import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; +import 'package:trakli/presentation/utils/app_navigator.dart'; +import 'package:trakli/presentation/utils/design_tokens.dart'; +import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/page_app_bar.dart'; + +class BudgetScreen extends StatefulWidget { + const BudgetScreen({super.key}); + + @override + State createState() => _BudgetScreenState(); +} + +class _BudgetScreenState extends State { + bool _onlyActive = false; + String _query = ''; + + @override + void initState() { + super.initState(); + context.read().loadBudgets(); + } + + void _add() { + AppNavigator.push(context, const AddBudgetScreen()); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + + return Scaffold( + backgroundColor: tones.bgPage, + appBar: PageAppBar( + title: 'Budgets', + onSearchChanged: (v) => setState(() => _query = v), + searchHint: 'Search budgets', + actions: [ + PageAppBarAction(icon: Icons.add, onTap: _add, primary: true), + ], + ), + body: BlocBuilder( + builder: (context, state) { + if (state.isLoading && state.budgets.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + final q = _query.trim().toLowerCase(); + final filtered = state.budgets.where((b) { + if (_onlyActive && !b.isActive) return false; + if (q.isEmpty) return true; + return '${b.name} ${b.description ?? ''}' + .toLowerCase() + .contains(q); + }).toList(); + + if (state.budgets.isEmpty) { + return _EmptyState(onAdd: _add); + } + + return Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 4.h), + child: _FilterChips( + onlyActive: _onlyActive, + onChange: (v) => setState(() => _onlyActive = v), + ), + ), + Expanded( + child: filtered.isEmpty + ? _NoMatches(query: _query) + : ListView.separated( + padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 24.h), + itemCount: filtered.length, + separatorBuilder: (_, __) => SizedBox(height: 10.h), + itemBuilder: (_, i) { + final budget = filtered[i]; + return _BudgetCard( + budget: budget, + onTap: () => AppNavigator.push( + context, + BudgetDetailScreen(budget: budget), + ), + onEdit: () => AppNavigator.push( + context, + AddBudgetScreen(budget: budget), + ), + onDelete: () => context + .read() + .deleteBudget(budget.clientId), + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _FilterChips extends StatelessWidget { + final bool onlyActive; + final ValueChanged onChange; + const _FilterChips({required this.onlyActive, required this.onChange}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + Widget chip(String label, bool selected, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h), + decoration: BoxDecoration( + color: selected ? tones.brand.accent : tones.bgSurface, + border: Border.all( + color: selected ? tones.brand.accent : tones.borderLight, + ), + borderRadius: BorderRadius.circular(AppRadii.pill), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: selected ? Colors.white : tones.textSecondary, + ), + ), + ), + ); + } + + return Row( + children: [ + chip('All', !onlyActive, () => onChange(false)), + SizedBox(width: 8.w), + chip('Active', onlyActive, () => onChange(true)), + ], + ); + } +} + +class _BudgetCard extends StatelessWidget { + final BudgetEntity budget; + final VoidCallback onTap; + final VoidCallback onEdit; + final VoidCallback onDelete; + + const _BudgetCard({ + required this.budget, + required this.onTap, + required this.onEdit, + required this.onDelete, + }); + + String _periodLabel() { + return switch (budget.periodType) { + BudgetPeriodType.weekly => 'Weekly', + BudgetPeriodType.monthly => 'Monthly', + BudgetPeriodType.yearly => 'Yearly', + BudgetPeriodType.custom => 'Custom', + }; + } + + String _periodShort() { + return switch (budget.periodType) { + BudgetPeriodType.weekly => '/wk', + BudgetPeriodType.monthly => '/mo', + BudgetPeriodType.yearly => '/yr', + BudgetPeriodType.custom => '', + }; + } + + String _scopeText() { + if (budget.targets.isEmpty) return 'All transactions'; + final byType = {}; + for (final t in budget.targets) { + byType[t.type] = (byType[t.type] ?? 0) + 1; + } + final parts = byType.entries.map((e) { + final label = switch (e.key) { + BudgetTargetType.category => + '${e.value} ${e.value == 1 ? 'category' : 'categories'}', + BudgetTargetType.wallet => + '${e.value} ${e.value == 1 ? 'wallet' : 'wallets'}', + BudgetTargetType.group => + '${e.value} ${e.value == 1 ? 'group' : 'groups'}', + }; + return label; + }); + return parts.join(' · '); + } + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(AppRadii.lg), + child: Container( + padding: EdgeInsets.all(14.r), + decoration: BoxDecoration( + color: tones.bgSurface, + borderRadius: BorderRadius.circular(AppRadii.lg), + border: Border.all(color: tones.borderLight), + boxShadow: context.elevations.level1, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + budget.name, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + SizedBox(width: 8.w), + Text( + '${budget.currency} ${budget.amount.toStringAsFixed(0)}${_periodShort()}', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + ), + ), + SizedBox(width: 4.w), + PopupMenuButton( + padding: EdgeInsets.zero, + iconSize: 18.sp, + itemBuilder: (_) => [ + const PopupMenuItem(value: 'edit', child: Text('Edit')), + const PopupMenuItem( + value: 'delete', child: Text('Delete')), + ], + onSelected: (v) { + if (v == 'edit') onEdit(); + if (v == 'delete') onDelete(); + }, + ), + ], + ), + SizedBox(height: 6.h), + Row( + children: [ + _MiniChip(label: _periodLabel()), + SizedBox(width: 8.w), + Flexible( + child: Text( + _scopeText(), + style: TextStyle( + fontSize: 12.sp, + color: tones.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (!budget.isActive) ...[ + SizedBox(width: 8.w), + _MiniChip(label: 'Inactive', muted: true), + ], + ], + ), + ], + ), + ), + ); + } +} + +class _MiniChip extends StatelessWidget { + final String label; + final bool muted; + const _MiniChip({required this.label, this.muted = false}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), + decoration: BoxDecoration( + color: muted + ? tones.bgPage + : tones.brand.accent.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(AppRadii.pill), + ), + child: Text( + label, + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w600, + color: muted ? tones.textMuted : tones.brand.accent, + ), + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + final VoidCallback onAdd; + const _EmptyState({required this.onAdd}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 32.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.savings_outlined, size: 56.sp, color: tones.textMuted), + SizedBox(height: 16.h), + Text( + 'No budgets yet', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: tones.textPrimary, + ), + ), + SizedBox(height: 6.h), + Text( + 'Track your spending against a target.\nSet a limit and we\'ll watch it for you.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.sp, + color: tones.textSecondary, + ), + ), + SizedBox(height: 20.h), + FilledButton.icon( + onPressed: onAdd, + icon: const Icon(Icons.add), + label: const Text('Create budget'), + ), + ], + ), + ), + ); + } +} + +class _NoMatches extends StatelessWidget { + final String query; + const _NoMatches({required this.query}); + + @override + Widget build(BuildContext context) { + final tones = context.tones; + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 48.h), + child: Center( + child: Text( + query.isEmpty + ? 'No budgets match this filter.' + : 'No matches for "$query".', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14.sp, color: tones.textSecondary), + ), + ), + ); + } +} diff --git a/lib/presentation/budget/cubit/budget_cubit.dart b/lib/presentation/budget/cubit/budget_cubit.dart new file mode 100644 index 00000000..0094461e --- /dev/null +++ b/lib/presentation/budget/cubit/budget_cubit.dart @@ -0,0 +1,243 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/entities/budget_period_state_entity.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/domain/entities/budget_target_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +part 'budget_cubit.freezed.dart'; +part 'budget_state.dart'; + +@injectable +class BudgetCubit extends Cubit { + final BudgetRepository _repository; + StreamSubscription? _budgetsSubscription; + StreamSubscription? _targetsSubscription; + StreamSubscription? _periodStatesSubscription; + String? _watchedBudgetClientId; + + BudgetCubit(this._repository) : super(BudgetState.initial()) { + listenToBudgets(); + } + + Future loadBudgets({bool? active}) async { + emit(state.copyWith(isLoading: true, failure: const Failure.none())); + final result = await _repository.getAllBudgets(active: active); + result.fold( + (failure) => emit(state.copyWith(isLoading: false, failure: failure)), + (budgets) => emit(state.copyWith( + isLoading: false, + budgets: budgets, + failure: const Failure.none(), + )), + ); + } + + void listenToBudgets({bool? active}) { + _budgetsSubscription?.cancel(); + _budgetsSubscription = + _repository.listenToBudgets(active: active).listen((either) { + either.fold( + (failure) => emit(state.copyWith(failure: failure)), + (budgets) => emit(state.copyWith( + budgets: budgets, + failure: const Failure.none(), + )), + ); + }); + } + + Future addBudget({ + required String name, + required double amount, + required String currency, + required BudgetPeriodType periodType, + required DateTime startDate, + DateTime? endDate, + String? description, + bool rolloverEnabled = false, + int thresholdPercent = 80, + bool forecastAlertsEnabled = false, + bool isActive = true, + List targets = const [], + }) async { + emit(state.copyWith(isSaving: true, failure: const Failure.none())); + final result = await _repository.insertBudget( + name: name, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + description: description, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + targets: targets, + ); + result.fold( + (failure) => emit(state.copyWith(isSaving: false, failure: failure)), + (_) => emit(state.copyWith( + isSaving: false, + failure: const Failure.none(), + )), + ); + } + + Future updateBudget( + String clientId, { + String? name, + double? amount, + String? currency, + BudgetPeriodType? periodType, + DateTime? startDate, + DateTime? endDate, + String? description, + bool? rolloverEnabled, + int? thresholdPercent, + bool? forecastAlertsEnabled, + bool? isActive, + List? targets, + }) async { + emit(state.copyWith(isSaving: true, failure: const Failure.none())); + final result = await _repository.updateBudget( + clientId, + name: name, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + description: description, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + targets: targets, + ); + result.fold( + (failure) => emit(state.copyWith(isSaving: false, failure: failure)), + (_) => emit(state.copyWith( + isSaving: false, + failure: const Failure.none(), + )), + ); + } + + Future deleteBudget(String clientId) async { + emit(state.copyWith(isDeleting: true, failure: const Failure.none())); + final optimistic = + state.budgets.where((b) => b.clientId != clientId).toList(); + emit(state.copyWith(budgets: optimistic)); + + final result = await _repository.deleteBudget(clientId); + result.fold( + (failure) => emit(state.copyWith(isDeleting: false, failure: failure)), + (_) => emit(state.copyWith( + isDeleting: false, + failure: const Failure.none(), + )), + ); + } + + void watchBudget(String clientId) { + if (_watchedBudgetClientId == clientId) return; + _watchedBudgetClientId = clientId; + + _targetsSubscription?.cancel(); + _targetsSubscription = + _repository.listenToTargetsForBudget(clientId).listen((either) { + either.fold( + (failure) => emit(state.copyWith(failure: failure)), + (targets) => emit(state.copyWith( + selectedBudgetTargets: targets, + failure: const Failure.none(), + )), + ); + }); + + _periodStatesSubscription?.cancel(); + _periodStatesSubscription = + _repository.listenToPeriodStates(clientId).listen((either) { + either.fold( + (failure) => emit(state.copyWith(failure: failure)), + (states) => emit(state.copyWith( + selectedBudgetPeriodStates: states, + failure: const Failure.none(), + )), + ); + }); + } + + Future fetchProgress(int serverId) async { + emit(state.copyWith(isProgressLoading: true)); + final result = await _repository.fetchBudgetProgress(serverId); + result.fold( + (failure) => emit(state.copyWith( + isProgressLoading: false, + failure: failure, + )), + (progress) => emit(state.copyWith( + isProgressLoading: false, + selectedBudgetProgress: progress, + failure: const Failure.none(), + )), + ); + } + + Future fetchPeriodTransactions(int serverId, {int limit = 50}) async { + emit(state.copyWith(isPeriodTransactionsLoading: true)); + final result = + await _repository.fetchBudgetTransactions(serverId, limit: limit); + result.fold( + (failure) => emit(state.copyWith( + isPeriodTransactionsLoading: false, + failure: failure, + )), + (response) => emit(state.copyWith( + isPeriodTransactionsLoading: false, + selectedBudgetTransactions: response, + failure: const Failure.none(), + )), + ); + } + + Future closeBudgetPeriod(int serverId) async { + emit(state.copyWith(isClosingPeriod: true)); + final result = await _repository.closeBudgetPeriod(serverId); + result.fold( + (failure) => emit(state.copyWith( + isClosingPeriod: false, + failure: failure, + )), + (_) async { + emit(state.copyWith( + isClosingPeriod: false, + failure: const Failure.none(), + )); + await _repository.refreshPeriodStates(); + await fetchProgress(serverId); + }, + ); + } + + Future refreshPeriodStates() async { + await _repository.refreshPeriodStates(); + } + + @override + Future close() { + _budgetsSubscription?.cancel(); + _targetsSubscription?.cancel(); + _periodStatesSubscription?.cancel(); + return super.close(); + } +} diff --git a/lib/presentation/budget/cubit/budget_cubit.freezed.dart b/lib/presentation/budget/cubit/budget_cubit.freezed.dart new file mode 100644 index 00000000..e5cf746b --- /dev/null +++ b/lib/presentation/budget/cubit/budget_cubit.freezed.dart @@ -0,0 +1,455 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'budget_cubit.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$BudgetState { + List get budgets => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + bool get isSaving => throw _privateConstructorUsedError; + bool get isDeleting => throw _privateConstructorUsedError; + bool get isProgressLoading => throw _privateConstructorUsedError; + bool get isPeriodTransactionsLoading => throw _privateConstructorUsedError; + bool get isClosingPeriod => throw _privateConstructorUsedError; + BudgetProgressEntity? get selectedBudgetProgress => + throw _privateConstructorUsedError; + BudgetTransactionsResponse? get selectedBudgetTransactions => + throw _privateConstructorUsedError; + List get selectedBudgetTargets => + throw _privateConstructorUsedError; + List get selectedBudgetPeriodStates => + throw _privateConstructorUsedError; + Failure get failure => throw _privateConstructorUsedError; + + /// Create a copy of BudgetState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BudgetStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BudgetStateCopyWith<$Res> { + factory $BudgetStateCopyWith( + BudgetState value, $Res Function(BudgetState) then) = + _$BudgetStateCopyWithImpl<$Res, BudgetState>; + @useResult + $Res call( + {List budgets, + bool isLoading, + bool isSaving, + bool isDeleting, + bool isProgressLoading, + bool isPeriodTransactionsLoading, + bool isClosingPeriod, + BudgetProgressEntity? selectedBudgetProgress, + BudgetTransactionsResponse? selectedBudgetTransactions, + List selectedBudgetTargets, + List selectedBudgetPeriodStates, + Failure failure}); + + $BudgetProgressEntityCopyWith<$Res>? get selectedBudgetProgress; + $FailureCopyWith<$Res> get failure; +} + +/// @nodoc +class _$BudgetStateCopyWithImpl<$Res, $Val extends BudgetState> + implements $BudgetStateCopyWith<$Res> { + _$BudgetStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BudgetState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? budgets = null, + Object? isLoading = null, + Object? isSaving = null, + Object? isDeleting = null, + Object? isProgressLoading = null, + Object? isPeriodTransactionsLoading = null, + Object? isClosingPeriod = null, + Object? selectedBudgetProgress = freezed, + Object? selectedBudgetTransactions = freezed, + Object? selectedBudgetTargets = null, + Object? selectedBudgetPeriodStates = null, + Object? failure = null, + }) { + return _then(_value.copyWith( + budgets: null == budgets + ? _value.budgets + : budgets // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + isSaving: null == isSaving + ? _value.isSaving + : isSaving // ignore: cast_nullable_to_non_nullable + as bool, + isDeleting: null == isDeleting + ? _value.isDeleting + : isDeleting // ignore: cast_nullable_to_non_nullable + as bool, + isProgressLoading: null == isProgressLoading + ? _value.isProgressLoading + : isProgressLoading // ignore: cast_nullable_to_non_nullable + as bool, + isPeriodTransactionsLoading: null == isPeriodTransactionsLoading + ? _value.isPeriodTransactionsLoading + : isPeriodTransactionsLoading // ignore: cast_nullable_to_non_nullable + as bool, + isClosingPeriod: null == isClosingPeriod + ? _value.isClosingPeriod + : isClosingPeriod // ignore: cast_nullable_to_non_nullable + as bool, + selectedBudgetProgress: freezed == selectedBudgetProgress + ? _value.selectedBudgetProgress + : selectedBudgetProgress // ignore: cast_nullable_to_non_nullable + as BudgetProgressEntity?, + selectedBudgetTransactions: freezed == selectedBudgetTransactions + ? _value.selectedBudgetTransactions + : selectedBudgetTransactions // ignore: cast_nullable_to_non_nullable + as BudgetTransactionsResponse?, + selectedBudgetTargets: null == selectedBudgetTargets + ? _value.selectedBudgetTargets + : selectedBudgetTargets // ignore: cast_nullable_to_non_nullable + as List, + selectedBudgetPeriodStates: null == selectedBudgetPeriodStates + ? _value.selectedBudgetPeriodStates + : selectedBudgetPeriodStates // ignore: cast_nullable_to_non_nullable + as List, + failure: null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure, + ) as $Val); + } + + /// Create a copy of BudgetState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $BudgetProgressEntityCopyWith<$Res>? get selectedBudgetProgress { + if (_value.selectedBudgetProgress == null) { + return null; + } + + return $BudgetProgressEntityCopyWith<$Res>(_value.selectedBudgetProgress!, + (value) { + return _then(_value.copyWith(selectedBudgetProgress: value) as $Val); + }); + } + + /// Create a copy of BudgetState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $FailureCopyWith<$Res> get failure { + return $FailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$BudgetStateImplCopyWith<$Res> + implements $BudgetStateCopyWith<$Res> { + factory _$$BudgetStateImplCopyWith( + _$BudgetStateImpl value, $Res Function(_$BudgetStateImpl) then) = + __$$BudgetStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List budgets, + bool isLoading, + bool isSaving, + bool isDeleting, + bool isProgressLoading, + bool isPeriodTransactionsLoading, + bool isClosingPeriod, + BudgetProgressEntity? selectedBudgetProgress, + BudgetTransactionsResponse? selectedBudgetTransactions, + List selectedBudgetTargets, + List selectedBudgetPeriodStates, + Failure failure}); + + @override + $BudgetProgressEntityCopyWith<$Res>? get selectedBudgetProgress; + @override + $FailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$BudgetStateImplCopyWithImpl<$Res> + extends _$BudgetStateCopyWithImpl<$Res, _$BudgetStateImpl> + implements _$$BudgetStateImplCopyWith<$Res> { + __$$BudgetStateImplCopyWithImpl( + _$BudgetStateImpl _value, $Res Function(_$BudgetStateImpl) _then) + : super(_value, _then); + + /// Create a copy of BudgetState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? budgets = null, + Object? isLoading = null, + Object? isSaving = null, + Object? isDeleting = null, + Object? isProgressLoading = null, + Object? isPeriodTransactionsLoading = null, + Object? isClosingPeriod = null, + Object? selectedBudgetProgress = freezed, + Object? selectedBudgetTransactions = freezed, + Object? selectedBudgetTargets = null, + Object? selectedBudgetPeriodStates = null, + Object? failure = null, + }) { + return _then(_$BudgetStateImpl( + budgets: null == budgets + ? _value._budgets + : budgets // ignore: cast_nullable_to_non_nullable + as List, + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + isSaving: null == isSaving + ? _value.isSaving + : isSaving // ignore: cast_nullable_to_non_nullable + as bool, + isDeleting: null == isDeleting + ? _value.isDeleting + : isDeleting // ignore: cast_nullable_to_non_nullable + as bool, + isProgressLoading: null == isProgressLoading + ? _value.isProgressLoading + : isProgressLoading // ignore: cast_nullable_to_non_nullable + as bool, + isPeriodTransactionsLoading: null == isPeriodTransactionsLoading + ? _value.isPeriodTransactionsLoading + : isPeriodTransactionsLoading // ignore: cast_nullable_to_non_nullable + as bool, + isClosingPeriod: null == isClosingPeriod + ? _value.isClosingPeriod + : isClosingPeriod // ignore: cast_nullable_to_non_nullable + as bool, + selectedBudgetProgress: freezed == selectedBudgetProgress + ? _value.selectedBudgetProgress + : selectedBudgetProgress // ignore: cast_nullable_to_non_nullable + as BudgetProgressEntity?, + selectedBudgetTransactions: freezed == selectedBudgetTransactions + ? _value.selectedBudgetTransactions + : selectedBudgetTransactions // ignore: cast_nullable_to_non_nullable + as BudgetTransactionsResponse?, + selectedBudgetTargets: null == selectedBudgetTargets + ? _value._selectedBudgetTargets + : selectedBudgetTargets // ignore: cast_nullable_to_non_nullable + as List, + selectedBudgetPeriodStates: null == selectedBudgetPeriodStates + ? _value._selectedBudgetPeriodStates + : selectedBudgetPeriodStates // ignore: cast_nullable_to_non_nullable + as List, + failure: null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure, + )); + } +} + +/// @nodoc + +class _$BudgetStateImpl implements _BudgetState { + const _$BudgetStateImpl( + {required final List budgets, + required this.isLoading, + required this.isSaving, + required this.isDeleting, + required this.isProgressLoading, + required this.isPeriodTransactionsLoading, + required this.isClosingPeriod, + this.selectedBudgetProgress, + this.selectedBudgetTransactions, + required final List selectedBudgetTargets, + required final List selectedBudgetPeriodStates, + required this.failure}) + : _budgets = budgets, + _selectedBudgetTargets = selectedBudgetTargets, + _selectedBudgetPeriodStates = selectedBudgetPeriodStates; + + final List _budgets; + @override + List get budgets { + if (_budgets is EqualUnmodifiableListView) return _budgets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_budgets); + } + + @override + final bool isLoading; + @override + final bool isSaving; + @override + final bool isDeleting; + @override + final bool isProgressLoading; + @override + final bool isPeriodTransactionsLoading; + @override + final bool isClosingPeriod; + @override + final BudgetProgressEntity? selectedBudgetProgress; + @override + final BudgetTransactionsResponse? selectedBudgetTransactions; + final List _selectedBudgetTargets; + @override + List get selectedBudgetTargets { + if (_selectedBudgetTargets is EqualUnmodifiableListView) + return _selectedBudgetTargets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_selectedBudgetTargets); + } + + final List _selectedBudgetPeriodStates; + @override + List get selectedBudgetPeriodStates { + if (_selectedBudgetPeriodStates is EqualUnmodifiableListView) + return _selectedBudgetPeriodStates; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_selectedBudgetPeriodStates); + } + + @override + final Failure failure; + + @override + String toString() { + return 'BudgetState(budgets: $budgets, isLoading: $isLoading, isSaving: $isSaving, isDeleting: $isDeleting, isProgressLoading: $isProgressLoading, isPeriodTransactionsLoading: $isPeriodTransactionsLoading, isClosingPeriod: $isClosingPeriod, selectedBudgetProgress: $selectedBudgetProgress, selectedBudgetTransactions: $selectedBudgetTransactions, selectedBudgetTargets: $selectedBudgetTargets, selectedBudgetPeriodStates: $selectedBudgetPeriodStates, failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BudgetStateImpl && + const DeepCollectionEquality().equals(other._budgets, _budgets) && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.isSaving, isSaving) || + other.isSaving == isSaving) && + (identical(other.isDeleting, isDeleting) || + other.isDeleting == isDeleting) && + (identical(other.isProgressLoading, isProgressLoading) || + other.isProgressLoading == isProgressLoading) && + (identical(other.isPeriodTransactionsLoading, + isPeriodTransactionsLoading) || + other.isPeriodTransactionsLoading == + isPeriodTransactionsLoading) && + (identical(other.isClosingPeriod, isClosingPeriod) || + other.isClosingPeriod == isClosingPeriod) && + (identical(other.selectedBudgetProgress, selectedBudgetProgress) || + other.selectedBudgetProgress == selectedBudgetProgress) && + (identical(other.selectedBudgetTransactions, + selectedBudgetTransactions) || + other.selectedBudgetTransactions == + selectedBudgetTransactions) && + const DeepCollectionEquality() + .equals(other._selectedBudgetTargets, _selectedBudgetTargets) && + const DeepCollectionEquality().equals( + other._selectedBudgetPeriodStates, + _selectedBudgetPeriodStates) && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_budgets), + isLoading, + isSaving, + isDeleting, + isProgressLoading, + isPeriodTransactionsLoading, + isClosingPeriod, + selectedBudgetProgress, + selectedBudgetTransactions, + const DeepCollectionEquality().hash(_selectedBudgetTargets), + const DeepCollectionEquality().hash(_selectedBudgetPeriodStates), + failure); + + /// Create a copy of BudgetState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BudgetStateImplCopyWith<_$BudgetStateImpl> get copyWith => + __$$BudgetStateImplCopyWithImpl<_$BudgetStateImpl>(this, _$identity); +} + +abstract class _BudgetState implements BudgetState { + const factory _BudgetState( + {required final List budgets, + required final bool isLoading, + required final bool isSaving, + required final bool isDeleting, + required final bool isProgressLoading, + required final bool isPeriodTransactionsLoading, + required final bool isClosingPeriod, + final BudgetProgressEntity? selectedBudgetProgress, + final BudgetTransactionsResponse? selectedBudgetTransactions, + required final List selectedBudgetTargets, + required final List selectedBudgetPeriodStates, + required final Failure failure}) = _$BudgetStateImpl; + + @override + List get budgets; + @override + bool get isLoading; + @override + bool get isSaving; + @override + bool get isDeleting; + @override + bool get isProgressLoading; + @override + bool get isPeriodTransactionsLoading; + @override + bool get isClosingPeriod; + @override + BudgetProgressEntity? get selectedBudgetProgress; + @override + BudgetTransactionsResponse? get selectedBudgetTransactions; + @override + List get selectedBudgetTargets; + @override + List get selectedBudgetPeriodStates; + @override + Failure get failure; + + /// Create a copy of BudgetState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BudgetStateImplCopyWith<_$BudgetStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/budget/cubit/budget_state.dart b/lib/presentation/budget/cubit/budget_state.dart new file mode 100644 index 00000000..283355da --- /dev/null +++ b/lib/presentation/budget/cubit/budget_state.dart @@ -0,0 +1,32 @@ +part of 'budget_cubit.dart'; + +@freezed +class BudgetState with _$BudgetState { + const factory BudgetState({ + required List budgets, + required bool isLoading, + required bool isSaving, + required bool isDeleting, + required bool isProgressLoading, + required bool isPeriodTransactionsLoading, + required bool isClosingPeriod, + BudgetProgressEntity? selectedBudgetProgress, + BudgetTransactionsResponse? selectedBudgetTransactions, + required List selectedBudgetTargets, + required List selectedBudgetPeriodStates, + required Failure failure, + }) = _BudgetState; + + factory BudgetState.initial() => const BudgetState( + budgets: [], + isLoading: false, + isSaving: false, + isDeleting: false, + isProgressLoading: false, + isPeriodTransactionsLoading: false, + isClosingPeriod: false, + selectedBudgetTargets: [], + selectedBudgetPeriodStates: [], + failure: Failure.none(), + ); +} diff --git a/lib/presentation/utils/custom_drawer.dart b/lib/presentation/utils/custom_drawer.dart index 2a465f42..0c894c62 100644 --- a/lib/presentation/utils/custom_drawer.dart +++ b/lib/presentation/utils/custom_drawer.dart @@ -7,6 +7,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:trakli/di/injection.dart'; import 'package:trakli/gen/assets.gen.dart'; import 'package:trakli/presentation/auth/cubits/auth/auth_cubit.dart'; +import 'package:trakli/presentation/budget/budget_screen.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/category/category_screen.dart'; import 'package:trakli/presentation/config/cubit/config_cubit.dart'; @@ -192,6 +193,41 @@ class CustomDrawer extends StatelessWidget { iconPath: Assets.images.people, subtitle: LocaleKeys.groupsDesc.tr(), ), + InkWell( + onTap: () => AppNavigator.push( + context, + const BudgetScreen(), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 8.h, + ), + child: Row( + children: [ + Icon( + Icons.savings_outlined, + size: 20.sp, + color: + Theme.of(context).colorScheme.onSurface, + ), + SizedBox(width: 14.w), + Expanded( + child: Text( + 'Budgets', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + ), + ), + ], + ), + ), + ), InkWell( onTap: () => AppNavigator.push( context, diff --git a/lib/presentation/utils/enums.dart b/lib/presentation/utils/enums.dart index 2ab1a66d..d3046432 100644 --- a/lib/presentation/utils/enums.dart +++ b/lib/presentation/utils/enums.dart @@ -212,3 +212,89 @@ enum NotificationType { }; } } + +enum BudgetPeriodType { + @JsonValue('weekly') + weekly, + @JsonValue('monthly') + monthly, + @JsonValue('yearly') + yearly, + @JsonValue('custom') + custom; + + String get serverKey { + return switch (this) { + BudgetPeriodType.weekly => 'weekly', + BudgetPeriodType.monthly => 'monthly', + BudgetPeriodType.yearly => 'yearly', + BudgetPeriodType.custom => 'custom', + }; + } + + static BudgetPeriodType? tryParse(String? value) { + return switch (value?.trim().toLowerCase()) { + 'weekly' => BudgetPeriodType.weekly, + 'monthly' => BudgetPeriodType.monthly, + 'yearly' => BudgetPeriodType.yearly, + 'custom' => BudgetPeriodType.custom, + _ => null, + }; + } +} + +enum BudgetTargetType { + @JsonValue('category') + category, + @JsonValue('group') + group, + @JsonValue('wallet') + wallet; + + String get serverKey { + return switch (this) { + BudgetTargetType.category => 'category', + BudgetTargetType.group => 'group', + BudgetTargetType.wallet => 'wallet', + }; + } + + static BudgetTargetType? tryParse(String? value) { + return switch (value?.trim().toLowerCase()) { + 'category' => BudgetTargetType.category, + 'group' => BudgetTargetType.group, + 'wallet' => BudgetTargetType.wallet, + _ => null, + }; + } +} + +enum BudgetStatus { + @JsonValue('on_track') + onTrack, + @JsonValue('near_limit') + nearLimit, + @JsonValue('over_budget') + overBudget, + @JsonValue('forecast_breach') + forecastBreach; + + String get serverKey { + return switch (this) { + BudgetStatus.onTrack => 'on_track', + BudgetStatus.nearLimit => 'near_limit', + BudgetStatus.overBudget => 'over_budget', + BudgetStatus.forecastBreach => 'forecast_breach', + }; + } + + static BudgetStatus? tryParse(String? value) { + return switch (value?.trim().toLowerCase()) { + 'on_track' => BudgetStatus.onTrack, + 'near_limit' => BudgetStatus.nearLimit, + 'over_budget' => BudgetStatus.overBudget, + 'forecast_breach' => BudgetStatus.forecastBreach, + _ => null, + }; + } +} From c9dc643d8dae0d9dcec6f9aa4bde6d9c407c44ac Mon Sep 17 00:00:00 2001 From: Fuh Austin Date: Wed, 27 May 2026 00:16:20 +0100 Subject: [PATCH 18/21] refactor(i18n): Extract 39 hardcoded strings to localization - Updated all language translation files - Fixed critical bug: 'Budgets' menu item now properly localized - Update the budget screens to use proper bloc and cubit patterns - Update budgets screens to use default custom snack bar --- assets/translations/de.json | 160 ++- assets/translations/en.json | 111 +- assets/translations/es.json | 160 ++- assets/translations/fr.json | 111 +- assets/translations/it.json | 160 ++- assets/translations/ru.json | 160 ++- doc/bloc_cubit_patterns.md | 1025 +++++++++++++++++ lib/gen/translations/codegen_loader.g.dart | 160 ++- lib/gen/translations/locale_keys.g.dart | 113 ++ .../lib/gen/translations/locale_keys.g.dart | 688 +++++++++++ .../budget/add_budget_screen.dart | 117 +- .../budget/budget_detail_screen.dart | 278 ++--- lib/presentation/budget/budget_screen.dart | 50 +- lib/presentation/utils/custom_drawer.dart | 29 +- lib/presentation/utils/dialogs.dart | 52 + lib/presentation/utils/helpers.dart | 9 +- pubspec.lock | 10 +- 17 files changed, 3121 insertions(+), 272 deletions(-) create mode 100644 doc/bloc_cubit_patterns.md create mode 100644 lib/generated/lib/gen/translations/locale_keys.g.dart diff --git a/assets/translations/de.json b/assets/translations/de.json index 409a37f3..2098563c 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -582,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 2a026767..ff00389e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -631,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 dbfe1e4a..d280e9c8 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -582,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 74e51986..457b76e6 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -631,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 2c9e6f54..4a6c207f 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -582,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 4309a89b..3bef411b 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -582,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/lib/gen/translations/codegen_loader.g.dart b/lib/gen/translations/codegen_loader.g.dart index 3e2524f5..35e0bdd2 100644 --- a/lib/gen/translations/codegen_loader.g.dart +++ b/lib/gen/translations/codegen_loader.g.dart @@ -46,15 +46,6 @@ abstract class LocaleKeys { static const parties = 'parties'; static const partyAddParty = 'partyAddParty'; static const partyCreateParty = 'partyCreateParty'; - static const partyEditParty = 'partyEditParty'; - static const partyName = 'partyName'; - static const partyNameRequired = 'partyNameRequired'; - static const partyDescription = 'partyDescription'; - static const partyUpdate = 'partyUpdate'; - static const partyAdd = 'partyAdd'; - static const partyDeleteParty = 'partyDeleteParty'; - static const partyDeletePartyConfirm = 'partyDeletePartyConfirm'; - static const partyNoParties = 'partyNoParties'; static const partyPartyName = 'partyPartyName'; static const partyEnterPartyName = 'partyEnterPartyName'; static const partyPartyDescription = 'partyPartyDescription'; @@ -125,7 +116,6 @@ abstract class LocaleKeys { static const selectLanguage = 'selectLanguage'; static const seeAll = 'seeAll'; static const phoneNumber = 'phoneNumber'; - static const phoneNumberHint = 'phoneNumberHint'; static const payment = 'payment'; static const notifications = 'notifications'; static const langEnglish = 'langEnglish'; @@ -161,6 +151,15 @@ abstract class LocaleKeys { static const selectCurrency = 'selectCurrency'; static const defaultWalletName = 'defaultWalletName'; static const defaultWalletDescription = 'defaultWalletDescription'; + static const partyEditParty = 'partyEditParty'; + static const partyName = 'partyName'; + static const partyNameRequired = 'partyNameRequired'; + static const partyDescription = 'partyDescription'; + static const partyUpdate = 'partyUpdate'; + static const partyAdd = 'partyAdd'; + static const partyDeleteParty = 'partyDeleteParty'; + static const partyDeletePartyConfirm = 'partyDeletePartyConfirm'; + static const partyNoParties = 'partyNoParties'; static const groupUpdate = 'groupUpdate'; static const defaultGroupName = 'defaultGroupName'; static const accountInfo = 'accountInfo'; @@ -176,6 +175,7 @@ abstract class LocaleKeys { static const toExcel = 'toExcel'; static const family = 'family'; static const searchHint = 'searchHint'; + static const totalIncome = 'totalIncome'; static const displaySettings = 'displaySettings'; static const transactionFormDisplayMode = 'transactionFormDisplayMode'; static const themeMode = 'themeMode'; @@ -189,7 +189,6 @@ abstract class LocaleKeys { static const anonymous = 'anonymous'; static const logOut = 'logOut'; static const logoutConfirm = 'logoutConfirm'; - static const alreadyHaveAccount = 'alreadyHaveAccount'; static const dontHaveAccount = 'dontHaveAccount'; static const benefitsAccount = 'benefitsAccount'; static const createAccountNow = 'createAccountNow'; @@ -227,6 +226,10 @@ abstract class LocaleKeys { static const to = 'to'; static const party = 'party'; static const category = 'category'; + static const description = 'description'; + static const attachment = 'attachment'; + static const date = 'date'; + static const time = 'time'; static const editCategory = 'editCategory'; static const addCategory = 'addCategory'; static const noCategoriesFound = 'noCategoriesFound'; @@ -235,14 +238,11 @@ abstract class LocaleKeys { static const addWallet = 'addWallet'; static const name = 'name'; static const nameIsRequired = 'nameIsRequired'; - static const description = 'description'; - static const attachment = 'attachment'; - static const date = 'date'; - static const time = 'time'; static const createSaving = 'createSaving'; static const pleaseSelectCurrency = 'pleaseSelectCurrency'; static const confirm = 'confirm'; static const categories = 'categories'; + static const edit = 'edit'; static const savings = 'savings'; static const addSaving = 'addSaving'; static const snapPicture = 'snapPicture'; @@ -303,6 +303,7 @@ abstract class LocaleKeys { static const selectCategory = 'selectCategory'; static const categoryIsRequired = 'categoryIsRequired'; static const pleaseSelectWallet = 'pleaseSelectWallet'; + static const phoneNumberHint = 'phoneNumberHint'; static const noWalletsYet = 'noWalletsYet'; static const deleteCategory = 'deleteCategory'; static const deleteCategoryConfirm = 'deleteCategoryConfirm'; @@ -310,8 +311,6 @@ abstract class LocaleKeys { static const transactionsIn = 'transactionsIn'; static const wallets = 'wallets'; static const noData = 'noData'; - static const totalIncome = 'totalIncome'; - static const totalExpense = 'totalExpense'; static const thisMonth = 'thisMonth'; static const thisWeek = 'thisWeek'; static const lastThreeMonths = 'lastThreeMonths'; @@ -326,7 +325,6 @@ abstract class LocaleKeys { static const categoryNameAlreadyExists = 'categoryNameAlreadyExists'; static const deleteWallet = 'deleteWallet'; static const deleteWalletConfirm = 'deleteWalletConfirm'; - static const edit = 'edit'; static const defaultName = 'defaultName'; static const officeElements = 'officeElements'; static const officeElementsDesc = 'officeElementsDesc'; @@ -459,15 +457,6 @@ abstract class LocaleKeys { static const setupWalletTitle = 'setupWalletTitle'; static const setupWalletDesc = 'setupWalletDesc'; static const walletSetup = 'walletSetup'; - static const setupGroupTitle = 'setupGroupTitle'; - static const setupGroupDesc = 'setupGroupDesc'; - static const groupSetup = 'groupSetup'; - static const setupCategoryTitle = 'setupCategoryTitle'; - static const setupCategoryDesc = 'setupCategoryDesc'; - static const createDefaultCategories = 'createDefaultCategories'; - static const createDefaultCategoriesDesc = 'createDefaultCategoriesDesc'; - static const skipCategories = 'skipCategories'; - static const skipCategoriesDesc = 'skipCategoriesDesc'; static const useDefaultWallet = 'useDefaultWallet'; static const renameDefaultWallet = 'renameDefaultWallet'; static const createNewWallet = 'createNewWallet'; @@ -478,6 +467,17 @@ abstract class LocaleKeys { static const createManually = 'createManually'; static const selectFromWalletList = 'selectFromWalletList'; static const selectFromGroupList = 'selectFromGroupList'; + static const groupSetup = 'groupSetup'; + static const setupGroupTitle = 'setupGroupTitle'; + static const setupGroupDesc = 'setupGroupDesc'; + static const setupCategoryTitle = 'setupCategoryTitle'; + static const setupCategoryDesc = 'setupCategoryDesc'; + static const createDefaultCategories = 'createDefaultCategories'; + static const createDefaultCategoriesDesc = 'createDefaultCategoriesDesc'; + static const skipCategories = 'skipCategories'; + static const skipCategoriesDesc = 'skipCategoriesDesc'; + static const alreadyHaveAccount = 'alreadyHaveAccount'; + static const totalExpense = 'totalExpense'; static const advanced = 'advanced'; static const synchronization = 'synchronization'; static const syncHistoryDesc = 'syncHistoryDesc'; @@ -560,6 +560,9 @@ abstract class LocaleKeys { static const files = 'files'; static const noImages = 'noImages'; static const noFiles = 'noFiles'; + static const orphanedMediaCleanupLog = 'orphanedMediaCleanupLog'; + static const orphanedMediaCleanupLogDesc = 'orphanedMediaCleanupLogDesc'; + static const orphanedMediaCleanupLogEmpty = 'orphanedMediaCleanupLogEmpty'; static const appUpdateGooglePlay = 'appUpdateGooglePlay'; static const appUpdateAppStore = 'appUpdateAppStore'; static const appUpdateNewVersionAvailable = 'appUpdateNewVersionAvailable'; @@ -572,9 +575,6 @@ abstract class LocaleKeys { static const appUpdateRestart = 'appUpdateRestart'; static const appUpdateReady = 'appUpdateReady'; static const appUpdateRestartPrompt = 'appUpdateRestartPrompt'; - static const orphanedMediaCleanupLog = 'orphanedMediaCleanupLog'; - static const orphanedMediaCleanupLogDesc = 'orphanedMediaCleanupLogDesc'; - static const orphanedMediaCleanupLogEmpty = 'orphanedMediaCleanupLogEmpty'; static const deleteYourAccount = 'deleteYourAccount'; static const deleteAccount = 'deleteAccount'; static const deleteAccountDesc = 'deleteAccountDesc'; @@ -636,5 +636,105 @@ abstract class LocaleKeys { static const importCreateTransactions = 'importCreateTransactions'; static const importAutoCreateMissingHint = 'importAutoCreateMissingHint'; static const importAutoCreateNoneNeeded = 'importAutoCreateNoneNeeded'; + static const budget = 'budget'; + static const budgets = 'budgets'; + static const addBudget = 'addBudget'; + static const createBudget = 'createBudget'; + static const editBudget = 'editBudget'; + static const deleteBudget = 'deleteBudget'; + static const deleteBudgetConfirm = 'deleteBudgetConfirm'; + static const deleteBudgetSuccess = 'deleteBudgetSuccess'; + static const deleteBudgetError = 'deleteBudgetError'; + static const closeBudgetPeriod = 'closeBudgetPeriod'; + static const closePeriod = 'closePeriod'; + static const closePeriodConfirm = 'closePeriodConfirm'; + static const closePeriodSuccess = 'closePeriodSuccess'; + static const closePeriodError = 'closePeriodError'; + static const budgetAmount = 'budgetAmount'; + static const budgetPeriod = 'budgetPeriod'; + static const budgetTargets = 'budgetTargets'; + static const budgetPeriodHistory = 'budgetPeriodHistory'; + static const budgetStatus = 'budgetStatus'; + static const budgetRemaining = 'budgetRemaining'; + static const budgetProjected = 'budgetProjected'; + static const budgetRefunds = 'budgetRefunds'; + static const budgetRollover = 'budgetRollover'; + static const budgetRolloverIn = 'budgetRolloverIn'; + static const budgetPeriodOpen = 'budgetPeriodOpen'; + static const closePeriodMessage = 'closePeriodMessage'; + static const closePeriodTitle = 'closePeriodTitle'; + static const budgetStatusOverBudget = 'budgetStatusOverBudget'; + static const budgetStatusForecastBreach = 'budgetStatusForecastBreach'; + static const budgetStatusNearLimit = 'budgetStatusNearLimit'; + static const budgetStatusOnTrack = 'budgetStatusOnTrack'; + static const budgetStatusAwaitingSync = 'budgetStatusAwaitingSync'; + static const budgetPeriodWeekly = 'budgetPeriodWeekly'; + static const budgetPeriodMonthly = 'budgetPeriodMonthly'; + static const budgetPeriodYearly = 'budgetPeriodYearly'; + static const budgetKpiRemaining = 'budgetKpiRemaining'; + static const budgetKpiProjected = 'budgetKpiProjected'; + static const budgetKpiRefunds = 'budgetKpiRefunds'; + static const budgetKpiRolloverIn = 'budgetKpiRolloverIn'; + static const budgetTargetCategory = 'budgetTargetCategory'; + static const budgetTargetWallet = 'budgetTargetWallet'; + static const budgetTargetGroup = 'budgetTargetGroup'; + static const budgetTargetsLabel = 'budgetTargetsLabel'; + static const budgetPeriodHistoryLabel = 'budgetPeriodHistoryLabel'; + static const budgetTargetsApplyAll = 'budgetTargetsApplyAll'; + static const budgetNoPeriods = 'budgetNoPeriods'; + static const budgetNoProgress = 'budgetNoProgress'; + static const budgetTargetUnnamed = 'budgetTargetUnnamed'; + static const newBudget = 'newBudget'; + static const period = 'period'; + static const startDate = 'startDate'; + static const endDate = 'endDate'; + static const target = 'target'; + static const targets = 'targets'; + static const selected = 'selected'; + static const budgetRolloverDescription = 'budgetRolloverDescription'; + static const budgetForecastAlerts = 'budgetForecastAlerts'; + static const budgetForecastAlertsDescription = 'budgetForecastAlertsDescription'; + static const active = 'active'; + static const saveChanges = 'saveChanges'; + static const budgetNameRequired = 'budgetNameRequired'; + static const budgetAmountRequired = 'budgetAmountRequired'; + static const budgetCurrencyRequired = 'budgetCurrencyRequired'; + static const nameHint = 'nameHint'; + static const periodWeekly = 'periodWeekly'; + static const periodMonthly = 'periodMonthly'; + static const periodYearly = 'periodYearly'; + static const periodCustom = 'periodCustom'; + static const selectDate = 'selectDate'; + static const selectEndDate = 'selectEndDate'; + static const alertAtThreshold = 'alertAtThreshold'; + static const selectTargets = 'selectTargets'; + static const noCategoriesYet = 'noCategoriesYet'; + static const noGroupsYet = 'noGroupsYet'; + static const budgetPageTitle = 'budgetPageTitle'; + static const searchBudgets = 'searchBudgets'; + static const filterAll = 'filterAll'; + static const filterActive = 'filterActive'; + static const periodWeeklyShort = 'periodWeeklyShort'; + static const periodMonthlyShort = 'periodMonthlyShort'; + static const periodYearlyShort = 'periodYearlyShort'; + static const scopeAllTransactions = 'scopeAllTransactions'; + static const targetTypeCategorySingular = 'targetTypeCategorySingular'; + static const targetTypeWalletSingular = 'targetTypeWalletSingular'; + static const targetTypeGroupSingular = 'targetTypeGroupSingular'; + static const statusInactive = 'statusInactive'; + static const noBudgetsYet = 'noBudgetsYet'; + static const noBudgetsDescription = 'noBudgetsDescription'; + static const noBudgetsMatchFilter = 'noBudgetsMatchFilter'; + static const noMatchesForQuery = 'noMatchesForQuery'; + static const budgetSuffix = 'budgetSuffix'; + static const drawerBudgets = 'drawerBudgets'; + static const drawerEveryday = 'drawerEveryday'; + static const drawerOrganize = 'drawerOrganize'; + static const drawerMore = 'drawerMore'; + static const supportEmail = 'supportEmail'; + static const supportEmailSubject = 'supportEmailSubject'; + static const supportEmailCopied = 'supportEmailCopied'; + static const defaultUserName = 'defaultUserName'; + static const appTitle = 'appTitle'; } diff --git a/lib/gen/translations/locale_keys.g.dart b/lib/gen/translations/locale_keys.g.dart index 0900c36c..35e0bdd2 100644 --- a/lib/gen/translations/locale_keys.g.dart +++ b/lib/gen/translations/locale_keys.g.dart @@ -97,6 +97,18 @@ abstract class LocaleKeys { static const groupCreateGroup = 'groupCreateGroup'; static const statistics = 'statistics'; static const profile = 'profile'; + static const ai = 'ai'; + static const aiChatTitle = 'aiChatTitle'; + static const aiChatEmptyHint = 'aiChatEmptyHint'; + static const aiChatComposerPlaceholder = 'aiChatComposerPlaceholder'; + static const aiChatNewChat = 'aiChatNewChat'; + static const aiChatThinking = 'aiChatThinking'; + static const aiChatError = 'aiChatError'; + static const aiChatHistory = 'aiChatHistory'; + static const aiChatUntitled = 'aiChatUntitled'; + static const aiChatNoSessions = 'aiChatNoSessions'; + static const aiChatNoSessionsHint = 'aiChatNoSessionsHint'; + static const aiChatDeleteConfirm = 'aiChatDeleteConfirm'; static const settings = 'settings'; static const typeHere = 'typeHere'; static const balanceAmountWithCurrency = 'balanceAmountWithCurrency'; @@ -191,6 +203,7 @@ abstract class LocaleKeys { static const about = 'about'; static const switchDefaultGroup = 'switchDefaultGroup'; static const walletTransfer = 'walletTransfer'; + static const transfer = 'transfer'; static const walletTransferDefaultDescription = 'walletTransferDefaultDescription'; static const sourceWallet = 'sourceWallet'; static const orangeMoney = 'orangeMoney'; @@ -623,5 +636,105 @@ abstract class LocaleKeys { static const importCreateTransactions = 'importCreateTransactions'; static const importAutoCreateMissingHint = 'importAutoCreateMissingHint'; static const importAutoCreateNoneNeeded = 'importAutoCreateNoneNeeded'; + static const budget = 'budget'; + static const budgets = 'budgets'; + static const addBudget = 'addBudget'; + static const createBudget = 'createBudget'; + static const editBudget = 'editBudget'; + static const deleteBudget = 'deleteBudget'; + static const deleteBudgetConfirm = 'deleteBudgetConfirm'; + static const deleteBudgetSuccess = 'deleteBudgetSuccess'; + static const deleteBudgetError = 'deleteBudgetError'; + static const closeBudgetPeriod = 'closeBudgetPeriod'; + static const closePeriod = 'closePeriod'; + static const closePeriodConfirm = 'closePeriodConfirm'; + static const closePeriodSuccess = 'closePeriodSuccess'; + static const closePeriodError = 'closePeriodError'; + static const budgetAmount = 'budgetAmount'; + static const budgetPeriod = 'budgetPeriod'; + static const budgetTargets = 'budgetTargets'; + static const budgetPeriodHistory = 'budgetPeriodHistory'; + static const budgetStatus = 'budgetStatus'; + static const budgetRemaining = 'budgetRemaining'; + static const budgetProjected = 'budgetProjected'; + static const budgetRefunds = 'budgetRefunds'; + static const budgetRollover = 'budgetRollover'; + static const budgetRolloverIn = 'budgetRolloverIn'; + static const budgetPeriodOpen = 'budgetPeriodOpen'; + static const closePeriodMessage = 'closePeriodMessage'; + static const closePeriodTitle = 'closePeriodTitle'; + static const budgetStatusOverBudget = 'budgetStatusOverBudget'; + static const budgetStatusForecastBreach = 'budgetStatusForecastBreach'; + static const budgetStatusNearLimit = 'budgetStatusNearLimit'; + static const budgetStatusOnTrack = 'budgetStatusOnTrack'; + static const budgetStatusAwaitingSync = 'budgetStatusAwaitingSync'; + static const budgetPeriodWeekly = 'budgetPeriodWeekly'; + static const budgetPeriodMonthly = 'budgetPeriodMonthly'; + static const budgetPeriodYearly = 'budgetPeriodYearly'; + static const budgetKpiRemaining = 'budgetKpiRemaining'; + static const budgetKpiProjected = 'budgetKpiProjected'; + static const budgetKpiRefunds = 'budgetKpiRefunds'; + static const budgetKpiRolloverIn = 'budgetKpiRolloverIn'; + static const budgetTargetCategory = 'budgetTargetCategory'; + static const budgetTargetWallet = 'budgetTargetWallet'; + static const budgetTargetGroup = 'budgetTargetGroup'; + static const budgetTargetsLabel = 'budgetTargetsLabel'; + static const budgetPeriodHistoryLabel = 'budgetPeriodHistoryLabel'; + static const budgetTargetsApplyAll = 'budgetTargetsApplyAll'; + static const budgetNoPeriods = 'budgetNoPeriods'; + static const budgetNoProgress = 'budgetNoProgress'; + static const budgetTargetUnnamed = 'budgetTargetUnnamed'; + static const newBudget = 'newBudget'; + static const period = 'period'; + static const startDate = 'startDate'; + static const endDate = 'endDate'; + static const target = 'target'; + static const targets = 'targets'; + static const selected = 'selected'; + static const budgetRolloverDescription = 'budgetRolloverDescription'; + static const budgetForecastAlerts = 'budgetForecastAlerts'; + static const budgetForecastAlertsDescription = 'budgetForecastAlertsDescription'; + static const active = 'active'; + static const saveChanges = 'saveChanges'; + static const budgetNameRequired = 'budgetNameRequired'; + static const budgetAmountRequired = 'budgetAmountRequired'; + static const budgetCurrencyRequired = 'budgetCurrencyRequired'; + static const nameHint = 'nameHint'; + static const periodWeekly = 'periodWeekly'; + static const periodMonthly = 'periodMonthly'; + static const periodYearly = 'periodYearly'; + static const periodCustom = 'periodCustom'; + static const selectDate = 'selectDate'; + static const selectEndDate = 'selectEndDate'; + static const alertAtThreshold = 'alertAtThreshold'; + static const selectTargets = 'selectTargets'; + static const noCategoriesYet = 'noCategoriesYet'; + static const noGroupsYet = 'noGroupsYet'; + static const budgetPageTitle = 'budgetPageTitle'; + static const searchBudgets = 'searchBudgets'; + static const filterAll = 'filterAll'; + static const filterActive = 'filterActive'; + static const periodWeeklyShort = 'periodWeeklyShort'; + static const periodMonthlyShort = 'periodMonthlyShort'; + static const periodYearlyShort = 'periodYearlyShort'; + static const scopeAllTransactions = 'scopeAllTransactions'; + static const targetTypeCategorySingular = 'targetTypeCategorySingular'; + static const targetTypeWalletSingular = 'targetTypeWalletSingular'; + static const targetTypeGroupSingular = 'targetTypeGroupSingular'; + static const statusInactive = 'statusInactive'; + static const noBudgetsYet = 'noBudgetsYet'; + static const noBudgetsDescription = 'noBudgetsDescription'; + static const noBudgetsMatchFilter = 'noBudgetsMatchFilter'; + static const noMatchesForQuery = 'noMatchesForQuery'; + static const budgetSuffix = 'budgetSuffix'; + static const drawerBudgets = 'drawerBudgets'; + static const drawerEveryday = 'drawerEveryday'; + static const drawerOrganize = 'drawerOrganize'; + static const drawerMore = 'drawerMore'; + static const supportEmail = 'supportEmail'; + static const supportEmailSubject = 'supportEmailSubject'; + static const supportEmailCopied = 'supportEmailCopied'; + static const defaultUserName = 'defaultUserName'; + static const appTitle = 'appTitle'; } diff --git a/lib/generated/lib/gen/translations/locale_keys.g.dart b/lib/generated/lib/gen/translations/locale_keys.g.dart new file mode 100644 index 00000000..08e2a7f4 --- /dev/null +++ b/lib/generated/lib/gen/translations/locale_keys.g.dart @@ -0,0 +1,688 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +// ignore_for_file: constant_identifier_names + +abstract class LocaleKeys { + static const appName = 'appName'; + static const welcomeText = 'welcomeText'; + static const welcomeTo = 'welcomeTo'; + static const homeWelcome = 'homeWelcome'; + static const onboardTitle1 = 'onboardTitle1'; + static const onboardDesc1 = 'onboardDesc1'; + static const onboardTitle2 = 'onboardTitle2'; + static const onboardDesc2 = 'onboardDesc2'; + static const onboardTitle3 = 'onboardTitle3'; + static const onboardDesc3 = 'onboardDesc3'; + static const skip = 'skip'; + static const login = 'login'; + static const loginGoogle = 'loginGoogle'; + static const loginApple = 'loginApple'; + static const signInSuccessful = 'signInSuccessful'; + static const go = 'go'; + static const createAccount = 'createAccount'; + static const loginDesc = 'loginDesc'; + static const email = 'email'; + static const password = 'password'; + static const forgotPassword = 'forgotPassword'; + static const reset = 'reset'; + static const retry = 'retry'; + static const noAccountYet = 'noAccountYet'; + static const otpDesc = 'otpDesc'; + static const continueT = 'continueT'; + static const otpCodeNotSent = 'otpCodeNotSent'; + static const emailEmptyDesc = 'emailEmptyDesc'; + static const emailValidDesc = 'emailValidDesc'; + static const passEmptyDesc = 'passEmptyDesc'; + static const otherR = 'otherR'; + static const home = 'home'; + static const history = 'history'; + static const wallet = 'wallet'; + static const transactionHistory = 'transactionHistory'; + static const addTransaction = 'addTransaction'; + static const addGroup = 'addGroup'; + static const myGroup = 'myGroup'; + static const homeDescText = 'homeDescText'; + static const groups = 'groups'; + static const parties = 'parties'; + static const partyAddParty = 'partyAddParty'; + static const partyCreateParty = 'partyCreateParty'; + static const partyPartyName = 'partyPartyName'; + static const partyEnterPartyName = 'partyEnterPartyName'; + static const partyPartyDescription = 'partyPartyDescription'; + static const transactionTracking = 'transactionTracking'; + static const support = 'support'; + static const transactions = 'transactions'; + static const transfers = 'transfers'; + static const transaction = 'transaction'; + static const moneySpent = 'moneySpent'; + static const moneyReceived = 'moneyReceived'; + static const totalCompanies = 'totalCompanies'; + static const transactionLast30Days = 'transactionLast30Days'; + static const transactionLast30DaysPerParties = 'transactionLast30DaysPerParties'; + static const totalExpenses = 'totalExpenses'; + static const totalSavings = 'totalSavings'; + static const totalSpent = 'totalSpent'; + static const trendingByMonth = 'trendingByMonth'; + static const showingTotalVisitors = 'showingTotalVisitors'; + static const days = 'days'; + static const months = 'months'; + static const fromDateToDate = 'fromDateToDate'; + static const transactionFilterBy = 'transactionFilterBy'; + static const transactionFrom = 'transactionFrom'; + static const transactionTo = 'transactionTo'; + static const transactionIncome = 'transactionIncome'; + static const transactionExpense = 'transactionExpense'; + static const transactionDate = 'transactionDate'; + static const transactionTime = 'transactionTime'; + static const transactionAmount = 'transactionAmount'; + static const transactionParty = 'transactionParty'; + static const transactionSentTo = 'transactionSentTo'; + static const transactionReceivedFrom = 'transactionReceivedFrom'; + static const transactionCategory = 'transactionCategory'; + static const transactionAttachment = 'transactionAttachment'; + static const transactionDescription = 'transactionDescription'; + static const transactionTypeHere = 'transactionTypeHere'; + static const transactionRecordIncome = 'transactionRecordIncome'; + static const transactionRecordExpenses = 'transactionRecordExpenses'; + static const transactionUploadHere = 'transactionUploadHere'; + static const transactionFileType = 'transactionFileType'; + static const transactionAmountHint = 'transactionAmountHint'; + static const exampleAmount = 'exampleAmount'; + static const maxSize = 'maxSize'; + static const groupsMyGroups = 'groupsMyGroups'; + static const groupGroupName = 'groupGroupName'; + static const groupEnterGroupName = 'groupEnterGroupName'; + static const groupGroupDescription = 'groupGroupDescription'; + static const groupAddGroup = 'groupAddGroup'; + static const groupCreateGroup = 'groupCreateGroup'; + static const statistics = 'statistics'; + static const profile = 'profile'; + static const ai = 'ai'; + static const aiChatTitle = 'aiChatTitle'; + static const aiChatEmptyHint = 'aiChatEmptyHint'; + static const aiChatComposerPlaceholder = 'aiChatComposerPlaceholder'; + static const aiChatNewChat = 'aiChatNewChat'; + static const aiChatThinking = 'aiChatThinking'; + static const aiChatError = 'aiChatError'; + static const aiChatHistory = 'aiChatHistory'; + static const aiChatUntitled = 'aiChatUntitled'; + static const aiChatNoSessions = 'aiChatNoSessions'; + static const aiChatNoSessionsHint = 'aiChatNoSessionsHint'; + static const aiChatDeleteConfirm = 'aiChatDeleteConfirm'; + static const settings = 'settings'; + static const typeHere = 'typeHere'; + static const balanceAmountWithCurrency = 'balanceAmountWithCurrency'; + static const totalBalance = 'totalBalance'; + static const selectLanguage = 'selectLanguage'; + static const seeAll = 'seeAll'; + static const phoneNumber = 'phoneNumber'; + static const payment = 'payment'; + static const notifications = 'notifications'; + static const langEnglish = 'langEnglish'; + static const langFrench = 'langFrench'; + static const langGerman = 'langGerman'; + static const langSpanish = 'langSpanish'; + static const langItalian = 'langItalian'; + static const langRussian = 'langRussian'; + static const firstName = 'firstName'; + static const lastName = 'lastName'; + static const username = 'username'; + static const confirmPassword = 'confirmPassword'; + static const register = 'register'; + static const firstNameEmptyDesc = 'firstNameEmptyDesc'; + static const invalidUserCredentials = 'invalidUserCredentials'; + static const editTransaction = 'editTransaction'; + static const deleteTransaction = 'deleteTransaction'; + static const deleteTransactionConfirmation = 'deleteTransactionConfirmation'; + static const invalidRequestDesc = 'invalidRequestDesc'; + static const noError = 'noError'; + static const internetConnectionDesc = 'internetConnectionDesc'; + static const notFoundDesc = 'notFoundDesc'; + static const serverErrorDesc = 'serverErrorDesc'; + static const cacheErrorDesc = 'cacheErrorDesc'; + static const syncErrorDesc = 'syncErrorDesc'; + static const unauthorizedErrorDesc = 'unauthorizedErrorDesc'; + static const unknownErrorDesc = 'unknownErrorDesc'; + static const cancel = 'cancel'; + static const delete = 'delete'; + static const next = 'next'; + static const prev = 'prev'; + static const done = 'done'; + static const selectCurrency = 'selectCurrency'; + static const defaultWalletName = 'defaultWalletName'; + static const defaultWalletDescription = 'defaultWalletDescription'; + static const partyEditParty = 'partyEditParty'; + static const partyName = 'partyName'; + static const partyNameRequired = 'partyNameRequired'; + static const partyDescription = 'partyDescription'; + static const partyUpdate = 'partyUpdate'; + static const partyAdd = 'partyAdd'; + static const partyDeleteParty = 'partyDeleteParty'; + static const partyDeletePartyConfirm = 'partyDeletePartyConfirm'; + static const partyNoParties = 'partyNoParties'; + static const groupUpdate = 'groupUpdate'; + static const defaultGroupName = 'defaultGroupName'; + static const accountInfo = 'accountInfo'; + static const editInfos = 'editInfos'; + static const country = 'country'; + static const cameroon = 'cameroon'; + static const notSet = 'notSet'; + static const group = 'group'; + static const noTransactionsFound = 'noTransactionsFound'; + static const noTransfersFound = 'noTransfersFound'; + static const export = 'export'; + static const toPdf = 'toPdf'; + static const toExcel = 'toExcel'; + static const family = 'family'; + static const searchHint = 'searchHint'; + static const totalIncome = 'totalIncome'; + static const displaySettings = 'displaySettings'; + static const transactionFormDisplayMode = 'transactionFormDisplayMode'; + static const themeMode = 'themeMode'; + static const selectFormDisplayMode = 'selectFormDisplayMode'; + static const selectThemeMode = 'selectThemeMode'; + static const light = 'light'; + static const dark = 'dark'; + static const system = 'system'; + static const synchronizing = 'synchronizing'; + static const otherScreen = 'otherScreen'; + static const anonymous = 'anonymous'; + static const logOut = 'logOut'; + static const logoutConfirm = 'logoutConfirm'; + static const dontHaveAccount = 'dontHaveAccount'; + static const benefitsAccount = 'benefitsAccount'; + static const createAccountNow = 'createAccountNow'; + static const pickWallet = 'pickWallet'; + static const selectWalletInfoDesc = 'selectWalletInfoDesc'; + static const allWallets = 'allWallets'; + static const display = 'display'; + static const defaultCurrency = 'defaultCurrency'; + static const defaults = 'defaults'; + static const defaultSettingsDesc = 'defaultSettingsDesc'; + static const defaultWallet = 'defaultWallet'; + static const about = 'about'; + static const switchDefaultGroup = 'switchDefaultGroup'; + static const walletTransfer = 'walletTransfer'; + static const transfer = 'transfer'; + static const walletTransferDefaultDescription = 'walletTransferDefaultDescription'; + static const sourceWallet = 'sourceWallet'; + static const orangeMoney = 'orangeMoney'; + static const orangeMoneyAmount = 'orangeMoneyAmount'; + static const destinationWallet = 'destinationWallet'; + static const bankAccountUba = 'bankAccountUba'; + static const bankAccountUbaAmount = 'bankAccountUbaAmount'; + static const amount = 'amount'; + static const amountHint = 'amountHint'; + static const amountRequired = 'amountRequired'; + static const mustBeNumber = 'mustBeNumber'; + static const amountNotZero = 'amountNotZero'; + static const exchangeRate = 'exchangeRate'; + static const exchangeRateHint = 'exchangeRateHint'; + static const valueRequired = 'valueRequired'; + static const transferMoney = 'transferMoney'; + static const databaseViewer = 'databaseViewer'; + static const unknown = 'unknown'; + static const from = 'from'; + static const to = 'to'; + static const party = 'party'; + static const category = 'category'; + static const description = 'description'; + static const attachment = 'attachment'; + static const date = 'date'; + static const time = 'time'; + static const editCategory = 'editCategory'; + static const addCategory = 'addCategory'; + static const noCategoriesFound = 'noCategoriesFound'; + static const noGroupsFound = 'noGroupsFound'; + static const editWallet = 'editWallet'; + static const addWallet = 'addWallet'; + static const name = 'name'; + static const nameIsRequired = 'nameIsRequired'; + static const createSaving = 'createSaving'; + static const pleaseSelectCurrency = 'pleaseSelectCurrency'; + static const confirm = 'confirm'; + static const categories = 'categories'; + static const edit = 'edit'; + static const savings = 'savings'; + static const addSaving = 'addSaving'; + static const snapPicture = 'snapPicture'; + static const uploadAttachment = 'uploadAttachment'; + static const amountIsRequired = 'amountIsRequired'; + static const amountMustNotBeZero = 'amountMustNotBeZero'; + static const walletIsRequired = 'walletIsRequired'; + static const updateIncome = 'updateIncome'; + static const updateExpenses = 'updateExpenses'; + static const fileSizeExceedsLimit = 'fileSizeExceedsLimit'; + static const couldNotLaunchUrl = 'couldNotLaunchUrl'; + static const couldNotLoadAttachment = 'couldNotLoadAttachment'; + static const couldNotLoadPdf = 'couldNotLoadPdf'; + static const fileNotFound = 'fileNotFound'; + static const noContent = 'noContent'; + static const previewNotAvailable = 'previewNotAvailable'; + static const cropper = 'cropper'; + static const aboutAppDescription = 'aboutAppDescription'; + static const privacyPolicy = 'privacyPolicy'; + static const termsAndConditions = 'termsAndConditions'; + static const upgradeToPremium = 'upgradeToPremium'; + static const pickGroup = 'pickGroup'; + static const pickGroupDesc = 'pickGroupDesc'; + static const search = 'search'; + static const share = 'share'; + static const selectIcon = 'selectIcon'; + static const selectPhoto = 'selectPhoto'; + static const useEmoji = 'useEmoji'; + static const camera = 'camera'; + static const gallery = 'gallery'; + static const selectEmoji = 'selectEmoji'; + static const searchEmoji = 'searchEmoji'; + static const selectWallet = 'selectWallet'; + static const selectSourceWallet = 'selectSourceWallet'; + static const selectDestinationWallet = 'selectDestinationWallet'; + static const noWalletsAvailable = 'noWalletsAvailable'; + static const cannotTransferToSameWallet = 'cannotTransferToSameWallet'; + static const insufficientBalance = 'insufficientBalance'; + static const transferFailed = 'transferFailed'; + static const transferSuccessful = 'transferSuccessful'; + static const convertingFrom = 'convertingFrom'; + static const processing = 'processing'; + static const useCurrentExchangeRate = 'useCurrentExchangeRate'; + static const destinationWalletWillReceive = 'destinationWalletWillReceive'; + static const transferFailedWithMessage = 'transferFailedWithMessage'; + static const addParty = 'addParty'; + static const categoryName = 'categoryName'; + static const updateCategory = 'updateCategory'; + static const createCategory = 'createCategory'; + static const enterName = 'enterName'; + static const balance = 'balance'; + static const type = 'type'; + static const selectWalletType = 'selectWalletType'; + static const updateWallet = 'updateWallet'; + static const createWallet = 'createWallet'; + static const phoneIsRequired = 'phoneIsRequired'; + static const save = 'save'; + static const selectCategory = 'selectCategory'; + static const categoryIsRequired = 'categoryIsRequired'; + static const pleaseSelectWallet = 'pleaseSelectWallet'; + static const phoneNumberHint = 'phoneNumberHint'; + static const noWalletsYet = 'noWalletsYet'; + static const deleteCategory = 'deleteCategory'; + static const deleteCategoryConfirm = 'deleteCategoryConfirm'; + static const noDescription = 'noDescription'; + static const transactionsIn = 'transactionsIn'; + static const wallets = 'wallets'; + static const noData = 'noData'; + static const thisMonth = 'thisMonth'; + static const thisWeek = 'thisWeek'; + static const lastThreeMonths = 'lastThreeMonths'; + static const lastSixMonths = 'lastSixMonths'; + static const thisYear = 'thisYear'; + static const custom = 'custom'; + static const noTransactionData = 'noTransactionData'; + static const addTransactionsToSeeChart = 'addTransactionsToSeeChart'; + static const deleteGroup = 'deleteGroup'; + static const deleteGroupConfirm = 'deleteGroupConfirm'; + static const duplicate = 'duplicate'; + static const categoryNameAlreadyExists = 'categoryNameAlreadyExists'; + static const deleteWallet = 'deleteWallet'; + static const deleteWalletConfirm = 'deleteWalletConfirm'; + static const defaultName = 'defaultName'; + static const officeElements = 'officeElements'; + static const officeElementsDesc = 'officeElementsDesc'; + static const monthly = 'monthly'; + static const yearly = 'yearly'; + static const recommended = 'recommended'; + static const loremIpsum = 'loremIpsum'; + static const full = 'full'; + static const compact = 'compact'; + static const cropAspectRatioCustom = 'cropAspectRatioCustom'; + static const deleteParty = 'deleteParty'; + static const deletePartyConfirm = 'deletePartyConfirm'; + static const general = 'general'; + static const pricePerMonth = 'pricePerMonth'; + static const partyTypeIndividual = 'partyTypeIndividual'; + static const partyTypeOrganization = 'partyTypeOrganization'; + static const partyTypeBusiness = 'partyTypeBusiness'; + static const partyTypePartnership = 'partyTypePartnership'; + static const partyTypeNonProfit = 'partyTypeNonProfit'; + static const partyTypeGovernmentAgency = 'partyTypeGovernmentAgency'; + static const partyTypeEducationalInstitution = 'partyTypeEducationalInstitution'; + static const partyTypeHealthcareProvider = 'partyTypeHealthcareProvider'; + static const selectPartyType = 'selectPartyType'; + static const selectDateOrDateRange = 'selectDateOrDateRange'; + static const selectDateRange = 'selectDateRange'; + static const fromDate = 'fromDate'; + static const toDate = 'toDate'; + static const apply = 'apply'; + static const sendCode = 'sendCode'; + static const code = 'code'; + static const newPassword = 'newPassword'; + static const resetPassword = 'resetPassword'; + static const passwordMatchError = 'passwordMatchError'; + static const otpEmptyError = 'otpEmptyError'; + static const otpInvalidError = 'otpInvalidError'; + static const startSignUp = 'startSignUp'; + static const verificationCode = 'verificationCode'; + static const enterCode = 'enterCode'; + static const submitCode = 'submitCode'; + static const resendCode = 'resendCode'; + static const or = 'or'; + static const proceedWithGoogle = 'proceedWithGoogle'; + static const proceedWithApple = 'proceedWithApple'; + static const emailPhoneValidateDesc = 'emailPhoneValidateDesc'; + static const logoutWarningTitle = 'logoutWarningTitle'; + static const logoutWarningMessage = 'logoutWarningMessage'; + static const syncNow = 'syncNow'; + static const logoutAnyway = 'logoutAnyway'; + static const balanceWillBeSetToZero = 'balanceWillBeSetToZero'; + static const accountAndPrivacy = 'accountAndPrivacy'; + static const data = 'data'; + static const dataDeletionOptions = 'dataDeletionOptions'; + static const dataDeletionDescription = 'dataDeletionDescription'; + static const dataDeletionRemoteDescription = 'dataDeletionRemoteDescription'; + static const dataDeletionWarning = 'dataDeletionWarning'; + static const requestDataDeletion = 'requestDataDeletion'; + static const dataDeletionEmailSubject = 'dataDeletionEmailSubject'; + static const dataDeletionEmailBody = 'dataDeletionEmailBody'; + static const dataDeletionFallbackTitle = 'dataDeletionFallbackTitle'; + static const dataDeletionFallbackInstructions = 'dataDeletionFallbackInstructions'; + static const dataDeletionFallbackEmailTo = 'dataDeletionFallbackEmailTo'; + static const dataDeletionFallbackSubjectLabel = 'dataDeletionFallbackSubjectLabel'; + static const dataDeletionFallbackBodyLabel = 'dataDeletionFallbackBodyLabel'; + static const copiedToClipboard = 'copiedToClipboard'; + static const partyEducationBanner = 'partyEducationBanner'; + static const groupEducationBanner = 'groupEducationBanner'; + static const emptyPartyTitle = 'emptyPartyTitle'; + static const emptyPartyDescription = 'emptyPartyDescription'; + static const emptyPartyStep1 = 'emptyPartyStep1'; + static const emptyPartyStep2 = 'emptyPartyStep2'; + static const emptyPartyStep3 = 'emptyPartyStep3'; + static const emptyPartyButtonText = 'emptyPartyButtonText'; + static const emptyPartyTipText = 'emptyPartyTipText'; + static const emptyCategoryTitle = 'emptyCategoryTitle'; + static const emptyCategoryDescription = 'emptyCategoryDescription'; + static const emptyCategoryStep1 = 'emptyCategoryStep1'; + static const emptyCategoryStep2 = 'emptyCategoryStep2'; + static const emptyCategoryStep3 = 'emptyCategoryStep3'; + static const emptyCategoryButtonText = 'emptyCategoryButtonText'; + static const emptyCategoryTipText = 'emptyCategoryTipText'; + static const emptyWalletTitle = 'emptyWalletTitle'; + static const emptyWalletDescription = 'emptyWalletDescription'; + static const emptyWalletStep1 = 'emptyWalletStep1'; + static const emptyWalletStep2 = 'emptyWalletStep2'; + static const emptyWalletStep3 = 'emptyWalletStep3'; + static const emptyWalletButtonText = 'emptyWalletButtonText'; + static const emptyWalletTipText = 'emptyWalletTipText'; + static const emptyTransactionTitle = 'emptyTransactionTitle'; + static const emptyTransactionDescription = 'emptyTransactionDescription'; + static const emptyTransactionStep1 = 'emptyTransactionStep1'; + static const emptyTransactionStep2 = 'emptyTransactionStep2'; + static const emptyTransactionStep3 = 'emptyTransactionStep3'; + static const emptyTransactionStep4 = 'emptyTransactionStep4'; + static const emptyTransactionButtonText = 'emptyTransactionButtonText'; + static const emptyTransactionTipText = 'emptyTransactionTipText'; + static const quickStart = 'quickStart'; + static const emptyTransactionTitleOne = 'emptyTransactionTitleOne'; + static const emptyTransactionDescriptionOne = 'emptyTransactionDescriptionOne'; + static const emptyTransactionInfo1 = 'emptyTransactionInfo1'; + static const emptyTransactionInfo2 = 'emptyTransactionInfo2'; + static const emptyTransactionInfo3 = 'emptyTransactionInfo3'; + static const emptyWalletTitleOne = 'emptyWalletTitleOne'; + static const emptyWalletDescriptionOne = 'emptyWalletDescriptionOne'; + static const emptyWalletInfo1 = 'emptyWalletInfo1'; + static const emptyWalletInfo2 = 'emptyWalletInfo2'; + static const emptyWalletInfo3 = 'emptyWalletInfo3'; + static const emptyCategoryTitleOne = 'emptyCategoryTitleOne'; + static const emptyCategoryDescriptionOne = 'emptyCategoryDescriptionOne'; + static const emptyCategoryInfo1 = 'emptyCategoryInfo1'; + static const emptyCategoryInfo2 = 'emptyCategoryInfo2'; + static const emptyCategoryInfo3 = 'emptyCategoryInfo3'; + static const emptyPartyTitleOne = 'emptyPartyTitleOne'; + static const emptyPartyDescriptionOne = 'emptyPartyDescriptionOne'; + static const emptyPartyInfo1 = 'emptyPartyInfo1'; + static const emptyPartyInfo2 = 'emptyPartyInfo2'; + static const emptyPartyInfo3 = 'emptyPartyInfo3'; + static const youAllSetTitleOne = 'youAllSetTitleOne'; + static const youAllSetDescriptionOne = 'youAllSetDescriptionOne'; + static const youAllSetInfo1 = 'youAllSetInfo1'; + static const youAllSetInfo2 = 'youAllSetInfo2'; + static const youAllSetInfo3 = 'youAllSetInfo3'; + static const whyThisMatters = 'whyThisMatters'; + static const financeInfo = 'financeInfo'; + static const setupWallet = 'setupWallet'; + static const manageCategories = 'manageCategories'; + static const addParties = 'addParties'; + static const startUsingTrakli = 'startUsingTrakli'; + static const welcomeDesc = 'welcomeDesc'; + static const selectLangDesc = 'selectLangDesc'; + static const setupWalletTitle = 'setupWalletTitle'; + static const setupWalletDesc = 'setupWalletDesc'; + static const walletSetup = 'walletSetup'; + static const useDefaultWallet = 'useDefaultWallet'; + static const renameDefaultWallet = 'renameDefaultWallet'; + static const createNewWallet = 'createNewWallet'; + static const allSetDesc = 'allSetDesc'; + static const goToDashboard = 'goToDashboard'; + static const stepCounter = 'stepCounter'; + static const createAutomatically = 'createAutomatically'; + static const createManually = 'createManually'; + static const selectFromWalletList = 'selectFromWalletList'; + static const selectFromGroupList = 'selectFromGroupList'; + static const groupSetup = 'groupSetup'; + static const setupGroupTitle = 'setupGroupTitle'; + static const setupGroupDesc = 'setupGroupDesc'; + static const setupCategoryTitle = 'setupCategoryTitle'; + static const setupCategoryDesc = 'setupCategoryDesc'; + static const createDefaultCategories = 'createDefaultCategories'; + static const createDefaultCategoriesDesc = 'createDefaultCategoriesDesc'; + static const skipCategories = 'skipCategories'; + static const skipCategoriesDesc = 'skipCategoriesDesc'; + static const alreadyHaveAccount = 'alreadyHaveAccount'; + static const totalExpense = 'totalExpense'; + static const advanced = 'advanced'; + static const synchronization = 'synchronization'; + static const syncHistoryDesc = 'syncHistoryDesc'; + static const lastSyncStatus = 'lastSyncStatus'; + static const noSyncHistory = 'noSyncHistory'; + static const pendingChanges = 'pendingChanges'; + static const noPendingChanges = 'noPendingChanges'; + static const failedChanges = 'failedChanges'; + static const noFailedChanges = 'noFailedChanges'; + static const dismiss = 'dismiss'; + static const categorySalary = 'categorySalary'; + static const categorySalaryDesc = 'categorySalaryDesc'; + static const categoryFreelance = 'categoryFreelance'; + static const categoryFreelanceDesc = 'categoryFreelanceDesc'; + static const categoryInvestments = 'categoryInvestments'; + static const categoryInvestmentsDesc = 'categoryInvestmentsDesc'; + static const categoryGifts = 'categoryGifts'; + static const categoryGiftsDesc = 'categoryGiftsDesc'; + static const categoryRefunds = 'categoryRefunds'; + static const categoryRefundsDesc = 'categoryRefundsDesc'; + static const categoryOtherIncome = 'categoryOtherIncome'; + static const categoryOtherIncomeDesc = 'categoryOtherIncomeDesc'; + static const categoryFoodDining = 'categoryFoodDining'; + static const categoryFoodDiningDesc = 'categoryFoodDiningDesc'; + static const categoryTransportation = 'categoryTransportation'; + static const categoryTransportationDesc = 'categoryTransportationDesc'; + static const categoryHousing = 'categoryHousing'; + static const categoryHousingDesc = 'categoryHousingDesc'; + static const categoryUtilities = 'categoryUtilities'; + static const categoryUtilitiesDesc = 'categoryUtilitiesDesc'; + static const categoryHealthcare = 'categoryHealthcare'; + static const categoryHealthcareDesc = 'categoryHealthcareDesc'; + static const categoryEntertainment = 'categoryEntertainment'; + static const categoryEntertainmentDesc = 'categoryEntertainmentDesc'; + static const categoryShopping = 'categoryShopping'; + static const categoryShoppingDesc = 'categoryShoppingDesc'; + static const categoryPersonalCare = 'categoryPersonalCare'; + static const categoryPersonalCareDesc = 'categoryPersonalCareDesc'; + static const categoryEducation = 'categoryEducation'; + static const categoryEducationDesc = 'categoryEducationDesc'; + static const categorySubscriptions = 'categorySubscriptions'; + static const categorySubscriptionsDesc = 'categorySubscriptionsDesc'; + static const categoryInsurance = 'categoryInsurance'; + static const categoryInsuranceDesc = 'categoryInsuranceDesc'; + static const categorySavings = 'categorySavings'; + static const categorySavingsDesc = 'categorySavingsDesc'; + static const categoryGiftsDonations = 'categoryGiftsDonations'; + static const categoryGiftsDonationsDesc = 'categoryGiftsDonationsDesc'; + static const categoryTravel = 'categoryTravel'; + static const categoryTravelDesc = 'categoryTravelDesc'; + static const categoryOtherExpenses = 'categoryOtherExpenses'; + static const categoryOtherExpensesDesc = 'categoryOtherExpensesDesc'; + static const notificationSettings = 'notificationSettings'; + static const notificationChannels = 'notificationChannels'; + static const notificationTypes = 'notificationTypes'; + static const emailNotifications = 'emailNotifications'; + static const emailNotificationsDesc = 'emailNotificationsDesc'; + static const pushNotifications = 'pushNotifications'; + static const pushNotificationsDesc = 'pushNotificationsDesc'; + static const inAppNotifications = 'inAppNotifications'; + static const inAppNotificationsDesc = 'inAppNotificationsDesc'; + static const reminders = 'reminders'; + static const remindersDesc = 'remindersDesc'; + static const financialInsights = 'financialInsights'; + static const financialInsightsDesc = 'financialInsightsDesc'; + static const insightsFrequency = 'insightsFrequency'; + static const insightsFrequencyDesc = 'insightsFrequencyDesc'; + static const insightsFrequencyWeekly = 'insightsFrequencyWeekly'; + static const insightsFrequencyMonthly = 'insightsFrequencyMonthly'; + static const selectFrequency = 'selectFrequency'; + static const engagementReminders = 'engagementReminders'; + static const engagementRemindersDesc = 'engagementRemindersDesc'; + static const preferencesUpdated = 'preferencesUpdated'; + static const preferencesUpdateFailed = 'preferencesUpdateFailed'; + static const loadingPreferences = 'loadingPreferences'; + static const errorLoadingPreferences = 'errorLoadingPreferences'; + static const openSourceProject = 'openSourceProject'; + static const viewOnGithub = 'viewOnGithub'; + static const images = 'images'; + static const files = 'files'; + static const noImages = 'noImages'; + static const noFiles = 'noFiles'; + static const orphanedMediaCleanupLog = 'orphanedMediaCleanupLog'; + static const orphanedMediaCleanupLogDesc = 'orphanedMediaCleanupLogDesc'; + static const orphanedMediaCleanupLogEmpty = 'orphanedMediaCleanupLogEmpty'; + static const appUpdateGooglePlay = 'appUpdateGooglePlay'; + static const appUpdateAppStore = 'appUpdateAppStore'; + static const appUpdateNewVersionAvailable = 'appUpdateNewVersionAvailable'; + static const appUpdateOptionalMessage = 'appUpdateOptionalMessage'; + static const appUpdateForceMessage = 'appUpdateForceMessage'; + static const appUpdateLater = 'appUpdateLater'; + static const appUpdateNow = 'appUpdateNow'; + static const appUpdateError = 'appUpdateError'; + static const appUpdateReadyToInstall = 'appUpdateReadyToInstall'; + static const appUpdateRestart = 'appUpdateRestart'; + static const appUpdateReady = 'appUpdateReady'; + static const appUpdateRestartPrompt = 'appUpdateRestartPrompt'; + static const deleteYourAccount = 'deleteYourAccount'; + static const deleteAccount = 'deleteAccount'; + static const deleteAccountDesc = 'deleteAccountDesc'; + static const helpUsImprove = 'helpUsImprove'; + static const whyDeleteAccount = 'whyDeleteAccount'; + static const transactionsDesc = 'transactionsDesc'; + static const transfersDesc = 'transfersDesc'; + static const categoryDesc = 'categoryDesc'; + static const groupsDesc = 'groupsDesc'; + static const partiesDesc = 'partiesDesc'; + static const walletsDesc = 'walletsDesc'; + static const cancelled = 'cancelled'; + static const imports = 'imports'; + static const importsDesc = 'importsDesc'; + static const importScanDocument = 'importScanDocument'; + static const importScanDocumentDesc = 'importScanDocumentDesc'; + static const importAnalyze = 'importAnalyze'; + static const importAmount = 'importAmount'; + static const importType = 'importType'; + static const importDescription = 'importDescription'; + static const importParty = 'importParty'; + static const importWallet = 'importWallet'; + static const importCategory = 'importCategory'; + static const importDate = 'importDate'; + static const importDocTypeLabel = 'importDocTypeLabel'; + static const importDocBankStatement = 'importDocBankStatement'; + static const importDocReceipt = 'importDocReceipt'; + static const importDocInvoice = 'importDocInvoice'; + static const importDocPayStub = 'importDocPayStub'; + static const importDocUtilityBill = 'importDocUtilityBill'; + static const importReviewSuggestions = 'importReviewSuggestions'; + static const importAnalyzing = 'importAnalyzing'; + static const importExtracting = 'importExtracting'; + static const importEnriching = 'importEnriching'; + static const importCheckingDuplicates = 'importCheckingDuplicates'; + static const importAnalysisFailed = 'importAnalysisFailed'; + static const importAnalysisFailedHint = 'importAnalysisFailedHint'; + static const importNoSuggestions = 'importNoSuggestions'; + static const importNoSuggestionsTitle = 'importNoSuggestionsTitle'; + static const importNoTransactionsFound = 'importNoTransactionsFound'; + static const importNoSuggestionsClose = 'importNoSuggestionsClose'; + static const importNoAcceptedSuggestions = 'importNoAcceptedSuggestions'; + static const importCreatedCount = 'importCreatedCount'; + static const importAutoCreateWallets = 'importAutoCreateWallets'; + static const importAutoCreateParties = 'importAutoCreateParties'; + static const importAutoCreateCategories = 'importAutoCreateCategories'; + static const importConfirm = 'importConfirm'; + static const importRecentActivity = 'importRecentActivity'; + static const importNoActivity = 'importNoActivity'; + static const importSourceLabel = 'importSourceLabel'; + static const importSourceCamera = 'importSourceCamera'; + static const importSourceCameraDesc = 'importSourceCameraDesc'; + static const importSourceGallery = 'importSourceGallery'; + static const importSourceGalleryDesc = 'importSourceGalleryDesc'; + static const importSourceFile = 'importSourceFile'; + static const importSourceFileDesc = 'importSourceFileDesc'; + static const importAiSuggested = 'importAiSuggested'; + static const importConfirmSheetTitle = 'importConfirmSheetTitle'; + static const importCreateTransactions = 'importCreateTransactions'; + static const importAutoCreateMissingHint = 'importAutoCreateMissingHint'; + static const importAutoCreateNoneNeeded = 'importAutoCreateNoneNeeded'; + static const budget = 'budget'; + static const budgets = 'budgets'; + static const addBudget = 'addBudget'; + static const createBudget = 'createBudget'; + static const editBudget = 'editBudget'; + static const deleteBudget = 'deleteBudget'; + static const deleteBudgetConfirm = 'deleteBudgetConfirm'; + static const deleteBudgetSuccess = 'deleteBudgetSuccess'; + static const deleteBudgetError = 'deleteBudgetError'; + static const closeBudgetPeriod = 'closeBudgetPeriod'; + static const closePeriod = 'closePeriod'; + static const closePeriodConfirm = 'closePeriodConfirm'; + static const closePeriodSuccess = 'closePeriodSuccess'; + static const closePeriodError = 'closePeriodError'; + static const budgetAmount = 'budgetAmount'; + static const budgetPeriod = 'budgetPeriod'; + static const budgetTargets = 'budgetTargets'; + static const budgetPeriodHistory = 'budgetPeriodHistory'; + static const budgetStatus = 'budgetStatus'; + static const budgetRemaining = 'budgetRemaining'; + static const budgetProjected = 'budgetProjected'; + static const budgetRefunds = 'budgetRefunds'; + static const budgetRollover = 'budgetRollover'; + static const budgetRolloverIn = 'budgetRolloverIn'; + static const budgetPeriodOpen = 'budgetPeriodOpen'; + static const closePeriodMessage = 'closePeriodMessage'; + static const closePeriodTitle = 'closePeriodTitle'; + static const budgetStatusOverBudget = 'budgetStatusOverBudget'; + static const budgetStatusForecastBreach = 'budgetStatusForecastBreach'; + static const budgetStatusNearLimit = 'budgetStatusNearLimit'; + static const budgetStatusOnTrack = 'budgetStatusOnTrack'; + static const budgetStatusAwaitingSync = 'budgetStatusAwaitingSync'; + static const budgetPeriodWeekly = 'budgetPeriodWeekly'; + static const budgetPeriodMonthly = 'budgetPeriodMonthly'; + static const budgetPeriodYearly = 'budgetPeriodYearly'; + static const budgetKpiRemaining = 'budgetKpiRemaining'; + static const budgetKpiProjected = 'budgetKpiProjected'; + static const budgetKpiRefunds = 'budgetKpiRefunds'; + static const budgetKpiRolloverIn = 'budgetKpiRolloverIn'; + static const budgetTargetCategory = 'budgetTargetCategory'; + static const budgetTargetWallet = 'budgetTargetWallet'; + static const budgetTargetGroup = 'budgetTargetGroup'; + static const budgetTargetsLabel = 'budgetTargetsLabel'; + static const budgetPeriodHistoryLabel = 'budgetPeriodHistoryLabel'; + static const budgetTargetsApplyAll = 'budgetTargetsApplyAll'; + static const budgetNoPeriods = 'budgetNoPeriods'; + static const budgetNoProgress = 'budgetNoProgress'; + static const budgetTargetUnnamed = 'budgetTargetUnnamed'; + +} diff --git a/lib/presentation/budget/add_budget_screen.dart b/lib/presentation/budget/add_budget_screen.dart index f42f01d1..90e57f1f 100644 --- a/lib/presentation/budget/add_budget_screen.dart +++ b/lib/presentation/budget/add_budget_screen.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -7,6 +8,8 @@ import 'package:trakli/domain/entities/category_entity.dart'; import 'package:trakli/domain/entities/group_entity.dart'; import 'package:trakli/domain/entities/wallet_entity.dart'; import 'package:trakli/domain/repositories/budget_repository.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; +import 'package:trakli/presentation/utils/helpers.dart' show showSnackBar; import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; import 'package:trakli/presentation/category/cubit/category_cubit.dart'; import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; @@ -104,23 +107,17 @@ class _AddBudgetScreenState extends State { Future _submit() async { final name = _name.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please enter a name')), - ); + showSnackBar(message: LocaleKeys.budgetNameRequired.tr()); return; } final amount = double.tryParse(_amount.text.trim()); if (amount == null || amount <= 0) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please enter a valid amount')), - ); + showSnackBar(message: LocaleKeys.budgetAmountRequired.tr()); return; } final currency = _currency.text.trim().toUpperCase(); if (currency.length != 3) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Currency must be 3 letters')), - ); + showSnackBar(message: LocaleKeys.budgetCurrencyRequired.tr()); return; } @@ -134,9 +131,8 @@ class _AddBudgetScreenState extends State { periodType: _periodType, startDate: _startDate, endDate: _endDate, - description: _description.text.trim().isEmpty - ? null - : _description.text.trim(), + description: + _description.text.trim().isEmpty ? null : _description.text.trim(), rolloverEnabled: _rolloverEnabled, thresholdPercent: _threshold, forecastAlertsEnabled: _forecastAlertsEnabled, @@ -151,9 +147,8 @@ class _AddBudgetScreenState extends State { periodType: _periodType, startDate: _startDate, endDate: _endDate, - description: _description.text.trim().isEmpty - ? null - : _description.text.trim(), + description: + _description.text.trim().isEmpty ? null : _description.text.trim(), rolloverEnabled: _rolloverEnabled, thresholdPercent: _threshold, forecastAlertsEnabled: _forecastAlertsEnabled, @@ -171,7 +166,7 @@ class _AddBudgetScreenState extends State { return Scaffold( backgroundColor: tones.bgPage, appBar: AppBar( - title: Text(_isEdit ? 'Edit budget' : 'New budget'), + title: Text(_isEdit ? LocaleKeys.editBudget.tr() : LocaleKeys.newBudget.tr()), ), body: BlocBuilder( builder: (context, state) { @@ -182,16 +177,16 @@ class _AddBudgetScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SectionLabel(text: 'Name'), + _SectionLabel(text: LocaleKeys.name.tr()), TextField( controller: _name, - decoration: const InputDecoration( - hintText: 'e.g. Groceries', - border: OutlineInputBorder(), + decoration: InputDecoration( + hintText: LocaleKeys.nameHint.tr(), + border: const OutlineInputBorder(), ), ), SizedBox(height: 14.h), - _SectionLabel(text: 'Amount'), + _SectionLabel(text: LocaleKeys.amount.tr()), Row( children: [ Expanded( @@ -206,9 +201,9 @@ class _AddBudgetScreenState extends State { RegExp(r'[0-9.]'), ), ], - decoration: const InputDecoration( - hintText: '0.00', - border: OutlineInputBorder(), + decoration: InputDecoration( + hintText: LocaleKeys.amountHint.tr(), + border: const OutlineInputBorder(), ), ), ), @@ -227,28 +222,28 @@ class _AddBudgetScreenState extends State { ], ), SizedBox(height: 14.h), - _SectionLabel(text: 'Period'), + _SectionLabel(text: LocaleKeys.period.tr()), _PeriodChips( selected: _periodType, onChange: (p) => setState(() => _periodType = p), ), SizedBox(height: 14.h), - _SectionLabel(text: 'Start date'), + _SectionLabel(text: LocaleKeys.startDate.tr()), _DateRow( date: _startDate, onTap: () => _pickDate(isStart: true), ), if (_periodType == BudgetPeriodType.custom) ...[ SizedBox(height: 14.h), - _SectionLabel(text: 'End date'), + _SectionLabel(text: LocaleKeys.endDate.tr()), _DateRow( date: _endDate, onTap: () => _pickDate(isStart: false), - placeholder: 'Select end date', + placeholder: LocaleKeys.selectEndDate.tr(), ), ], SizedBox(height: 14.h), - _SectionLabel(text: 'Targets'), + _SectionLabel(text: LocaleKeys.targets.tr()), InkWell( onTap: _pickTargets, borderRadius: BorderRadius.circular(AppRadii.md), @@ -266,8 +261,8 @@ class _AddBudgetScreenState extends State { Expanded( child: Text( _targets.isEmpty - ? 'Apply to all transactions' - : '${_targets.length} ${_targets.length == 1 ? 'target' : 'targets'} selected', + ? LocaleKeys.budgetTargetsApplyAll.tr() + : '${_targets.length} ${_targets.length == 1 ? LocaleKeys.target.tr() : LocaleKeys.targets.tr()} ${LocaleKeys.selected.tr()}', style: TextStyle( fontSize: 14.sp, color: _targets.isEmpty @@ -286,7 +281,9 @@ class _AddBudgetScreenState extends State { ), ), SizedBox(height: 14.h), - _SectionLabel(text: 'Alert at $_threshold% used'), + _SectionLabel( + text: LocaleKeys.alertAtThreshold.tr().replaceFirst('{0}', '$_threshold'), + ), Slider( value: _threshold.toDouble(), min: 50, @@ -297,19 +294,15 @@ class _AddBudgetScreenState extends State { ), SwitchListTile( contentPadding: EdgeInsets.zero, - title: const Text('Roll over unused amount'), - subtitle: const Text( - 'Carry leftover budget into next period', - ), + title: Text(LocaleKeys.budgetRollover.tr()), + subtitle: Text(LocaleKeys.budgetRolloverDescription.tr()), value: _rolloverEnabled, onChanged: (v) => setState(() => _rolloverEnabled = v), ), SwitchListTile( contentPadding: EdgeInsets.zero, - title: const Text('Forecast alerts'), - subtitle: const Text( - 'Warn me if I\'m projected to overspend', - ), + title: Text(LocaleKeys.budgetForecastAlerts.tr()), + subtitle: Text(LocaleKeys.budgetForecastAlertsDescription.tr()), value: _forecastAlertsEnabled, onChanged: (v) => setState(() => _forecastAlertsEnabled = v), @@ -317,12 +310,12 @@ class _AddBudgetScreenState extends State { if (_isEdit) SwitchListTile( contentPadding: EdgeInsets.zero, - title: const Text('Active'), + title: Text(LocaleKeys.active.tr()), value: _isActive, onChanged: (v) => setState(() => _isActive = v), ), SizedBox(height: 14.h), - _SectionLabel(text: 'Description (optional)'), + _SectionLabel(text: LocaleKeys.description.tr()), TextField( controller: _description, maxLines: 3, @@ -358,7 +351,7 @@ class _AddBudgetScreenState extends State { width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) - : Text(_isEdit ? 'Save changes' : 'Create budget'), + : Text(_isEdit ? LocaleKeys.saveChanges.tr() : LocaleKeys.createBudget.tr()), ), ), ), @@ -430,10 +423,10 @@ class _PeriodChips extends StatelessWidget { scrollDirection: Axis.horizontal, child: Row( children: [ - chip(BudgetPeriodType.weekly, 'Weekly'), - chip(BudgetPeriodType.monthly, 'Monthly'), - chip(BudgetPeriodType.yearly, 'Yearly'), - chip(BudgetPeriodType.custom, 'Custom'), + chip(BudgetPeriodType.weekly, LocaleKeys.periodWeekly.tr()), + chip(BudgetPeriodType.monthly, LocaleKeys.periodMonthly.tr()), + chip(BudgetPeriodType.yearly, LocaleKeys.periodYearly.tr()), + chip(BudgetPeriodType.custom, LocaleKeys.periodCustom.tr()), ], ), ); @@ -453,8 +446,11 @@ class _DateRow extends StatelessWidget { @override Widget build(BuildContext context) { final tones = context.tones; + final displayPlaceholder = placeholder == 'Select date' + ? LocaleKeys.selectDate.tr() + : placeholder; final label = date == null - ? placeholder + ? displayPlaceholder : '${date!.year}-${date!.month.toString().padLeft(2, '0')}-${date!.day.toString().padLeft(2, '0')}'; return InkWell( onTap: onTap, @@ -478,9 +474,7 @@ class _DateRow extends StatelessWidget { label, style: TextStyle( fontSize: 14.sp, - color: date == null - ? tones.textMuted - : tones.textPrimary, + color: date == null ? tones.textMuted : tones.textPrimary, ), ), ), @@ -555,23 +549,23 @@ class _TargetPickerSheetState extends State<_TargetPickerSheet> children: [ Expanded( child: Text( - 'Select targets', + LocaleKeys.selectTargets.tr(), style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w700, ), ), ), - TextButton(onPressed: _confirm, child: const Text('Done')), + TextButton(onPressed: _confirm, child: Text(LocaleKeys.done.tr())), ], ), ), TabBar( controller: _tabs, - tabs: const [ - Tab(text: 'Categories'), - Tab(text: 'Wallets'), - Tab(text: 'Groups'), + tabs: [ + Tab(text: LocaleKeys.categories.tr()), + Tab(text: LocaleKeys.wallets.tr()), + Tab(text: LocaleKeys.groups.tr()), ], ), Expanded( @@ -619,15 +613,14 @@ class _CategoryList extends StatelessWidget { builder: (context, state) { final items = state.categories; if (items.isEmpty) { - return const Center(child: Text('No categories yet')); + return Center(child: Text(LocaleKeys.noCategoriesYet.tr())); } return ListView.builder( controller: scrollController, itemCount: items.length, itemBuilder: (_, i) => _TargetTile( label: items[i].name, - selected: - selectedKeys.contains('category::${items[i].clientId}'), + selected: selectedKeys.contains('category::${items[i].clientId}'), onToggle: () => onToggle(items[i].clientId), ), ); @@ -652,7 +645,7 @@ class _WalletList extends StatelessWidget { builder: (context, state) { final items = state.wallets; if (items.isEmpty) { - return const Center(child: Text('No wallets yet')); + return Center(child: Text(LocaleKeys.noWalletsYet.tr())); } return ListView.builder( controller: scrollController, @@ -684,7 +677,7 @@ class _GroupList extends StatelessWidget { builder: (context, state) { final items = state.groups; if (items.isEmpty) { - return const Center(child: Text('No groups yet')); + return Center(child: Text(LocaleKeys.noGroupsYet.tr())); } return ListView.builder( controller: scrollController, diff --git a/lib/presentation/budget/budget_detail_screen.dart b/lib/presentation/budget/budget_detail_screen.dart index acf4e04a..8d59ff33 100644 --- a/lib/presentation/budget/budget_detail_screen.dart +++ b/lib/presentation/budget/budget_detail_screen.dart @@ -1,14 +1,21 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:trakli/core/error/failures/failures.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; import 'package:trakli/domain/entities/budget_period_state_entity.dart'; import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/budget/add_budget_screen.dart'; import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/enums.dart'; +import 'package:trakli/presentation/utils/dialogs.dart' + show showDeleteConfirmationDialog, showConfirmationDialog; +import 'package:trakli/presentation/utils/helpers.dart' + show showSnackBar, formatDateYmd; class BudgetDetailScreen extends StatefulWidget { final BudgetEntity budget; @@ -38,57 +45,37 @@ class _BudgetDetailScreenState extends State { ); } + bool _budgetWasDeleted(BudgetState state) { + return state.budgets.length < (_currentBudget(state).id != null ? 1 : 0); + } + Future _confirmDelete(BudgetEntity budget) async { - final confirm = await showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Delete budget?'), - content: Text( - 'This will remove "${budget.name}" from your budgets. This cannot be undone.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: () => Navigator.pop(context, true), - style: FilledButton.styleFrom(backgroundColor: Colors.redAccent), - child: const Text('Delete'), - ), - ], - ), + final confirm = await showDeleteConfirmationDialog( + context, + title: LocaleKeys.deleteBudget.tr(), + message: LocaleKeys.deleteBudgetConfirm.tr(namedArgs: {'name': budget.name}), ); - if (confirm != true || !mounted) return; - await context.read().deleteBudget(budget.clientId); - if (mounted) AppNavigator.pop(context); + if (!confirm || !mounted) return; + + if (mounted) { + context.read().deleteBudget(budget.clientId); + } } Future _confirmClosePeriod(BudgetEntity budget) async { final id = budget.id; if (id == null) return; - final confirm = await showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Close this period?'), - content: const Text( - 'This will lock the current period\'s spending and start a new one. ' - 'Unused amount will roll into the next period.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Close period'), - ), - ], - ), + final confirm = await showConfirmationDialog( + context, + title: LocaleKeys.closePeriodTitle.tr(), + message: LocaleKeys.closePeriodMessage.tr(), + confirmText: LocaleKeys.closePeriodConfirm.tr(), ); - if (confirm != true || !mounted) return; - await context.read().closeBudgetPeriod(id); + if (!confirm || !mounted) return; + + if (mounted) { + context.read().closeBudgetPeriod(id); + } } @override @@ -97,7 +84,7 @@ class _BudgetDetailScreenState extends State { return Scaffold( backgroundColor: tones.bgPage, appBar: AppBar( - title: const Text('Budget'), + title: Text(LocaleKeys.budget.tr()), actions: [ BlocBuilder( builder: (context, state) { @@ -121,60 +108,96 @@ class _BudgetDetailScreenState extends State { ), ], ), - body: BlocBuilder( - builder: (context, state) { - final budget = _currentBudget(state); - final progress = state.selectedBudgetProgress; - final targets = state.selectedBudgetTargets; - final periods = state.selectedBudgetPeriodStates; - return RefreshIndicator( - onRefresh: () async { - final id = budget.id; - if (id != null) { - await context.read().fetchProgress(id); - await context.read().refreshPeriodStates(); - } - }, - child: ListView( - padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 24.h), - children: [ - _HeaderCard(budget: budget, progress: progress), - SizedBox(height: 14.h), - if (state.isProgressLoading && progress == null) - const Center(child: CircularProgressIndicator()) - else if (progress != null) - _KpiGrid(budget: budget, progress: progress) - else if (budget.id == null) - _InfoBanner( - text: - 'Progress will be available once this budget syncs with the server.', - ), - SizedBox(height: 20.h), - _SectionHeader(text: 'Targets'), - _TargetsBlock(budget: budget, targets: targets), - SizedBox(height: 20.h), - _SectionHeader(text: 'Period history'), - _PeriodHistoryBlock(periods: periods), - if (budget.rolloverEnabled && budget.id != null) ...[ - SizedBox(height: 24.h), - FilledButton.icon( - onPressed: state.isClosingPeriod - ? null - : () => _confirmClosePeriod(budget), - icon: state.isClosingPeriod - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.lock_outline), - label: const Text('Close current period'), - ), - ], - ], - ), - ); + body: BlocListener( + listenWhen: (prev, curr) { + final deletionCompleted = prev.isDeleting && !curr.isDeleting; + final closingCompleted = + prev.isClosingPeriod && !curr.isClosingPeriod; + return deletionCompleted || closingCompleted; + }, + listener: (context, state) { + if (state.isDeleting || state.isClosingPeriod) return; + + if (state.failure != const Failure.none()) { + showSnackBar( + message: state.isDeleting + ? LocaleKeys.deleteBudgetError.tr() + : LocaleKeys.closePeriodError.tr(), + ); + return; + } + + if (_budgetWasDeleted(state)) { + showSnackBar( + message: LocaleKeys.deleteBudgetSuccess.tr(), + isSuccess: true, + ); + AppNavigator.pop(context); + } else { + showSnackBar( + message: LocaleKeys.closePeriodSuccess.tr(), + isSuccess: true, + ); + } }, + child: BlocBuilder( + builder: (context, state) { + final budget = _currentBudget(state); + final progress = state.selectedBudgetProgress; + final targets = state.selectedBudgetTargets; + final periods = state.selectedBudgetPeriodStates; + return RefreshIndicator( + onRefresh: () { + final id = budget.id; + if (id != null) { + return Future.wait([ + context.read().fetchProgress(id), + context.read().refreshPeriodStates(), + ]); + } + return Future.value(); + }, + child: ListView( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 24.h), + children: [ + _HeaderCard(budget: budget, progress: progress), + SizedBox(height: 14.h), + if (state.isProgressLoading && progress == null) + const Center(child: CircularProgressIndicator()) + else if (progress != null) + _KpiGrid(budget: budget, progress: progress) + else if (budget.id == null) + _InfoBanner( + text: LocaleKeys.budgetNoProgress.tr(), + ), + SizedBox(height: 20.h), + _SectionHeader(text: LocaleKeys.budgetTargetsLabel.tr()), + _TargetsBlock(budget: budget, targets: targets), + SizedBox(height: 20.h), + _SectionHeader( + text: LocaleKeys.budgetPeriodHistoryLabel.tr()), + _PeriodHistoryBlock(periods: periods), + if (budget.rolloverEnabled && budget.id != null) ...[ + SizedBox(height: 24.h), + FilledButton.icon( + onPressed: state.isClosingPeriod + ? null + : () => _confirmClosePeriod(budget), + icon: state.isClosingPeriod + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.lock_outline), + label: Text(LocaleKeys.closePeriod.tr()), + ), + ], + ], + ), + ); + }, + ), ), ); } @@ -197,27 +220,28 @@ class _HeaderCard extends StatelessWidget { String _statusLabel(BudgetStatus? status) { return switch (status) { - BudgetStatus.overBudget => 'OVER BUDGET', - BudgetStatus.forecastBreach => 'FORECAST BREACH', - BudgetStatus.nearLimit => 'NEAR LIMIT', - BudgetStatus.onTrack => 'ON TRACK', - null => 'AWAITING SYNC', + BudgetStatus.overBudget => LocaleKeys.budgetStatusOverBudget.tr(), + BudgetStatus.forecastBreach => LocaleKeys.budgetStatusForecastBreach.tr(), + BudgetStatus.nearLimit => LocaleKeys.budgetStatusNearLimit.tr(), + BudgetStatus.onTrack => LocaleKeys.budgetStatusOnTrack.tr(), + null => LocaleKeys.budgetStatusAwaitingSync.tr(), }; } String _periodLabel() { final start = budget.startDate; - final fmt = (DateTime d) => - '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; if (budget.periodType == BudgetPeriodType.custom && budget.endDate != null) { - return '${fmt(start)} → ${fmt(budget.endDate!)}'; + return '${formatDateYmd(start)} → ${formatDateYmd(budget.endDate!)}'; } return switch (budget.periodType) { - BudgetPeriodType.weekly => 'Weekly · starting ${fmt(start)}', - BudgetPeriodType.monthly => 'Monthly · starting ${fmt(start)}', - BudgetPeriodType.yearly => 'Yearly · starting ${fmt(start)}', - BudgetPeriodType.custom => fmt(start), + BudgetPeriodType.weekly => + '${LocaleKeys.budgetPeriodWeekly.tr()} ${formatDateYmd(start)}', + BudgetPeriodType.monthly => + '${LocaleKeys.budgetPeriodMonthly.tr()} ${formatDateYmd(start)}', + BudgetPeriodType.yearly => + '${LocaleKeys.budgetPeriodYearly.tr()} ${formatDateYmd(start)}', + BudgetPeriodType.custom => formatDateYmd(start), }; } @@ -278,8 +302,7 @@ class _HeaderCard extends StatelessWidget { color: tones.textSecondary, ), ), - if (budget.description != null && - budget.description!.isNotEmpty) ...[ + if (budget.description != null && budget.description!.isNotEmpty) ...[ SizedBox(height: 8.h), Text( budget.description!, @@ -335,13 +358,13 @@ class _KpiGrid extends StatelessWidget { @override Widget build(BuildContext context) { final cells = <_Kpi>[ - _Kpi('Remaining', + _Kpi(LocaleKeys.budgetKpiRemaining.tr(), '${budget.currency} ${progress.remaining.toStringAsFixed(2)}'), - _Kpi('Projected', + _Kpi(LocaleKeys.budgetKpiProjected.tr(), '${budget.currency} ${progress.projectedSpend.toStringAsFixed(2)}'), - _Kpi('Refunds', + _Kpi(LocaleKeys.budgetKpiRefunds.tr(), '${budget.currency} ${progress.refunds.toStringAsFixed(2)}'), - _Kpi('Rollover in', + _Kpi(LocaleKeys.budgetKpiRolloverIn.tr(), '${budget.currency} ${progress.rolloverIn.toStringAsFixed(2)}'), ]; @@ -435,9 +458,9 @@ class _TargetsBlock extends StatelessWidget { String _typeLabel(BudgetTargetType type) { return switch (type) { - BudgetTargetType.category => 'Category', - BudgetTargetType.wallet => 'Wallet', - BudgetTargetType.group => 'Group', + BudgetTargetType.category => LocaleKeys.budgetTargetCategory.tr(), + BudgetTargetType.wallet => LocaleKeys.budgetTargetWallet.tr(), + BudgetTargetType.group => LocaleKeys.budgetTargetGroup.tr(), }; } @@ -461,7 +484,7 @@ class _TargetsBlock extends StatelessWidget { border: Border.all(color: tones.borderLight), ), child: Text( - 'Applies to all transactions in this period.', + LocaleKeys.budgetTargetsApplyAll.tr(), style: TextStyle(fontSize: 13.sp, color: tones.textSecondary), ), ); @@ -480,7 +503,8 @@ class _TargetsBlock extends StatelessWidget { _iconFor(budget.targets[i].type), color: tones.textMuted, ), - title: Text(budget.targets[i].name ?? '(unnamed)'), + title: Text(budget.targets[i].name ?? + LocaleKeys.budgetTargetUnnamed.tr()), subtitle: Text(_typeLabel(budget.targets[i].type)), dense: true, ), @@ -497,9 +521,6 @@ class _PeriodHistoryBlock extends StatelessWidget { final List periods; const _PeriodHistoryBlock({required this.periods}); - String _fmtDate(DateTime d) => - '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; - @override Widget build(BuildContext context) { final tones = context.tones; @@ -512,7 +533,7 @@ class _PeriodHistoryBlock extends StatelessWidget { border: Border.all(color: tones.borderLight), ), child: Text( - 'No closed periods yet.', + LocaleKeys.budgetNoPeriods.tr(), style: TextStyle(fontSize: 13.sp, color: tones.textSecondary), ), ); @@ -529,7 +550,7 @@ class _PeriodHistoryBlock extends StatelessWidget { ListTile( dense: true, title: Text( - '${_fmtDate(periods[i].periodStart)} → ${_fmtDate(periods[i].periodEnd)}', + '${formatDateYmd(periods[i].periodStart)} → ${formatDateYmd(periods[i].periodEnd)}', ), subtitle: Text( 'Spent ${periods[i].netSpent.toStringAsFixed(2)}' @@ -537,8 +558,8 @@ class _PeriodHistoryBlock extends StatelessWidget { ' · rollover out ${periods[i].rolloverOut.toStringAsFixed(2)}', ), trailing: periods[i].closedAt == null - ? const Text('Open', - style: TextStyle(fontWeight: FontWeight.w600)) + ? Text(LocaleKeys.budgetPeriodOpen.tr(), + style: const TextStyle(fontWeight: FontWeight.w600)) : null, ), if (i != periods.length - 1) @@ -565,8 +586,7 @@ class _InfoBanner extends StatelessWidget { ), child: Row( children: [ - Icon(Icons.info_outline, - color: tones.brand.accent, size: 18.sp), + Icon(Icons.info_outline, color: tones.brand.accent, size: 18.sp), SizedBox(width: 8.w), Expanded( child: Text( diff --git a/lib/presentation/budget/budget_screen.dart b/lib/presentation/budget/budget_screen.dart index 4165b281..516434ee 100644 --- a/lib/presentation/budget/budget_screen.dart +++ b/lib/presentation/budget/budget_screen.dart @@ -1,7 +1,9 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/budget/add_budget_screen.dart'; import 'package:trakli/presentation/budget/budget_detail_screen.dart'; import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; @@ -38,9 +40,9 @@ class _BudgetScreenState extends State { return Scaffold( backgroundColor: tones.bgPage, appBar: PageAppBar( - title: 'Budgets', + title: LocaleKeys.budgetPageTitle.tr(), onSearchChanged: (v) => setState(() => _query = v), - searchHint: 'Search budgets', + searchHint: LocaleKeys.searchBudgets.tr(), actions: [ PageAppBarAction(icon: Icons.add, onTap: _add, primary: true), ], @@ -141,9 +143,9 @@ class _FilterChips extends StatelessWidget { return Row( children: [ - chip('All', !onlyActive, () => onChange(false)), + chip(LocaleKeys.filterAll.tr(), !onlyActive, () => onChange(false)), SizedBox(width: 8.w), - chip('Active', onlyActive, () => onChange(true)), + chip(LocaleKeys.filterActive.tr(), onlyActive, () => onChange(true)), ], ); } @@ -164,24 +166,24 @@ class _BudgetCard extends StatelessWidget { String _periodLabel() { return switch (budget.periodType) { - BudgetPeriodType.weekly => 'Weekly', - BudgetPeriodType.monthly => 'Monthly', - BudgetPeriodType.yearly => 'Yearly', - BudgetPeriodType.custom => 'Custom', + BudgetPeriodType.weekly => LocaleKeys.periodWeekly.tr(), + BudgetPeriodType.monthly => LocaleKeys.periodMonthly.tr(), + BudgetPeriodType.yearly => LocaleKeys.periodYearly.tr(), + BudgetPeriodType.custom => LocaleKeys.periodCustom.tr(), }; } String _periodShort() { return switch (budget.periodType) { - BudgetPeriodType.weekly => '/wk', - BudgetPeriodType.monthly => '/mo', - BudgetPeriodType.yearly => '/yr', + BudgetPeriodType.weekly => LocaleKeys.periodWeeklyShort.tr(), + BudgetPeriodType.monthly => LocaleKeys.periodMonthlyShort.tr(), + BudgetPeriodType.yearly => LocaleKeys.periodYearlyShort.tr(), BudgetPeriodType.custom => '', }; } String _scopeText() { - if (budget.targets.isEmpty) return 'All transactions'; + if (budget.targets.isEmpty) return LocaleKeys.scopeAllTransactions.tr(); final byType = {}; for (final t in budget.targets) { byType[t.type] = (byType[t.type] ?? 0) + 1; @@ -189,11 +191,11 @@ class _BudgetCard extends StatelessWidget { final parts = byType.entries.map((e) { final label = switch (e.key) { BudgetTargetType.category => - '${e.value} ${e.value == 1 ? 'category' : 'categories'}', + '${e.value} ${e.value == 1 ? LocaleKeys.targetTypeCategorySingular.tr() : LocaleKeys.categories.tr()}', BudgetTargetType.wallet => - '${e.value} ${e.value == 1 ? 'wallet' : 'wallets'}', + '${e.value} ${e.value == 1 ? LocaleKeys.targetTypeWalletSingular.tr() : LocaleKeys.wallets.tr()}', BudgetTargetType.group => - '${e.value} ${e.value == 1 ? 'group' : 'groups'}', + '${e.value} ${e.value == 1 ? LocaleKeys.targetTypeGroupSingular.tr() : LocaleKeys.groups.tr()}', }; return label; }); @@ -246,9 +248,9 @@ class _BudgetCard extends StatelessWidget { padding: EdgeInsets.zero, iconSize: 18.sp, itemBuilder: (_) => [ - const PopupMenuItem(value: 'edit', child: Text('Edit')), - const PopupMenuItem( - value: 'delete', child: Text('Delete')), + PopupMenuItem(value: 'edit', child: Text(LocaleKeys.edit.tr())), + PopupMenuItem( + value: 'delete', child: Text(LocaleKeys.delete.tr())), ], onSelected: (v) { if (v == 'edit') onEdit(); @@ -275,7 +277,7 @@ class _BudgetCard extends StatelessWidget { ), if (!budget.isActive) ...[ SizedBox(width: 8.w), - _MiniChip(label: 'Inactive', muted: true), + _MiniChip(label: LocaleKeys.statusInactive.tr(), muted: true), ], ], ), @@ -330,7 +332,7 @@ class _EmptyState extends StatelessWidget { Icon(Icons.savings_outlined, size: 56.sp, color: tones.textMuted), SizedBox(height: 16.h), Text( - 'No budgets yet', + LocaleKeys.noBudgetsYet.tr(), style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w700, @@ -339,7 +341,7 @@ class _EmptyState extends StatelessWidget { ), SizedBox(height: 6.h), Text( - 'Track your spending against a target.\nSet a limit and we\'ll watch it for you.', + LocaleKeys.noBudgetsDescription.tr(), textAlign: TextAlign.center, style: TextStyle( fontSize: 13.sp, @@ -350,7 +352,7 @@ class _EmptyState extends StatelessWidget { FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.add), - label: const Text('Create budget'), + label: Text(LocaleKeys.createBudget.tr()), ), ], ), @@ -371,8 +373,8 @@ class _NoMatches extends StatelessWidget { child: Center( child: Text( query.isEmpty - ? 'No budgets match this filter.' - : 'No matches for "$query".', + ? LocaleKeys.noBudgetsMatchFilter.tr() + : LocaleKeys.noMatchesForQuery.tr().replaceFirst('{0}', query), textAlign: TextAlign.center, style: TextStyle(fontSize: 14.sp, color: tones.textSecondary), ), diff --git a/lib/presentation/utils/custom_drawer.dart b/lib/presentation/utils/custom_drawer.dart index 0c894c62..41b919c2 100644 --- a/lib/presentation/utils/custom_drawer.dart +++ b/lib/presentation/utils/custom_drawer.dart @@ -23,17 +23,18 @@ import 'package:trakli/presentation/utils/premium_tile.dart'; import 'package:trakli/presentation/widgets/database_viewer.dart'; import 'package:url_launcher/url_launcher.dart'; -const String _supportEmail = 'support@trakli.app'; +// Note: supportEmail is localized, see _launchSupportEmail method class CustomDrawer extends StatelessWidget { const CustomDrawer({super.key}); Future _launchSupportEmail(BuildContext context) async { + final supportEmail = LocaleKeys.supportEmail.tr(); final Uri emailUri = Uri( scheme: 'mailto', - path: _supportEmail, + path: supportEmail, queryParameters: { - 'subject': 'Trakli Support Request', + 'subject': LocaleKeys.supportEmailSubject.tr(), }, ); @@ -43,21 +44,21 @@ class CustomDrawer extends StatelessWidget { mode: LaunchMode.externalApplication, ); if (!launched && context.mounted) { - await _copyEmailAndShowSnackbar(context); + await _copyEmailAndShowSnackbar(context, supportEmail); } } catch (e) { if (context.mounted) { - await _copyEmailAndShowSnackbar(context); + await _copyEmailAndShowSnackbar(context, supportEmail); } } } - Future _copyEmailAndShowSnackbar(BuildContext context) async { - await Clipboard.setData(const ClipboardData(text: _supportEmail)); + Future _copyEmailAndShowSnackbar(BuildContext context, String email) async { + await Clipboard.setData(ClipboardData(text: email)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('$_supportEmail (copied to clipboard)'), + SnackBar( + content: Text(LocaleKeys.supportEmailCopied.tr().replaceFirst('{0}', email)), ), ); } @@ -93,7 +94,7 @@ class CustomDrawer extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - user?.fullName ?? 'Trakli', + user?.fullName ?? LocaleKeys.defaultUserName.tr(), style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w800, @@ -134,7 +135,7 @@ class CustomDrawer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sectionLabel(context, 'EVERYDAY'), + _sectionLabel(context, LocaleKeys.drawerEveryday.tr()), _listItem( context, onTap: () { @@ -174,7 +175,7 @@ class CustomDrawer extends StatelessWidget { subtitle: LocaleKeys.categoryDesc.tr(), ), SizedBox(height: 8.h), - _sectionLabel(context, 'ORGANIZE'), + _sectionLabel(context, LocaleKeys.drawerOrganize.tr()), _listItem( context, onTap: () { @@ -214,7 +215,7 @@ class CustomDrawer extends StatelessWidget { SizedBox(width: 14.w), Expanded( child: Text( - 'Budgets', + LocaleKeys.drawerBudgets.tr(), style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, @@ -264,7 +265,7 @@ class CustomDrawer extends StatelessWidget { ), ), SizedBox(height: 8.h), - _sectionLabel(context, 'MORE'), + _sectionLabel(context, LocaleKeys.drawerMore.tr()), _listItem( context, onTap: () { diff --git a/lib/presentation/utils/dialogs.dart b/lib/presentation/utils/dialogs.dart index 97a27a72..16107591 100644 --- a/lib/presentation/utils/dialogs.dart +++ b/lib/presentation/utils/dialogs.dart @@ -55,3 +55,55 @@ Future showDeleteConfirmationDialog( return result ?? false; } + +Future showConfirmationDialog( + BuildContext context, { + required String title, + required String message, + required String confirmText, + Color confirmColor = Colors.blue, +}) async { + final result = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + content: Text( + message, + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF576760), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + LocaleKeys.cancel.tr(), + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF576760), + ), + ), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + style: FilledButton.styleFrom(backgroundColor: confirmColor), + child: Text( + confirmText, + style: TextStyle(fontSize: 14.sp), + ), + ), + ], + ); + }, + ); + + return result ?? false; +} diff --git a/lib/presentation/utils/helpers.dart b/lib/presentation/utils/helpers.dart index 2c48577c..e2a12ad5 100644 --- a/lib/presentation/utils/helpers.dart +++ b/lib/presentation/utils/helpers.dart @@ -338,7 +338,8 @@ class CropAspectRatioPresetCustom implements CropAspectRatioPresetData { void showSnackBar({ required dynamic message, - Color backgroundColor = const Color(0xFFEB5757), + bool isSuccess = false, + Color? backgroundColor, Color textColor = Colors.white, double fontSize = 16, bool isFloating = true, @@ -346,10 +347,11 @@ void showSnackBar({ }) { final String messageText = message is Failure ? message.customMessage : message.toString(); + final bgColor = backgroundColor ?? (isSuccess ? appPrimaryColor : appDangerColor); scaffoldMessengerKey.currentState?.showSnackBar( SnackBar( - backgroundColor: backgroundColor, + backgroundColor: bgColor, behavior: isFloating ? SnackBarBehavior.floating : SnackBarBehavior.fixed, shape: borderRadius != null ? RoundedRectangleBorder( @@ -386,6 +388,9 @@ void showSnackBar({ ); } +String formatDateYmd(DateTime date) => + '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + Widget bulletPoint(BuildContext context, String text) { return Row( spacing: 4.w, diff --git a/pubspec.lock b/pubspec.lock index 93295228..43d0659f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -1276,10 +1276,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -2150,5 +2150,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.32.0" From cd52f769714dad05ec594418d7a9143aa4b3077b Mon Sep 17 00:00:00 2001 From: Fuh Austin Date: Wed, 27 May 2026 00:51:13 +0100 Subject: [PATCH 19/21] refactor(database): Rename budgetables table to budget targets --- lib/data/database/app_database.dart | 8 +- lib/data/database/app_database.g.dart | 219 +++++++++--------- .../{budgetables.dart => budget_targets.dart} | 4 +- .../budget/budget_local_datasource.dart | 24 +- lib/data/mappers/budget_mapper.dart | 2 +- lib/data/sync/budget_sync_handler.dart | 10 +- lib/di/injection.config.dart | 112 ++++----- 7 files changed, 190 insertions(+), 189 deletions(-) rename lib/data/database/tables/{budgetables.dart => budget_targets.dart} (86%) diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index dbc95b2f..2e31d2a8 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -29,7 +29,7 @@ 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 'package:trakli/data/database/tables/budgets.dart'; -import 'package:trakli/data/database/tables/budgetables.dart'; +import 'package:trakli/data/database/tables/budget_targets.dart'; import 'package:trakli/data/database/tables/budget_period_states.dart'; import 'app_database.steps.dart'; @@ -51,7 +51,7 @@ part 'app_database.g.dart'; MediaFiles, Transfers, Budgets, - Budgetables, + BudgetTargets, BudgetPeriodStates, ]) class AppDatabase extends _$AppDatabase with SynchronizerDb { @@ -78,7 +78,7 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb { } if (from < 5 && to >= 5) { await m.createTable(budgets); - await m.createTable(budgetables); + await m.createTable(budgetTargets); await m.createTable(budgetPeriodStates); } }, @@ -285,7 +285,7 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb { await notifications.deleteAll(); await mediaFiles.deleteAll(); await transfers.deleteAll(); - await budgetables.deleteAll(); + await budgetTargets.deleteAll(); await budgetPeriodStates.deleteAll(); await budgets.deleteAll(); } diff --git a/lib/data/database/app_database.g.dart b/lib/data/database/app_database.g.dart index be453b9b..beba7d60 100644 --- a/lib/data/database/app_database.g.dart +++ b/lib/data/database/app_database.g.dart @@ -7354,12 +7354,12 @@ class BudgetsCompanion extends UpdateCompanion { } } -class $BudgetablesTable extends Budgetables - with TableInfo<$BudgetablesTable, Budgetable> { +class $BudgetTargetsTable extends BudgetTargets + with TableInfo<$BudgetTargetsTable, BudgetTarget> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $BudgetablesTable(this.attachedDatabase, [this._alias]); + $BudgetTargetsTable(this.attachedDatabase, [this._alias]); @override late final GeneratedColumn budgetClientId = GeneratedColumn( 'budget_client_id', aliasedName, false, @@ -7372,7 +7372,7 @@ class $BudgetablesTable extends Budgetables targetType = GeneratedColumn('target_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) .withConverter( - $BudgetablesTable.$convertertargetType); + $BudgetTargetsTable.$convertertargetType); @override late final GeneratedColumn targetClientId = GeneratedColumn( 'target_client_id', aliasedName, false, @@ -7384,17 +7384,17 @@ class $BudgetablesTable extends Budgetables String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'budgetables'; + static const String $name = 'budget_targets'; @override Set get $primaryKey => {budgetClientId, targetType, targetClientId}; @override - Budgetable map(Map data, {String? tablePrefix}) { + BudgetTarget map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return Budgetable( + return BudgetTarget( budgetClientId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}budget_client_id'])!, - targetType: $BudgetablesTable.$convertertargetType.fromSql( + targetType: $BudgetTargetsTable.$convertertargetType.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}target_type'])!), targetClientId: attachedDatabase.typeMapping.read( @@ -7403,8 +7403,8 @@ class $BudgetablesTable extends Budgetables } @override - $BudgetablesTable createAlias(String alias) { - return $BudgetablesTable(attachedDatabase, alias); + $BudgetTargetsTable createAlias(String alias) { + return $BudgetTargetsTable(attachedDatabase, alias); } static JsonTypeConverter2 @@ -7412,11 +7412,11 @@ class $BudgetablesTable extends Budgetables const EnumNameConverter(BudgetTargetType.values); } -class Budgetable extends DataClass implements Insertable { +class BudgetTarget extends DataClass implements Insertable { final String budgetClientId; final BudgetTargetType targetType; final String targetClientId; - const Budgetable( + const BudgetTarget( {required this.budgetClientId, required this.targetType, required this.targetClientId}); @@ -7426,26 +7426,26 @@ class Budgetable extends DataClass implements Insertable { map['budget_client_id'] = Variable(budgetClientId); { map['target_type'] = Variable( - $BudgetablesTable.$convertertargetType.toSql(targetType)); + $BudgetTargetsTable.$convertertargetType.toSql(targetType)); } map['target_client_id'] = Variable(targetClientId); return map; } - BudgetablesCompanion toCompanion(bool nullToAbsent) { - return BudgetablesCompanion( + BudgetTargetsCompanion toCompanion(bool nullToAbsent) { + return BudgetTargetsCompanion( budgetClientId: Value(budgetClientId), targetType: Value(targetType), targetClientId: Value(targetClientId), ); } - factory Budgetable.fromJson(Map json, + factory BudgetTarget.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return Budgetable( + return BudgetTarget( budgetClientId: serializer.fromJson(json['budgetClientId']), - targetType: $BudgetablesTable.$convertertargetType + targetType: $BudgetTargetsTable.$convertertargetType .fromJson(serializer.fromJson(json['targetType'])), targetClientId: serializer.fromJson(json['targetClientId']), ); @@ -7456,22 +7456,22 @@ class Budgetable extends DataClass implements Insertable { return { 'budgetClientId': serializer.toJson(budgetClientId), 'targetType': serializer.toJson( - $BudgetablesTable.$convertertargetType.toJson(targetType)), + $BudgetTargetsTable.$convertertargetType.toJson(targetType)), 'targetClientId': serializer.toJson(targetClientId), }; } - Budgetable copyWith( + BudgetTarget copyWith( {String? budgetClientId, BudgetTargetType? targetType, String? targetClientId}) => - Budgetable( + BudgetTarget( budgetClientId: budgetClientId ?? this.budgetClientId, targetType: targetType ?? this.targetType, targetClientId: targetClientId ?? this.targetClientId, ); - Budgetable copyWithCompanion(BudgetablesCompanion data) { - return Budgetable( + BudgetTarget copyWithCompanion(BudgetTargetsCompanion data) { + return BudgetTarget( budgetClientId: data.budgetClientId.present ? data.budgetClientId.value : this.budgetClientId, @@ -7485,7 +7485,7 @@ class Budgetable extends DataClass implements Insertable { @override String toString() { - return (StringBuffer('Budgetable(') + return (StringBuffer('BudgetTarget(') ..write('budgetClientId: $budgetClientId, ') ..write('targetType: $targetType, ') ..write('targetClientId: $targetClientId') @@ -7498,24 +7498,24 @@ class Budgetable extends DataClass implements Insertable { @override bool operator ==(Object other) => identical(this, other) || - (other is Budgetable && + (other is BudgetTarget && other.budgetClientId == this.budgetClientId && other.targetType == this.targetType && other.targetClientId == this.targetClientId); } -class BudgetablesCompanion extends UpdateCompanion { +class BudgetTargetsCompanion extends UpdateCompanion { final Value budgetClientId; final Value targetType; final Value targetClientId; final Value rowid; - const BudgetablesCompanion({ + const BudgetTargetsCompanion({ this.budgetClientId = const Value.absent(), this.targetType = const Value.absent(), this.targetClientId = const Value.absent(), this.rowid = const Value.absent(), }); - BudgetablesCompanion.insert({ + BudgetTargetsCompanion.insert({ required String budgetClientId, required BudgetTargetType targetType, required String targetClientId, @@ -7523,7 +7523,7 @@ class BudgetablesCompanion extends UpdateCompanion { }) : budgetClientId = Value(budgetClientId), targetType = Value(targetType), targetClientId = Value(targetClientId); - static Insertable custom({ + static Insertable custom({ Expression? budgetClientId, Expression? targetType, Expression? targetClientId, @@ -7537,12 +7537,12 @@ class BudgetablesCompanion extends UpdateCompanion { }); } - BudgetablesCompanion copyWith( + BudgetTargetsCompanion copyWith( {Value? budgetClientId, Value? targetType, Value? targetClientId, Value? rowid}) { - return BudgetablesCompanion( + return BudgetTargetsCompanion( budgetClientId: budgetClientId ?? this.budgetClientId, targetType: targetType ?? this.targetType, targetClientId: targetClientId ?? this.targetClientId, @@ -7558,7 +7558,7 @@ class BudgetablesCompanion extends UpdateCompanion { } if (targetType.present) { map['target_type'] = Variable( - $BudgetablesTable.$convertertargetType.toSql(targetType.value)); + $BudgetTargetsTable.$convertertargetType.toSql(targetType.value)); } if (targetClientId.present) { map['target_client_id'] = Variable(targetClientId.value); @@ -7571,7 +7571,7 @@ class BudgetablesCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('BudgetablesCompanion(') + return (StringBuffer('BudgetTargetsCompanion(') ..write('budgetClientId: $budgetClientId, ') ..write('targetType: $targetType, ') ..write('targetClientId: $targetClientId, ') @@ -8216,7 +8216,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $MediaFilesTable mediaFiles = $MediaFilesTable(this); late final $TransfersTable transfers = $TransfersTable(this); late final $BudgetsTable budgets = $BudgetsTable(this); - late final $BudgetablesTable budgetables = $BudgetablesTable(this); + late final $BudgetTargetsTable budgetTargets = $BudgetTargetsTable(this); late final $BudgetPeriodStatesTable budgetPeriodStates = $BudgetPeriodStatesTable(this); @override @@ -8238,7 +8238,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { mediaFiles, transfers, budgets, - budgetables, + budgetTargets, budgetPeriodStates ]; @override @@ -12808,18 +12808,18 @@ final class $$BudgetsTableReferences extends BaseReferences<_$AppDatabase, $BudgetsTable, Budget> { $$BudgetsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$BudgetablesTable, List> - _budgetablesRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable(db.budgetables, + static MultiTypedResultKey<$BudgetTargetsTable, List> + _budgetTargetsRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable(db.budgetTargets, aliasName: $_aliasNameGenerator( - db.budgets.clientId, db.budgetables.budgetClientId)); + db.budgets.clientId, db.budgetTargets.budgetClientId)); - $$BudgetablesTableProcessedTableManager get budgetablesRefs { - final manager = $$BudgetablesTableTableManager($_db, $_db.budgetables) + $$BudgetTargetsTableProcessedTableManager get budgetTargetsRefs { + final manager = $$BudgetTargetsTableTableManager($_db, $_db.budgetTargets) .filter((f) => f.budgetClientId.clientId .sqlEquals($_itemColumn('client_id')!)); - final cache = $_typedResult.readTableOrNull(_budgetablesRefsTable($_db)); + final cache = $_typedResult.readTableOrNull(_budgetTargetsRefsTable($_db)); return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } @@ -12923,19 +12923,19 @@ class $$BudgetsTableFilterComposer ColumnFilters get ownerId => $composableBuilder( column: $table.ownerId, builder: (column) => ColumnFilters(column)); - Expression budgetablesRefs( - Expression Function($$BudgetablesTableFilterComposer f) f) { - final $$BudgetablesTableFilterComposer composer = $composerBuilder( + Expression budgetTargetsRefs( + Expression Function($$BudgetTargetsTableFilterComposer f) f) { + final $$BudgetTargetsTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.clientId, - referencedTable: $db.budgetables, + referencedTable: $db.budgetTargets, getReferencedColumn: (t) => t.budgetClientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$BudgetablesTableFilterComposer( + $$BudgetTargetsTableFilterComposer( $db: $db, - $table: $db.budgetables, + $table: $db.budgetTargets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -13122,19 +13122,19 @@ class $$BudgetsTableAnnotationComposer GeneratedColumn get ownerId => $composableBuilder(column: $table.ownerId, builder: (column) => column); - Expression budgetablesRefs( - Expression Function($$BudgetablesTableAnnotationComposer a) f) { - final $$BudgetablesTableAnnotationComposer composer = $composerBuilder( + Expression budgetTargetsRefs( + Expression Function($$BudgetTargetsTableAnnotationComposer a) f) { + final $$BudgetTargetsTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.clientId, - referencedTable: $db.budgetables, + referencedTable: $db.budgetTargets, getReferencedColumn: (t) => t.budgetClientId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$BudgetablesTableAnnotationComposer( + $$BudgetTargetsTableAnnotationComposer( $db: $db, - $table: $db.budgetables, + $table: $db.budgetTargets, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -13178,7 +13178,7 @@ class $$BudgetsTableTableManager extends RootTableManager< (Budget, $$BudgetsTableReferences), Budget, PrefetchHooks Function( - {bool budgetablesRefs, bool budgetPeriodStatesRefs})> { + {bool budgetTargetsRefs, bool budgetPeriodStatesRefs})> { $$BudgetsTableTableManager(_$AppDatabase db, $BudgetsTable table) : super(TableManagerState( db: db, @@ -13294,25 +13294,25 @@ class $$BudgetsTableTableManager extends RootTableManager< (e.readTable(table), $$BudgetsTableReferences(db, table, e))) .toList(), prefetchHooksCallback: ( - {budgetablesRefs = false, budgetPeriodStatesRefs = false}) { + {budgetTargetsRefs = false, budgetPeriodStatesRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ - if (budgetablesRefs) db.budgetables, + if (budgetTargetsRefs) db.budgetTargets, if (budgetPeriodStatesRefs) db.budgetPeriodStates ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ - if (budgetablesRefs) + if (budgetTargetsRefs) await $_getPrefetchedData( + BudgetTarget>( currentTable: table, - referencedTable: - $$BudgetsTableReferences._budgetablesRefsTable(db), + referencedTable: $$BudgetsTableReferences + ._budgetTargetsRefsTable(db), managerFromTypedResult: (p0) => $$BudgetsTableReferences(db, table, p0) - .budgetablesRefs, + .budgetTargetsRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where( (e) => e.budgetClientId == item.clientId), @@ -13349,15 +13349,15 @@ typedef $$BudgetsTableProcessedTableManager = ProcessedTableManager< (Budget, $$BudgetsTableReferences), Budget, PrefetchHooks Function( - {bool budgetablesRefs, bool budgetPeriodStatesRefs})>; -typedef $$BudgetablesTableCreateCompanionBuilder = BudgetablesCompanion + {bool budgetTargetsRefs, bool budgetPeriodStatesRefs})>; +typedef $$BudgetTargetsTableCreateCompanionBuilder = BudgetTargetsCompanion Function({ required String budgetClientId, required BudgetTargetType targetType, required String targetClientId, Value rowid, }); -typedef $$BudgetablesTableUpdateCompanionBuilder = BudgetablesCompanion +typedef $$BudgetTargetsTableUpdateCompanionBuilder = BudgetTargetsCompanion Function({ Value budgetClientId, Value targetType, @@ -13365,13 +13365,14 @@ typedef $$BudgetablesTableUpdateCompanionBuilder = BudgetablesCompanion Value rowid, }); -final class $$BudgetablesTableReferences - extends BaseReferences<_$AppDatabase, $BudgetablesTable, Budgetable> { - $$BudgetablesTableReferences(super.$_db, super.$_table, super.$_typedResult); +final class $$BudgetTargetsTableReferences + extends BaseReferences<_$AppDatabase, $BudgetTargetsTable, BudgetTarget> { + $$BudgetTargetsTableReferences( + super.$_db, super.$_table, super.$_typedResult); static $BudgetsTable _budgetClientIdTable(_$AppDatabase db) => db.budgets.createAlias($_aliasNameGenerator( - db.budgetables.budgetClientId, db.budgets.clientId)); + db.budgetTargets.budgetClientId, db.budgets.clientId)); $$BudgetsTableProcessedTableManager get budgetClientId { final $_column = $_itemColumn('budget_client_id')!; @@ -13385,9 +13386,9 @@ final class $$BudgetablesTableReferences } } -class $$BudgetablesTableFilterComposer - extends Composer<_$AppDatabase, $BudgetablesTable> { - $$BudgetablesTableFilterComposer({ +class $$BudgetTargetsTableFilterComposer + extends Composer<_$AppDatabase, $BudgetTargetsTable> { + $$BudgetTargetsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -13424,9 +13425,9 @@ class $$BudgetablesTableFilterComposer } } -class $$BudgetablesTableOrderingComposer - extends Composer<_$AppDatabase, $BudgetablesTable> { - $$BudgetablesTableOrderingComposer({ +class $$BudgetTargetsTableOrderingComposer + extends Composer<_$AppDatabase, $BudgetTargetsTable> { + $$BudgetTargetsTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -13461,9 +13462,9 @@ class $$BudgetablesTableOrderingComposer } } -class $$BudgetablesTableAnnotationComposer - extends Composer<_$AppDatabase, $BudgetablesTable> { - $$BudgetablesTableAnnotationComposer({ +class $$BudgetTargetsTableAnnotationComposer + extends Composer<_$AppDatabase, $BudgetTargetsTable> { + $$BudgetTargetsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -13498,35 +13499,35 @@ class $$BudgetablesTableAnnotationComposer } } -class $$BudgetablesTableTableManager extends RootTableManager< +class $$BudgetTargetsTableTableManager extends RootTableManager< _$AppDatabase, - $BudgetablesTable, - Budgetable, - $$BudgetablesTableFilterComposer, - $$BudgetablesTableOrderingComposer, - $$BudgetablesTableAnnotationComposer, - $$BudgetablesTableCreateCompanionBuilder, - $$BudgetablesTableUpdateCompanionBuilder, - (Budgetable, $$BudgetablesTableReferences), - Budgetable, + $BudgetTargetsTable, + BudgetTarget, + $$BudgetTargetsTableFilterComposer, + $$BudgetTargetsTableOrderingComposer, + $$BudgetTargetsTableAnnotationComposer, + $$BudgetTargetsTableCreateCompanionBuilder, + $$BudgetTargetsTableUpdateCompanionBuilder, + (BudgetTarget, $$BudgetTargetsTableReferences), + BudgetTarget, PrefetchHooks Function({bool budgetClientId})> { - $$BudgetablesTableTableManager(_$AppDatabase db, $BudgetablesTable table) + $$BudgetTargetsTableTableManager(_$AppDatabase db, $BudgetTargetsTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$BudgetablesTableFilterComposer($db: db, $table: table), + $$BudgetTargetsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$BudgetablesTableOrderingComposer($db: db, $table: table), + $$BudgetTargetsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$BudgetablesTableAnnotationComposer($db: db, $table: table), + $$BudgetTargetsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value budgetClientId = const Value.absent(), Value targetType = const Value.absent(), Value targetClientId = const Value.absent(), Value rowid = const Value.absent(), }) => - BudgetablesCompanion( + BudgetTargetsCompanion( budgetClientId: budgetClientId, targetType: targetType, targetClientId: targetClientId, @@ -13538,7 +13539,7 @@ class $$BudgetablesTableTableManager extends RootTableManager< required String targetClientId, Value rowid = const Value.absent(), }) => - BudgetablesCompanion.insert( + BudgetTargetsCompanion.insert( budgetClientId: budgetClientId, targetType: targetType, targetClientId: targetClientId, @@ -13547,7 +13548,7 @@ class $$BudgetablesTableTableManager extends RootTableManager< withReferenceMapper: (p0) => p0 .map((e) => ( e.readTable(table), - $$BudgetablesTableReferences(db, table, e) + $$BudgetTargetsTableReferences(db, table, e) )) .toList(), prefetchHooksCallback: ({budgetClientId = false}) { @@ -13572,8 +13573,8 @@ class $$BudgetablesTableTableManager extends RootTableManager< currentTable: table, currentColumn: table.budgetClientId, referencedTable: - $$BudgetablesTableReferences._budgetClientIdTable(db), - referencedColumn: $$BudgetablesTableReferences + $$BudgetTargetsTableReferences._budgetClientIdTable(db), + referencedColumn: $$BudgetTargetsTableReferences ._budgetClientIdTable(db) .clientId, ) as T; @@ -13589,17 +13590,17 @@ class $$BudgetablesTableTableManager extends RootTableManager< )); } -typedef $$BudgetablesTableProcessedTableManager = ProcessedTableManager< +typedef $$BudgetTargetsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $BudgetablesTable, - Budgetable, - $$BudgetablesTableFilterComposer, - $$BudgetablesTableOrderingComposer, - $$BudgetablesTableAnnotationComposer, - $$BudgetablesTableCreateCompanionBuilder, - $$BudgetablesTableUpdateCompanionBuilder, - (Budgetable, $$BudgetablesTableReferences), - Budgetable, + $BudgetTargetsTable, + BudgetTarget, + $$BudgetTargetsTableFilterComposer, + $$BudgetTargetsTableOrderingComposer, + $$BudgetTargetsTableAnnotationComposer, + $$BudgetTargetsTableCreateCompanionBuilder, + $$BudgetTargetsTableUpdateCompanionBuilder, + (BudgetTarget, $$BudgetTargetsTableReferences), + BudgetTarget, PrefetchHooks Function({bool budgetClientId})>; typedef $$BudgetPeriodStatesTableCreateCompanionBuilder = BudgetPeriodStatesCompanion Function({ @@ -14063,8 +14064,8 @@ class $AppDatabaseManager { $$TransfersTableTableManager(_db, _db.transfers); $$BudgetsTableTableManager get budgets => $$BudgetsTableTableManager(_db, _db.budgets); - $$BudgetablesTableTableManager get budgetables => - $$BudgetablesTableTableManager(_db, _db.budgetables); + $$BudgetTargetsTableTableManager get budgetTargets => + $$BudgetTargetsTableTableManager(_db, _db.budgetTargets); $$BudgetPeriodStatesTableTableManager get budgetPeriodStates => $$BudgetPeriodStatesTableTableManager(_db, _db.budgetPeriodStates); } diff --git a/lib/data/database/tables/budgetables.dart b/lib/data/database/tables/budget_targets.dart similarity index 86% rename from lib/data/database/tables/budgetables.dart rename to lib/data/database/tables/budget_targets.dart index d5eb5eb7..cd5e6248 100644 --- a/lib/data/database/tables/budgetables.dart +++ b/lib/data/database/tables/budget_targets.dart @@ -2,8 +2,8 @@ import 'package:drift/drift.dart'; import 'package:trakli/data/database/tables/budgets.dart'; import 'package:trakli/presentation/utils/enums.dart'; -@DataClassName('Budgetable') -class Budgetables extends Table { +@DataClassName('BudgetTarget') +class BudgetTargets extends Table { TextColumn get budgetClientId => text().references(Budgets, #clientId)(); TextColumn get targetType => textEnum()(); TextColumn get targetClientId => text()(); diff --git a/lib/data/datasources/budget/budget_local_datasource.dart b/lib/data/datasources/budget/budget_local_datasource.dart index c1c066f9..c15c8c7f 100644 --- a/lib/data/datasources/budget/budget_local_datasource.dart +++ b/lib/data/datasources/budget/budget_local_datasource.dart @@ -28,12 +28,12 @@ class ResolvedBudgetTarget { abstract class BudgetLocalDataSource { Future> getAllBudgets({bool? active}); Future getBudgetByClientId(String clientId); - Future> getTargetsForBudget(String budgetClientId); + Future> getTargetsForBudget(String budgetClientId); Future> getPeriodStatesForBudget( String budgetClientId); Stream> watchAllBudgets({bool? active}); - Stream> watchTargetsForBudget(String budgetClientId); + Stream> watchTargetsForBudget(String budgetClientId); Stream> watchPeriodStatesForBudget( String budgetClientId); @@ -99,8 +99,8 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { } @override - Future> getTargetsForBudget(String budgetClientId) { - return (database.select(database.budgetables) + Future> getTargetsForBudget(String budgetClientId) { + return (database.select(database.budgetTargets) ..where((bt) => bt.budgetClientId.equals(budgetClientId))) .get(); } @@ -125,8 +125,8 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { } @override - Stream> watchTargetsForBudget(String budgetClientId) { - return (database.select(database.budgetables) + Stream> watchTargetsForBudget(String budgetClientId) { + return (database.select(database.budgetTargets) ..where((bt) => bt.budgetClientId.equals(budgetClientId))) .watch(); } @@ -188,8 +188,8 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { ); for (final t in targets) { - await database.into(database.budgetables).insert( - BudgetablesCompanion.insert( + await database.into(database.budgetTargets).insert( + BudgetTargetsCompanion.insert( budgetClientId: clientId, targetType: t.type, targetClientId: t.clientId, @@ -263,12 +263,12 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { ); if (targets != null) { - await (database.delete(database.budgetables) + await (database.delete(database.budgetTargets) ..where((bt) => bt.budgetClientId.equals(clientId))) .go(); for (final t in targets) { - await database.into(database.budgetables).insert( - BudgetablesCompanion.insert( + await database.into(database.budgetTargets).insert( + BudgetTargetsCompanion.insert( budgetClientId: clientId, targetType: t.type, targetClientId: t.clientId, @@ -329,7 +329,7 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { ..where((b) => b.clientId.equals(clientId))) .getSingle(); await database.transaction(() async { - await (database.delete(database.budgetables) + await (database.delete(database.budgetTargets) ..where((bt) => bt.budgetClientId.equals(clientId))) .go(); await (database.delete(database.budgetPeriodStates) diff --git a/lib/data/mappers/budget_mapper.dart b/lib/data/mappers/budget_mapper.dart index 69f660e5..a34c0436 100644 --- a/lib/data/mappers/budget_mapper.dart +++ b/lib/data/mappers/budget_mapper.dart @@ -38,7 +38,7 @@ class BudgetMapper { ); } - static BudgetTargetEntity targetFromDb(db.Budgetable row, {String? name}) { + static BudgetTargetEntity targetFromDb(db.BudgetTarget row, {String? name}) { return BudgetTargetEntity( type: row.targetType, clientId: row.targetClientId, diff --git a/lib/data/sync/budget_sync_handler.dart b/lib/data/sync/budget_sync_handler.dart index 9f819076..d4719061 100644 --- a/lib/data/sync/budget_sync_handler.dart +++ b/lib/data/sync/budget_sync_handler.dart @@ -87,7 +87,7 @@ class BudgetSyncHandler @override Future deleteAllLocal() async { - await db.budgetables.deleteAll(); + await db.budgetTargets.deleteAll(); await db.budgetPeriodStates.deleteAll(); await table.deleteAll(); } @@ -101,7 +101,7 @@ class BudgetSyncHandler @override Future deleteLocal(BudgetCompleteDto entity) async { final clientId = entity.budget.clientId; - await (db.delete(db.budgetables) + await (db.delete(db.budgetTargets) ..where((t) => t.budgetClientId.equals(clientId))) .go(); await (db.delete(db.budgetPeriodStates) @@ -194,7 +194,7 @@ class BudgetSyncHandler await table.insertOnConflictUpdate(companion); if (entity.targets.isNotEmpty || _shouldResetTargets(entity)) { - await (db.delete(db.budgetables) + await (db.delete(db.budgetTargets) ..where((t) => t.budgetClientId.equals(budget.clientId))) .go(); for (final t in entity.targets) { @@ -202,8 +202,8 @@ class BudgetSyncHandler if (targetClientId == null || targetClientId.isEmpty) { continue; } - await db.into(db.budgetables).insert( - BudgetablesCompanion.insert( + await db.into(db.budgetTargets).insert( + BudgetTargetsCompanion.insert( budgetClientId: budget.clientId, targetType: t.type, targetClientId: targetClientId, diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index d0328abf..01106f4e 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -252,8 +252,8 @@ _i174.GetIt $initGetIt( gh.factory<_i624.OAuthService>(() => _i624.OAuthService()); gh.factory<_i1041.SyncCubit>(() => _i1041.SyncCubit()); gh.factory<_i363.StatisticsFilterCubit>(() => _i363.StatisticsFilterCubit()); - gh.singleton<_i196.FeatureRemoteConfig>(() => _i196.FeatureRemoteConfig()); gh.singleton<_i957.SyncService>(() => _i957.SyncService()); + gh.singleton<_i196.FeatureRemoteConfig>(() => _i196.FeatureRemoteConfig()); gh.lazySingleton<_i877.SyncDependencyManagerBase>( () => syncModule.provideSyncDependencyManager()); gh.lazySingleton<_i627.ThemeCubit>(() => _i627.ThemeCubit()); @@ -384,12 +384,12 @@ _i174.GetIt $initGetIt( () => _i760.BudgetRemoteDataSourceImpl(dio: gh<_i361.Dio>())); gh.factory<_i961.GetCategoriesUseCase>( () => _i961.GetCategoriesUseCase(gh<_i410.CategoryRepository>())); - gh.factory<_i292.DeleteCategoryUseCase>( - () => _i292.DeleteCategoryUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i986.UpdateCategoryUseCase>( () => _i986.UpdateCategoryUseCase(gh<_i410.CategoryRepository>())); gh.factory<_i445.AddCategoryUseCase>( () => _i445.AddCategoryUseCase(gh<_i410.CategoryRepository>())); + gh.factory<_i292.DeleteCategoryUseCase>( + () => _i292.DeleteCategoryUseCase(gh<_i410.CategoryRepository>())); gh.singleton<_i11.CloudBenefitRepository>(() => _i415.CloudBenefitRepositoryImpl( gh<_i61.CloudBenefitRemoteDataSource>())); @@ -470,20 +470,20 @@ _i174.GetIt $initGetIt( gh<_i961.GetCategoriesUseCase>(), gh<_i500.ListenToCategoriesUseCase>(), )); - gh.factory<_i444.StreamAuthStatus>( - () => _i444.StreamAuthStatus(gh<_i800.AuthRepository>())); - gh.factory<_i880.GetLoggedInUser>( - () => _i880.GetLoggedInUser(gh<_i800.AuthRepository>())); gh.factory<_i723.LoginWithPhonePassword>( () => _i723.LoginWithPhonePassword(gh<_i800.AuthRepository>())); - gh.factory<_i768.LoginWithEmailPassword>( - () => _i768.LoginWithEmailPassword(gh<_i800.AuthRepository>())); - gh.factory<_i42.LoginByEmailUsecase>( - () => _i42.LoginByEmailUsecase(gh<_i800.AuthRepository>())); gh.factory<_i2.OnboardingCompleted>( () => _i2.OnboardingCompleted(gh<_i800.AuthRepository>())); gh.factory<_i498.LoginByPhoneUsecase>( () => _i498.LoginByPhoneUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i880.GetLoggedInUser>( + () => _i880.GetLoggedInUser(gh<_i800.AuthRepository>())); + gh.factory<_i768.LoginWithEmailPassword>( + () => _i768.LoginWithEmailPassword(gh<_i800.AuthRepository>())); + gh.factory<_i42.LoginByEmailUsecase>( + () => _i42.LoginByEmailUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i444.StreamAuthStatus>( + () => _i444.StreamAuthStatus(gh<_i800.AuthRepository>())); gh.factory<_i828.IsOnboardingCompleted>( () => _i828.IsOnboardingCompleted(gh<_i800.AuthRepository>())); gh.lazySingleton<_i918.BudgetSyncHandler>(() => _i918.BudgetSyncHandler( @@ -497,14 +497,14 @@ _i174.GetIt $initGetIt( db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); + gh.factory<_i929.GetImportSessionsUseCase>( + () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); + gh.factory<_i36.ConfirmSessionUseCase>( + () => _i36.ConfirmSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i661.GetImportSessionUseCase>( () => _i661.GetImportSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i60.AnalyzeDocumentUseCase>( () => _i60.AnalyzeDocumentUseCase(gh<_i32.ImportRepository>())); - gh.factory<_i36.ConfirmSessionUseCase>( - () => _i36.ConfirmSessionUseCase(gh<_i32.ImportRepository>())); - gh.factory<_i929.GetImportSessionsUseCase>( - () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); gh.lazySingleton<_i340.BudgetRepository>(() => _i81.BudgetRepositoryImpl( syncHandler: gh<_i918.BudgetSyncHandler>(), localDataSource: gh<_i293.BudgetLocalDataSource>(), @@ -512,12 +512,12 @@ _i174.GetIt $initGetIt( db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i911.UpdatePartyUseCase>( - () => _i911.UpdatePartyUseCase(gh<_i661.PartyRepository>())); - gh.factory<_i84.AddPartyUseCase>( - () => _i84.AddPartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i56.DeletePartyUseCase>( () => _i56.DeletePartyUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i84.AddPartyUseCase>( + () => _i84.AddPartyUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i911.UpdatePartyUseCase>( + () => _i911.UpdatePartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i12.GetPartiesUseCase>( () => _i12.GetPartiesUseCase(gh<_i661.PartyRepository>())); gh.factory<_i714.ListenToPartiesUseCase>( @@ -539,40 +539,40 @@ _i174.GetIt $initGetIt( )); gh.factory<_i88.BenefitsCubit>( () => _i88.BenefitsCubit(gh<_i61.FetchBenefits>())); - gh.factory<_i684.DeleteAccountUseCase>( - () => _i684.DeleteAccountUseCase(gh<_i800.AuthRepository>())); gh.factory<_i640.LogoutUsecase>( () => _i640.LogoutUsecase(gh<_i800.AuthRepository>())); + gh.factory<_i684.DeleteAccountUseCase>( + () => _i684.DeleteAccountUseCase(gh<_i800.AuthRepository>())); gh.lazySingleton<_i368.WalletRepository>(() => _i305.WalletRepositoryImpl( syncHandler: gh<_i849.WalletSyncHandler>(), localDataSource: gh<_i849.WalletLocalDataSource>(), db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i947.GetAllTransactionsUseCase>( - () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i163.DeleteTransactionUseCase>( + () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i973.ListenToTransactionsUseCase>(() => _i973.ListenToTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i241.UpdateTransactionUseCase>( () => _i241.UpdateTransactionUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i163.DeleteTransactionUseCase>( - () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i837.MarkNotificationAsReadUseCase>(() => - _i837.MarkNotificationAsReadUseCase(gh<_i965.NotificationRepository>())); + gh.factory<_i947.GetAllTransactionsUseCase>( + () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i422.GetNotificationsUseCase>( () => _i422.GetNotificationsUseCase(gh<_i965.NotificationRepository>())); + gh.factory<_i837.MarkNotificationAsReadUseCase>(() => + _i837.MarkNotificationAsReadUseCase(gh<_i965.NotificationRepository>())); gh.lazySingleton<_i957.GroupRepository>(() => _i875.GroupRepositoryImpl( syncHandler: gh<_i235.GroupSyncHandler>(), localDataSource: gh<_i873.GroupLocalDataSource>(), db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i62.DeleteWalletUseCase>( - () => _i62.DeleteWalletUseCase(gh<_i368.WalletRepository>())); - gh.factory<_i418.UpdateWalletUseCase>( - () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i80.AddWalletUseCase>( () => _i80.AddWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i418.UpdateWalletUseCase>( + () => _i418.UpdateWalletUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i62.DeleteWalletUseCase>( + () => _i62.DeleteWalletUseCase(gh<_i368.WalletRepository>())); gh.factory<_i713.GetWalletsUseCase>( () => _i713.GetWalletsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i314.FetchSubscriptionPlans>( @@ -583,16 +583,16 @@ _i174.GetIt $initGetIt( gh<_i79.TransactionRemoteDataSource>(), gh<_i893.TransactionSyncHandler>(), )); - gh.factory<_i436.UpdateConfigUseCase>( - () => _i436.UpdateConfigUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i132.GetConfigsUseCase>( + () => _i132.GetConfigsUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i608.ListenToConfigsUseCase>( () => _i608.ListenToConfigsUseCase(gh<_i899.ConfigRepository>())); - gh.factory<_i833.SaveConfigUseCase>( - () => _i833.SaveConfigUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i933.GetConfigUseCase>( () => _i933.GetConfigUseCase(gh<_i899.ConfigRepository>())); - gh.factory<_i132.GetConfigsUseCase>( - () => _i132.GetConfigsUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i833.SaveConfigUseCase>( + () => _i833.SaveConfigUseCase(gh<_i899.ConfigRepository>())); + gh.factory<_i436.UpdateConfigUseCase>( + () => _i436.UpdateConfigUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i536.DeleteConfigUseCase>( () => _i536.DeleteConfigUseCase(gh<_i899.ConfigRepository>())); gh.lazySingleton<_i55.TransferRepository>(() => _i268.TransferRepositoryImpl( @@ -609,24 +609,24 @@ _i174.GetIt $initGetIt( localDataSource: gh<_i900.ExchangeRateLocalDataSource>(), configRepository: gh<_i899.ConfigRepository>(), )); - gh.factory<_i82.ListenToWalletsUseCase>( - () => _i82.ListenToWalletsUseCase(gh<_i368.WalletRepository>())); gh.factory<_i225.EnsureDefaultWalletExistsUseCase>(() => _i225.EnsureDefaultWalletExistsUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i82.ListenToWalletsUseCase>( + () => _i82.ListenToWalletsUseCase(gh<_i368.WalletRepository>())); + gh.factory<_i524.LoginWithEmailUseCase>( + () => _i524.LoginWithEmailUseCase(gh<_i800.AuthRepository>())); gh.factory<_i705.RegisterUseCase>( () => _i705.RegisterUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i400.LoginWithPhoneUseCase>( - () => _i400.LoginWithPhoneUseCase(gh<_i800.AuthRepository>())); gh.factory<_i402.GetOtpCodeUseCase>( () => _i402.GetOtpCodeUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i400.LoginWithPhoneUseCase>( + () => _i400.LoginWithPhoneUseCase(gh<_i800.AuthRepository>())); + gh.factory<_i542.PasswordResetCodeUseCase>( + () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); gh.factory<_i494.PasswordResetUseCase>( () => _i494.PasswordResetUseCase(gh<_i800.AuthRepository>())); gh.factory<_i100.VerifyEmailUseCase>( () => _i100.VerifyEmailUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i542.PasswordResetCodeUseCase>( - () => _i542.PasswordResetCodeUseCase(gh<_i800.AuthRepository>())); - gh.factory<_i524.LoginWithEmailUseCase>( - () => _i524.LoginWithEmailUseCase(gh<_i800.AuthRepository>())); gh.factory<_i841.PartyCubit>(() => _i841.PartyCubit( getPartiesUseCase: gh<_i12.GetPartiesUseCase>(), addPartyUseCase: gh<_i84.AddPartyUseCase>(), @@ -673,21 +673,21 @@ _i174.GetIt $initGetIt( ensureDefaultWalletExistsUseCase: gh<_i225.EnsureDefaultWalletExistsUseCase>(), )); + gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => + _i1004.CreateTransferWithTransactionsUseCase( + gh<_i55.TransferRepository>())); gh.factory<_i453.AddTransferUseCase>( () => _i453.AddTransferUseCase(gh<_i55.TransferRepository>())); gh.factory<_i744.ListenToTransfersUseCase>( () => _i744.ListenToTransfersUseCase(gh<_i55.TransferRepository>())); - gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => - _i1004.CreateTransferWithTransactionsUseCase( - gh<_i55.TransferRepository>())); gh.factory<_i397.ListenExchangeRate>( () => _i397.ListenExchangeRate(gh<_i1057.ExchangeRateRepository>())); - gh.factory<_i146.ListenToGroupsUseCase>( - () => _i146.ListenToGroupsUseCase(gh<_i957.GroupRepository>())); - gh.factory<_i982.GetGroupsUseCase>( - () => _i982.GetGroupsUseCase(gh<_i957.GroupRepository>())); gh.factory<_i759.DeleteGroupUseCase>( () => _i759.DeleteGroupUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i982.GetGroupsUseCase>( + () => _i982.GetGroupsUseCase(gh<_i957.GroupRepository>())); + gh.factory<_i146.ListenToGroupsUseCase>( + () => _i146.ListenToGroupsUseCase(gh<_i957.GroupRepository>())); gh.factory<_i353.AddGroupUseCase>( () => _i353.AddGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i820.UpdateGroupUseCase>( @@ -740,14 +740,14 @@ _i174.GetIt $initGetIt( gh<_i118.TransactionRepository>(), gh<_i1057.ExchangeRateRepository>(), )); - gh.factory<_i706.DeleteMediaUseCase>( - () => _i706.DeleteMediaUseCase(gh<_i442.MediaRepository>())); - gh.factory<_i843.AddMediaToTransactionUseCase>( - () => _i843.AddMediaToTransactionUseCase(gh<_i442.MediaRepository>())); gh.factory<_i150.GetFileContentUseCase>( () => _i150.GetFileContentUseCase(gh<_i442.MediaRepository>())); + gh.factory<_i706.DeleteMediaUseCase>( + () => _i706.DeleteMediaUseCase(gh<_i442.MediaRepository>())); gh.factory<_i1026.GetMediaForTransactionUseCase>( () => _i1026.GetMediaForTransactionUseCase(gh<_i442.MediaRepository>())); + gh.factory<_i843.AddMediaToTransactionUseCase>( + () => _i843.AddMediaToTransactionUseCase(gh<_i442.MediaRepository>())); gh.factory<_i484.CurrencyCubit>(() => _i484.CurrencyCubit( gh<_i933.GetConfigUseCase>(), gh<_i833.SaveConfigUseCase>(), From 91dc7fa7631e34fc935ba8d1112c807800c51b6b Mon Sep 17 00:00:00 2001 From: Fuh Austin Date: Wed, 27 May 2026 10:47:10 +0100 Subject: [PATCH 20/21] refactor(architecture): Add use cases for AI Chat and Budget features Extract use cases for AI Chat and Budget features to align with established architectural patterns used across other features. Update AiChatCubit and BudgetCubit to inject use cases instead of repositories, replacing all repository method calls with use case invocations. Regenerate DI configuration to auto-register new use cases. This eliminates architectural inconsistency and improves maintainability by ensuring all features follow the unified UseCase pattern with dependency injection. --- lib/di/injection.config.dart | 81 ++++++++++- .../usecases/ai/check_health_usecase.dart | 17 +++ .../usecases/ai/create_session_usecase.dart | 36 +++++ .../usecases/ai/delete_session_usecase.dart | 22 +++ .../usecases/ai/get_session_usecase.dart | 23 +++ .../usecases/ai/list_sessions_usecase.dart | 18 +++ .../usecases/ai/send_message_usecase.dart | 34 +++++ .../budget/close_budget_period_usecase.dart | 23 +++ .../budget/delete_budget_usecase.dart | 22 +++ .../budget/fetch_budget_progress_usecase.dart | 25 ++++ .../fetch_budget_transactions_usecase.dart | 29 ++++ .../budget/get_all_budgets_usecase.dart | 25 ++++ .../usecases/budget/get_budget_usecase.dart | 23 +++ .../budget/insert_budget_usecase.dart | 61 ++++++++ .../budget/listen_to_budgets_usecase.dart | 25 ++++ .../listen_to_period_states_usecase.dart | 27 ++++ .../budget/listen_to_targets_usecase.dart | 25 ++++ .../budget/refresh_period_states_usecase.dart | 17 +++ .../budget/update_budget_usecase.dart | 64 ++++++++ .../ai_chat/cubit/ai_chat_cubit.dart | 43 ++++-- .../budget/cubit/budget_cubit.dart | 137 +++++++++++++----- 21 files changed, 722 insertions(+), 55 deletions(-) create mode 100644 lib/domain/usecases/ai/check_health_usecase.dart create mode 100644 lib/domain/usecases/ai/create_session_usecase.dart create mode 100644 lib/domain/usecases/ai/delete_session_usecase.dart create mode 100644 lib/domain/usecases/ai/get_session_usecase.dart create mode 100644 lib/domain/usecases/ai/list_sessions_usecase.dart create mode 100644 lib/domain/usecases/ai/send_message_usecase.dart create mode 100644 lib/domain/usecases/budget/close_budget_period_usecase.dart create mode 100644 lib/domain/usecases/budget/delete_budget_usecase.dart create mode 100644 lib/domain/usecases/budget/fetch_budget_progress_usecase.dart create mode 100644 lib/domain/usecases/budget/fetch_budget_transactions_usecase.dart create mode 100644 lib/domain/usecases/budget/get_all_budgets_usecase.dart create mode 100644 lib/domain/usecases/budget/get_budget_usecase.dart create mode 100644 lib/domain/usecases/budget/insert_budget_usecase.dart create mode 100644 lib/domain/usecases/budget/listen_to_budgets_usecase.dart create mode 100644 lib/domain/usecases/budget/listen_to_period_states_usecase.dart create mode 100644 lib/domain/usecases/budget/listen_to_targets_usecase.dart create mode 100644 lib/domain/usecases/budget/refresh_period_states_usecase.dart create mode 100644 lib/domain/usecases/budget/update_budget_usecase.dart diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index 01106f4e..3e0dc0fc 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -120,6 +120,12 @@ import '../domain/repositories/subscription_repository.dart' as _i804; import '../domain/repositories/transaction_repository.dart' as _i118; import '../domain/repositories/transfer_repository.dart' as _i55; import '../domain/repositories/wallet_repository.dart' as _i368; +import '../domain/usecases/ai/check_health_usecase.dart' as _i605; +import '../domain/usecases/ai/create_session_usecase.dart' as _i995; +import '../domain/usecases/ai/delete_session_usecase.dart' as _i236; +import '../domain/usecases/ai/get_session_usecase.dart' as _i752; +import '../domain/usecases/ai/list_sessions_usecase.dart' as _i505; +import '../domain/usecases/ai/send_message_usecase.dart' as _i308; import '../domain/usecases/app_update/check_app_update_usecase.dart' as _i150; import '../domain/usecases/auth/delete_account_usecase.dart' as _i684; import '../domain/usecases/auth/get_loggedin_user.dart' as _i880; @@ -138,6 +144,20 @@ import '../domain/usecases/auth/password_reset_usecase.dart' as _i494; import '../domain/usecases/auth/register_usecase.dart' as _i705; import '../domain/usecases/auth/stream_auth_status.dart' as _i444; import '../domain/usecases/auth/verify_email_usecase.dart' as _i100; +import '../domain/usecases/budget/close_budget_period_usecase.dart' as _i893; +import '../domain/usecases/budget/delete_budget_usecase.dart' as _i748; +import '../domain/usecases/budget/fetch_budget_progress_usecase.dart' as _i598; +import '../domain/usecases/budget/fetch_budget_transactions_usecase.dart' + as _i59; +import '../domain/usecases/budget/get_all_budgets_usecase.dart' as _i884; +import '../domain/usecases/budget/get_budget_usecase.dart' as _i102; +import '../domain/usecases/budget/insert_budget_usecase.dart' as _i363; +import '../domain/usecases/budget/listen_to_budgets_usecase.dart' as _i377; +import '../domain/usecases/budget/listen_to_period_states_usecase.dart' + as _i920; +import '../domain/usecases/budget/listen_to_targets_usecase.dart' as _i442; +import '../domain/usecases/budget/refresh_period_states_usecase.dart' as _i141; +import '../domain/usecases/budget/update_budget_usecase.dart' as _i617; import '../domain/usecases/category/add_category_usecase.dart' as _i445; import '../domain/usecases/category/delete_category_usecase.dart' as _i292; import '../domain/usecases/category/get_categories_usecase.dart' as _i961; @@ -461,8 +481,6 @@ _i174.GetIt $initGetIt( walletSyncHandler: gh<_i849.WalletSyncHandler>(), database: gh<_i704.AppDatabase>(), )); - gh.factory<_i415.AiChatCubit>( - () => _i415.AiChatCubit(gh<_i542.AiRepository>())); gh.factory<_i455.CategoryCubit>(() => _i455.CategoryCubit( gh<_i445.AddCategoryUseCase>(), gh<_i986.UpdateCategoryUseCase>(), @@ -522,6 +540,18 @@ _i174.GetIt $initGetIt( () => _i12.GetPartiesUseCase(gh<_i661.PartyRepository>())); gh.factory<_i714.ListenToPartiesUseCase>( () => _i714.ListenToPartiesUseCase(gh<_i661.PartyRepository>())); + gh.factory<_i995.CreateSessionUseCase>( + () => _i995.CreateSessionUseCase(gh<_i542.AiRepository>())); + gh.factory<_i236.DeleteSessionUseCase>( + () => _i236.DeleteSessionUseCase(gh<_i542.AiRepository>())); + gh.factory<_i752.GetSessionUseCase>( + () => _i752.GetSessionUseCase(gh<_i542.AiRepository>())); + gh.factory<_i308.SendMessageUseCase>( + () => _i308.SendMessageUseCase(gh<_i542.AiRepository>())); + gh.factory<_i605.CheckHealthUseCase>( + () => _i605.CheckHealthUseCase(gh<_i542.AiRepository>())); + gh.factory<_i505.ListSessionsUseCase>( + () => _i505.ListSessionsUseCase(gh<_i542.AiRepository>())); gh.factory<_i559.AppUpdateCubit>( () => _i559.AppUpdateCubit(gh<_i150.CheckAppUpdateUseCase>())); gh.factory<_i538.ImportCubit>(() => _i538.ImportCubit( @@ -557,6 +587,13 @@ _i174.GetIt $initGetIt( () => _i241.UpdateTransactionUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i947.GetAllTransactionsUseCase>( () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i415.AiChatCubit>(() => _i415.AiChatCubit( + listSessionsUseCase: gh<_i505.ListSessionsUseCase>(), + getSessionUseCase: gh<_i752.GetSessionUseCase>(), + createSessionUseCase: gh<_i995.CreateSessionUseCase>(), + sendMessageUseCase: gh<_i308.SendMessageUseCase>(), + deleteSessionUseCase: gh<_i236.DeleteSessionUseCase>(), + )); gh.factory<_i422.GetNotificationsUseCase>( () => _i422.GetNotificationsUseCase(gh<_i965.NotificationRepository>())); gh.factory<_i837.MarkNotificationAsReadUseCase>(() => @@ -640,8 +677,30 @@ _i174.GetIt $initGetIt( gh<_i542.PasswordResetCodeUseCase>(), gh<_i494.PasswordResetUseCase>(), )); - gh.factory<_i1064.BudgetCubit>( - () => _i1064.BudgetCubit(gh<_i340.BudgetRepository>())); + gh.factory<_i920.ListenToPeriodStatesUseCase>( + () => _i920.ListenToPeriodStatesUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i102.GetBudgetUseCase>( + () => _i102.GetBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i377.ListenToBudgetsUseCase>( + () => _i377.ListenToBudgetsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i893.CloseBudgetPeriodUseCase>( + () => _i893.CloseBudgetPeriodUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i442.ListenToTargetsUseCase>( + () => _i442.ListenToTargetsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i617.UpdateBudgetUseCase>( + () => _i617.UpdateBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i141.RefreshPeriodStatesUseCase>( + () => _i141.RefreshPeriodStatesUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i363.InsertBudgetUseCase>( + () => _i363.InsertBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i59.FetchBudgetTransactionsUseCase>( + () => _i59.FetchBudgetTransactionsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i748.DeleteBudgetUseCase>( + () => _i748.DeleteBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i598.FetchBudgetProgressUseCase>( + () => _i598.FetchBudgetProgressUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i884.GetAllBudgetsUseCase>( + () => _i884.GetAllBudgetsUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i831.RegisterCubit>(() => _i831.RegisterCubit( gh<_i705.RegisterUseCase>(), gh<_i402.GetOtpCodeUseCase>(), @@ -725,6 +784,20 @@ _i174.GetIt $initGetIt( )); gh.factory<_i977.PlansCubit>( () => _i977.PlansCubit(gh<_i314.FetchSubscriptionPlans>())); + gh.factory<_i1064.BudgetCubit>(() => _i1064.BudgetCubit( + getAllBudgetsUseCase: gh<_i884.GetAllBudgetsUseCase>(), + insertBudgetUseCase: gh<_i363.InsertBudgetUseCase>(), + updateBudgetUseCase: gh<_i617.UpdateBudgetUseCase>(), + deleteBudgetUseCase: gh<_i748.DeleteBudgetUseCase>(), + fetchBudgetProgressUseCase: gh<_i598.FetchBudgetProgressUseCase>(), + fetchBudgetTransactionsUseCase: + gh<_i59.FetchBudgetTransactionsUseCase>(), + closeBudgetPeriodUseCase: gh<_i893.CloseBudgetPeriodUseCase>(), + listenToBudgetsUseCase: gh<_i377.ListenToBudgetsUseCase>(), + listenToTargetsUseCase: gh<_i442.ListenToTargetsUseCase>(), + listenToPeriodStatesUseCase: gh<_i920.ListenToPeriodStatesUseCase>(), + refreshPeriodStatesUseCase: gh<_i141.RefreshPeriodStatesUseCase>(), + )); gh.factory<_i798.UpdateDefaultCurrencyUseCase>(() => _i798.UpdateDefaultCurrencyUseCase(gh<_i1057.ExchangeRateRepository>())); gh.factory<_i676.GroupCubit>(() => _i676.GroupCubit( diff --git a/lib/domain/usecases/ai/check_health_usecase.dart b/lib/domain/usecases/ai/check_health_usecase.dart new file mode 100644 index 00000000..00c7bf96 --- /dev/null +++ b/lib/domain/usecases/ai/check_health_usecase.dart @@ -0,0 +1,17 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +@injectable +class CheckHealthUseCase implements UseCase { + final AiRepository _repository; + + CheckHealthUseCase(this._repository); + + @override + Future> call(NoParams params) async { + return await _repository.checkHealth(); + } +} diff --git a/lib/domain/usecases/ai/create_session_usecase.dart b/lib/domain/usecases/ai/create_session_usecase.dart new file mode 100644 index 00000000..3544c76f --- /dev/null +++ b/lib/domain/usecases/ai/create_session_usecase.dart @@ -0,0 +1,36 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +class CreateSessionParams { + final String message; + final String? formatHint; + final String? title; + + const CreateSessionParams({ + required this.message, + this.formatHint, + this.title, + }); +} + +@injectable +class CreateSessionUseCase + implements UseCase { + final AiRepository _repository; + + CreateSessionUseCase(this._repository); + + @override + Future> call( + CreateSessionParams params) async { + return await _repository.createSession( + message: params.message, + formatHint: params.formatHint, + title: params.title, + ); + } +} diff --git a/lib/domain/usecases/ai/delete_session_usecase.dart b/lib/domain/usecases/ai/delete_session_usecase.dart new file mode 100644 index 00000000..7e5fcfb3 --- /dev/null +++ b/lib/domain/usecases/ai/delete_session_usecase.dart @@ -0,0 +1,22 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +class DeleteSessionParams { + final int id; + const DeleteSessionParams({required this.id}); +} + +@injectable +class DeleteSessionUseCase implements UseCase { + final AiRepository _repository; + + DeleteSessionUseCase(this._repository); + + @override + Future> call(DeleteSessionParams params) async { + return await _repository.deleteSession(params.id); + } +} diff --git a/lib/domain/usecases/ai/get_session_usecase.dart b/lib/domain/usecases/ai/get_session_usecase.dart new file mode 100644 index 00000000..b31b8f9e --- /dev/null +++ b/lib/domain/usecases/ai/get_session_usecase.dart @@ -0,0 +1,23 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +class GetSessionParams { + final int id; + const GetSessionParams({required this.id}); +} + +@injectable +class GetSessionUseCase implements UseCase { + final AiRepository _repository; + + GetSessionUseCase(this._repository); + + @override + Future> call(GetSessionParams params) async { + return await _repository.getSession(params.id); + } +} diff --git a/lib/domain/usecases/ai/list_sessions_usecase.dart b/lib/domain/usecases/ai/list_sessions_usecase.dart new file mode 100644 index 00000000..e410e1a8 --- /dev/null +++ b/lib/domain/usecases/ai/list_sessions_usecase.dart @@ -0,0 +1,18 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +@injectable +class ListSessionsUseCase implements UseCase, NoParams> { + final AiRepository _repository; + + ListSessionsUseCase(this._repository); + + @override + Future>> call(NoParams params) async { + return await _repository.listSessions(); + } +} diff --git a/lib/domain/usecases/ai/send_message_usecase.dart b/lib/domain/usecases/ai/send_message_usecase.dart new file mode 100644 index 00000000..1419c65a --- /dev/null +++ b/lib/domain/usecases/ai/send_message_usecase.dart @@ -0,0 +1,34 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/data/datasources/ai/dto/message_pair_dto.dart'; +import 'package:trakli/domain/repositories/ai_repository.dart'; + +class SendMessageParams { + final int sessionId; + final String message; + final String? formatHint; + + const SendMessageParams({ + required this.sessionId, + required this.message, + this.formatHint, + }); +} + +@injectable +class SendMessageUseCase implements UseCase { + final AiRepository _repository; + + SendMessageUseCase(this._repository); + + @override + Future> call(SendMessageParams params) async { + return await _repository.sendMessage( + sessionId: params.sessionId, + message: params.message, + formatHint: params.formatHint, + ); + } +} diff --git a/lib/domain/usecases/budget/close_budget_period_usecase.dart b/lib/domain/usecases/budget/close_budget_period_usecase.dart new file mode 100644 index 00000000..eca22fe2 --- /dev/null +++ b/lib/domain/usecases/budget/close_budget_period_usecase.dart @@ -0,0 +1,23 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class CloseBudgetPeriodParams { + final int id; + const CloseBudgetPeriodParams({required this.id}); +} + +@injectable +class CloseBudgetPeriodUseCase + implements UseCase { + final BudgetRepository _repository; + + CloseBudgetPeriodUseCase(this._repository); + + @override + Future> call(CloseBudgetPeriodParams params) async { + return await _repository.closeBudgetPeriod(params.id); + } +} diff --git a/lib/domain/usecases/budget/delete_budget_usecase.dart b/lib/domain/usecases/budget/delete_budget_usecase.dart new file mode 100644 index 00000000..052d6ea5 --- /dev/null +++ b/lib/domain/usecases/budget/delete_budget_usecase.dart @@ -0,0 +1,22 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class DeleteBudgetParams { + final String clientId; + const DeleteBudgetParams({required this.clientId}); +} + +@injectable +class DeleteBudgetUseCase implements UseCase { + final BudgetRepository _repository; + + DeleteBudgetUseCase(this._repository); + + @override + Future> call(DeleteBudgetParams params) async { + return await _repository.deleteBudget(params.clientId); + } +} diff --git a/lib/domain/usecases/budget/fetch_budget_progress_usecase.dart b/lib/domain/usecases/budget/fetch_budget_progress_usecase.dart new file mode 100644 index 00000000..fa101bd0 --- /dev/null +++ b/lib/domain/usecases/budget/fetch_budget_progress_usecase.dart @@ -0,0 +1,25 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class FetchBudgetProgressParams { + final int id; + const FetchBudgetProgressParams({required this.id}); +} + +@injectable +class FetchBudgetProgressUseCase + implements UseCase { + final BudgetRepository _repository; + + FetchBudgetProgressUseCase(this._repository); + + @override + Future> call( + FetchBudgetProgressParams params) async { + return await _repository.fetchBudgetProgress(params.id); + } +} diff --git a/lib/domain/usecases/budget/fetch_budget_transactions_usecase.dart b/lib/domain/usecases/budget/fetch_budget_transactions_usecase.dart new file mode 100644 index 00000000..7f8ff7b4 --- /dev/null +++ b/lib/domain/usecases/budget/fetch_budget_transactions_usecase.dart @@ -0,0 +1,29 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class FetchBudgetTransactionsParams { + final int id; + final int limit; + const FetchBudgetTransactionsParams({required this.id, this.limit = 50}); +} + +@injectable +class FetchBudgetTransactionsUseCase + implements + UseCase { + final BudgetRepository _repository; + + FetchBudgetTransactionsUseCase(this._repository); + + @override + Future> call( + FetchBudgetTransactionsParams params) async { + return await _repository.fetchBudgetTransactions(params.id, + limit: params.limit); + } +} diff --git a/lib/domain/usecases/budget/get_all_budgets_usecase.dart b/lib/domain/usecases/budget/get_all_budgets_usecase.dart new file mode 100644 index 00000000..a491c944 --- /dev/null +++ b/lib/domain/usecases/budget/get_all_budgets_usecase.dart @@ -0,0 +1,25 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class GetAllBudgetsParams { + final bool? active; + const GetAllBudgetsParams({this.active}); +} + +@injectable +class GetAllBudgetsUseCase + implements UseCase, GetAllBudgetsParams> { + final BudgetRepository _repository; + + GetAllBudgetsUseCase(this._repository); + + @override + Future>> call( + GetAllBudgetsParams params) async { + return await _repository.getAllBudgets(active: params.active); + } +} diff --git a/lib/domain/usecases/budget/get_budget_usecase.dart b/lib/domain/usecases/budget/get_budget_usecase.dart new file mode 100644 index 00000000..b13c8c47 --- /dev/null +++ b/lib/domain/usecases/budget/get_budget_usecase.dart @@ -0,0 +1,23 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class GetBudgetParams { + final String clientId; + const GetBudgetParams({required this.clientId}); +} + +@injectable +class GetBudgetUseCase implements UseCase { + final BudgetRepository _repository; + + GetBudgetUseCase(this._repository); + + @override + Future> call(GetBudgetParams params) async { + return await _repository.getBudget(params.clientId); + } +} diff --git a/lib/domain/usecases/budget/insert_budget_usecase.dart b/lib/domain/usecases/budget/insert_budget_usecase.dart new file mode 100644 index 00000000..563c29f4 --- /dev/null +++ b/lib/domain/usecases/budget/insert_budget_usecase.dart @@ -0,0 +1,61 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class InsertBudgetParams { + final String name; + final double amount; + final String currency; + final BudgetPeriodType periodType; + final DateTime startDate; + final DateTime? endDate; + final String? description; + final bool rolloverEnabled; + final int thresholdPercent; + final bool forecastAlertsEnabled; + final bool isActive; + final List targets; + + const InsertBudgetParams({ + required this.name, + required this.amount, + required this.currency, + required this.periodType, + required this.startDate, + this.endDate, + this.description, + this.rolloverEnabled = false, + this.thresholdPercent = 80, + this.forecastAlertsEnabled = false, + this.isActive = true, + this.targets = const [], + }); +} + +@injectable +class InsertBudgetUseCase implements UseCase { + final BudgetRepository _repository; + + InsertBudgetUseCase(this._repository); + + @override + Future> call(InsertBudgetParams params) async { + return await _repository.insertBudget( + name: params.name, + amount: params.amount, + currency: params.currency, + periodType: params.periodType, + startDate: params.startDate, + endDate: params.endDate, + description: params.description, + rolloverEnabled: params.rolloverEnabled, + thresholdPercent: params.thresholdPercent, + forecastAlertsEnabled: params.forecastAlertsEnabled, + isActive: params.isActive, + targets: params.targets, + ); + } +} diff --git a/lib/domain/usecases/budget/listen_to_budgets_usecase.dart b/lib/domain/usecases/budget/listen_to_budgets_usecase.dart new file mode 100644 index 00000000..444f2ed9 --- /dev/null +++ b/lib/domain/usecases/budget/listen_to_budgets_usecase.dart @@ -0,0 +1,25 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/entities/budget_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class ListenToBudgetsParams { + final bool? active; + const ListenToBudgetsParams({this.active}); +} + +@injectable +class ListenToBudgetsUseCase + implements StreamUseCase, ListenToBudgetsParams> { + final BudgetRepository _repository; + + ListenToBudgetsUseCase(this._repository); + + @override + Stream>> call( + ListenToBudgetsParams params) { + return _repository.listenToBudgets(active: params.active); + } +} diff --git a/lib/domain/usecases/budget/listen_to_period_states_usecase.dart b/lib/domain/usecases/budget/listen_to_period_states_usecase.dart new file mode 100644 index 00000000..ffdf3891 --- /dev/null +++ b/lib/domain/usecases/budget/listen_to_period_states_usecase.dart @@ -0,0 +1,27 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/entities/budget_period_state_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class ListenToPeriodStatesParams { + final String budgetClientId; + const ListenToPeriodStatesParams({required this.budgetClientId}); +} + +@injectable +class ListenToPeriodStatesUseCase + implements + StreamUseCase, + ListenToPeriodStatesParams> { + final BudgetRepository _repository; + + ListenToPeriodStatesUseCase(this._repository); + + @override + Stream>> call( + ListenToPeriodStatesParams params) { + return _repository.listenToPeriodStates(params.budgetClientId); + } +} diff --git a/lib/domain/usecases/budget/listen_to_targets_usecase.dart b/lib/domain/usecases/budget/listen_to_targets_usecase.dart new file mode 100644 index 00000000..a6c8afd2 --- /dev/null +++ b/lib/domain/usecases/budget/listen_to_targets_usecase.dart @@ -0,0 +1,25 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/entities/budget_target_entity.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +class ListenToTargetsParams { + final String budgetClientId; + const ListenToTargetsParams({required this.budgetClientId}); +} + +@injectable +class ListenToTargetsUseCase + implements StreamUseCase, ListenToTargetsParams> { + final BudgetRepository _repository; + + ListenToTargetsUseCase(this._repository); + + @override + Stream>> call( + ListenToTargetsParams params) { + return _repository.listenToTargetsForBudget(params.budgetClientId); + } +} diff --git a/lib/domain/usecases/budget/refresh_period_states_usecase.dart b/lib/domain/usecases/budget/refresh_period_states_usecase.dart new file mode 100644 index 00000000..57bae1a9 --- /dev/null +++ b/lib/domain/usecases/budget/refresh_period_states_usecase.dart @@ -0,0 +1,17 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; + +@injectable +class RefreshPeriodStatesUseCase implements UseCase { + final BudgetRepository _repository; + + RefreshPeriodStatesUseCase(this._repository); + + @override + Future> call(NoParams params) async { + return await _repository.refreshPeriodStates(); + } +} diff --git a/lib/domain/usecases/budget/update_budget_usecase.dart b/lib/domain/usecases/budget/update_budget_usecase.dart new file mode 100644 index 00000000..76c3f654 --- /dev/null +++ b/lib/domain/usecases/budget/update_budget_usecase.dart @@ -0,0 +1,64 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; +import 'package:trakli/domain/repositories/budget_repository.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +class UpdateBudgetParams { + final String clientId; + final String? name; + final double? amount; + final String? currency; + final BudgetPeriodType? periodType; + final DateTime? startDate; + final DateTime? endDate; + final String? description; + final bool? rolloverEnabled; + final int? thresholdPercent; + final bool? forecastAlertsEnabled; + final bool? isActive; + final List? targets; + + const UpdateBudgetParams({ + required this.clientId, + this.name, + this.amount, + this.currency, + this.periodType, + this.startDate, + this.endDate, + this.description, + this.rolloverEnabled, + this.thresholdPercent, + this.forecastAlertsEnabled, + this.isActive, + this.targets, + }); +} + +@injectable +class UpdateBudgetUseCase implements UseCase { + final BudgetRepository _repository; + + UpdateBudgetUseCase(this._repository); + + @override + Future> call(UpdateBudgetParams params) async { + return await _repository.updateBudget( + params.clientId, + name: params.name, + amount: params.amount, + currency: params.currency, + periodType: params.periodType, + startDate: params.startDate, + endDate: params.endDate, + description: params.description, + rolloverEnabled: params.rolloverEnabled, + thresholdPercent: params.thresholdPercent, + forecastAlertsEnabled: params.forecastAlertsEnabled, + isActive: params.isActive, + targets: params.targets, + ); + } +} diff --git a/lib/presentation/ai_chat/cubit/ai_chat_cubit.dart b/lib/presentation/ai_chat/cubit/ai_chat_cubit.dart index dad8638b..df9b7af5 100644 --- a/lib/presentation/ai_chat/cubit/ai_chat_cubit.dart +++ b/lib/presentation/ai_chat/cubit/ai_chat_cubit.dart @@ -4,17 +4,26 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; import 'package:trakli/core/utils/services/logger.dart'; import 'package:trakli/data/datasources/ai/dto/chat_message_dto.dart'; import 'package:trakli/data/datasources/ai/dto/chat_session_dto.dart'; -import 'package:trakli/domain/repositories/ai_repository.dart'; +import 'package:trakli/domain/usecases/ai/create_session_usecase.dart'; +import 'package:trakli/domain/usecases/ai/delete_session_usecase.dart'; +import 'package:trakli/domain/usecases/ai/get_session_usecase.dart'; +import 'package:trakli/domain/usecases/ai/list_sessions_usecase.dart'; +import 'package:trakli/domain/usecases/ai/send_message_usecase.dart'; part 'ai_chat_cubit.freezed.dart'; part 'ai_chat_state.dart'; @injectable class AiChatCubit extends Cubit { - final AiRepository _repo; + final ListSessionsUseCase _listSessionsUseCase; + final GetSessionUseCase _getSessionUseCase; + final CreateSessionUseCase _createSessionUseCase; + final SendMessageUseCase _sendMessageUseCase; + final DeleteSessionUseCase _deleteSessionUseCase; static const Duration _pollInterval = Duration(seconds: 3); static const Duration _stuckThreshold = Duration(seconds: 90); @@ -22,13 +31,24 @@ class AiChatCubit extends Cubit { Timer? _pollTimer; DateTime? _pollStartedAt; - AiChatCubit(this._repo) : super(AiChatState.initial()); + AiChatCubit({ + required ListSessionsUseCase listSessionsUseCase, + required GetSessionUseCase getSessionUseCase, + required CreateSessionUseCase createSessionUseCase, + required SendMessageUseCase sendMessageUseCase, + required DeleteSessionUseCase deleteSessionUseCase, + }) : _listSessionsUseCase = listSessionsUseCase, + _getSessionUseCase = getSessionUseCase, + _createSessionUseCase = createSessionUseCase, + _sendMessageUseCase = sendMessageUseCase, + _deleteSessionUseCase = deleteSessionUseCase, + super(AiChatState.initial()); Future loadMostRecent() async { if (state.isInitializing) return; emit(state.copyWith(isInitializing: true, failure: null)); - final result = await _repo.listSessions(); + final result = await _listSessionsUseCase(NoParams()); await result.fold( (failure) async { emit(state.copyWith(isInitializing: false, failure: failure)); @@ -45,7 +65,7 @@ class AiChatCubit extends Cubit { } Future _loadSession(int id) async { - final result = await _repo.getSession(id); + final result = await _getSessionUseCase(GetSessionParams(id: id)); result.fold( (failure) => emit(state.copyWith(failure: failure)), (session) => emit( @@ -62,7 +82,7 @@ class AiChatCubit extends Cubit { } Future deleteSession(int id) async { - final result = await _repo.deleteSession(id); + final result = await _deleteSessionUseCase(DeleteSessionParams(id: id)); result.fold( (failure) { logger.e('AI deleteSession failed (id=$id): $failure'); @@ -89,7 +109,9 @@ class AiChatCubit extends Cubit { emit(state.copyWith(isSending: true, failure: null)); if (!state.hasSession) { - final result = await _repo.createSession(message: trimmed); + final result = await _createSessionUseCase( + CreateSessionParams(message: trimmed), + ); result.fold( (failure) { logger.e('AI createSession failed: $failure'); @@ -108,9 +130,8 @@ class AiChatCubit extends Cubit { } final sessionId = state.session!.id; - final result = await _repo.sendMessage( - sessionId: sessionId, - message: trimmed, + final result = await _sendMessageUseCase( + SendMessageParams(sessionId: sessionId, message: trimmed), ); result.fold( (failure) { @@ -148,7 +169,7 @@ class AiChatCubit extends Cubit { return; } - final result = await _repo.getSession(session.id); + final result = await _getSessionUseCase(GetSessionParams(id: session.id)); result.fold( (_) {}, (fresh) { diff --git a/lib/presentation/budget/cubit/budget_cubit.dart b/lib/presentation/budget/cubit/budget_cubit.dart index 0094461e..5f8d3a66 100644 --- a/lib/presentation/budget/cubit/budget_cubit.dart +++ b/lib/presentation/budget/cubit/budget_cubit.dart @@ -4,11 +4,23 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:trakli/core/error/failures/failures.dart'; +import 'package:trakli/core/usecases/usecase.dart'; import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; import 'package:trakli/domain/entities/budget_period_state_entity.dart'; import 'package:trakli/domain/entities/budget_progress_entity.dart'; import 'package:trakli/domain/entities/budget_target_entity.dart'; +import 'package:trakli/domain/usecases/budget/close_budget_period_usecase.dart'; +import 'package:trakli/domain/usecases/budget/delete_budget_usecase.dart'; +import 'package:trakli/domain/usecases/budget/fetch_budget_progress_usecase.dart'; +import 'package:trakli/domain/usecases/budget/fetch_budget_transactions_usecase.dart'; +import 'package:trakli/domain/usecases/budget/get_all_budgets_usecase.dart'; +import 'package:trakli/domain/usecases/budget/insert_budget_usecase.dart'; +import 'package:trakli/domain/usecases/budget/listen_to_budgets_usecase.dart'; +import 'package:trakli/domain/usecases/budget/listen_to_period_states_usecase.dart'; +import 'package:trakli/domain/usecases/budget/listen_to_targets_usecase.dart'; +import 'package:trakli/domain/usecases/budget/refresh_period_states_usecase.dart'; +import 'package:trakli/domain/usecases/budget/update_budget_usecase.dart'; import 'package:trakli/domain/repositories/budget_repository.dart'; import 'package:trakli/presentation/utils/enums.dart'; @@ -17,19 +29,53 @@ part 'budget_state.dart'; @injectable class BudgetCubit extends Cubit { - final BudgetRepository _repository; + final GetAllBudgetsUseCase _getAllBudgetsUseCase; + final InsertBudgetUseCase _insertBudgetUseCase; + final UpdateBudgetUseCase _updateBudgetUseCase; + final DeleteBudgetUseCase _deleteBudgetUseCase; + final FetchBudgetProgressUseCase _fetchBudgetProgressUseCase; + final FetchBudgetTransactionsUseCase _fetchBudgetTransactionsUseCase; + final CloseBudgetPeriodUseCase _closeBudgetPeriodUseCase; + final ListenToBudgetsUseCase _listenToBudgetsUseCase; + final ListenToTargetsUseCase _listenToTargetsUseCase; + final ListenToPeriodStatesUseCase _listenToPeriodStatesUseCase; + final RefreshPeriodStatesUseCase _refreshPeriodStatesUseCase; + StreamSubscription? _budgetsSubscription; StreamSubscription? _targetsSubscription; StreamSubscription? _periodStatesSubscription; String? _watchedBudgetClientId; - BudgetCubit(this._repository) : super(BudgetState.initial()) { + BudgetCubit({ + required GetAllBudgetsUseCase getAllBudgetsUseCase, + required InsertBudgetUseCase insertBudgetUseCase, + required UpdateBudgetUseCase updateBudgetUseCase, + required DeleteBudgetUseCase deleteBudgetUseCase, + required FetchBudgetProgressUseCase fetchBudgetProgressUseCase, + required FetchBudgetTransactionsUseCase fetchBudgetTransactionsUseCase, + required CloseBudgetPeriodUseCase closeBudgetPeriodUseCase, + required ListenToBudgetsUseCase listenToBudgetsUseCase, + required ListenToTargetsUseCase listenToTargetsUseCase, + required ListenToPeriodStatesUseCase listenToPeriodStatesUseCase, + required RefreshPeriodStatesUseCase refreshPeriodStatesUseCase, + }) : _getAllBudgetsUseCase = getAllBudgetsUseCase, + _insertBudgetUseCase = insertBudgetUseCase, + _updateBudgetUseCase = updateBudgetUseCase, + _deleteBudgetUseCase = deleteBudgetUseCase, + _fetchBudgetProgressUseCase = fetchBudgetProgressUseCase, + _fetchBudgetTransactionsUseCase = fetchBudgetTransactionsUseCase, + _closeBudgetPeriodUseCase = closeBudgetPeriodUseCase, + _listenToBudgetsUseCase = listenToBudgetsUseCase, + _listenToTargetsUseCase = listenToTargetsUseCase, + _listenToPeriodStatesUseCase = listenToPeriodStatesUseCase, + _refreshPeriodStatesUseCase = refreshPeriodStatesUseCase, + super(BudgetState.initial()) { listenToBudgets(); } Future loadBudgets({bool? active}) async { emit(state.copyWith(isLoading: true, failure: const Failure.none())); - final result = await _repository.getAllBudgets(active: active); + final result = await _getAllBudgetsUseCase(GetAllBudgetsParams(active: active)); result.fold( (failure) => emit(state.copyWith(isLoading: false, failure: failure)), (budgets) => emit(state.copyWith( @@ -43,7 +89,7 @@ class BudgetCubit extends Cubit { void listenToBudgets({bool? active}) { _budgetsSubscription?.cancel(); _budgetsSubscription = - _repository.listenToBudgets(active: active).listen((either) { + _listenToBudgetsUseCase(ListenToBudgetsParams(active: active)).listen((either) { either.fold( (failure) => emit(state.copyWith(failure: failure)), (budgets) => emit(state.copyWith( @@ -69,19 +115,21 @@ class BudgetCubit extends Cubit { List targets = const [], }) async { emit(state.copyWith(isSaving: true, failure: const Failure.none())); - final result = await _repository.insertBudget( - name: name, - amount: amount, - currency: currency, - periodType: periodType, - startDate: startDate, - endDate: endDate, - description: description, - rolloverEnabled: rolloverEnabled, - thresholdPercent: thresholdPercent, - forecastAlertsEnabled: forecastAlertsEnabled, - isActive: isActive, - targets: targets, + final result = await _insertBudgetUseCase( + InsertBudgetParams( + name: name, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + description: description, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + targets: targets, + ), ); result.fold( (failure) => emit(state.copyWith(isSaving: false, failure: failure)), @@ -108,20 +156,22 @@ class BudgetCubit extends Cubit { List? targets, }) async { emit(state.copyWith(isSaving: true, failure: const Failure.none())); - final result = await _repository.updateBudget( - clientId, - name: name, - amount: amount, - currency: currency, - periodType: periodType, - startDate: startDate, - endDate: endDate, - description: description, - rolloverEnabled: rolloverEnabled, - thresholdPercent: thresholdPercent, - forecastAlertsEnabled: forecastAlertsEnabled, - isActive: isActive, - targets: targets, + final result = await _updateBudgetUseCase( + UpdateBudgetParams( + clientId: clientId, + name: name, + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate, + endDate: endDate, + description: description, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: forecastAlertsEnabled, + isActive: isActive, + targets: targets, + ), ); result.fold( (failure) => emit(state.copyWith(isSaving: false, failure: failure)), @@ -138,7 +188,9 @@ class BudgetCubit extends Cubit { state.budgets.where((b) => b.clientId != clientId).toList(); emit(state.copyWith(budgets: optimistic)); - final result = await _repository.deleteBudget(clientId); + final result = await _deleteBudgetUseCase( + DeleteBudgetParams(clientId: clientId), + ); result.fold( (failure) => emit(state.copyWith(isDeleting: false, failure: failure)), (_) => emit(state.copyWith( @@ -154,7 +206,7 @@ class BudgetCubit extends Cubit { _targetsSubscription?.cancel(); _targetsSubscription = - _repository.listenToTargetsForBudget(clientId).listen((either) { + _listenToTargetsUseCase(ListenToTargetsParams(budgetClientId: clientId)).listen((either) { either.fold( (failure) => emit(state.copyWith(failure: failure)), (targets) => emit(state.copyWith( @@ -166,7 +218,7 @@ class BudgetCubit extends Cubit { _periodStatesSubscription?.cancel(); _periodStatesSubscription = - _repository.listenToPeriodStates(clientId).listen((either) { + _listenToPeriodStatesUseCase(ListenToPeriodStatesParams(budgetClientId: clientId)).listen((either) { either.fold( (failure) => emit(state.copyWith(failure: failure)), (states) => emit(state.copyWith( @@ -179,7 +231,9 @@ class BudgetCubit extends Cubit { Future fetchProgress(int serverId) async { emit(state.copyWith(isProgressLoading: true)); - final result = await _repository.fetchBudgetProgress(serverId); + final result = await _fetchBudgetProgressUseCase( + FetchBudgetProgressParams(id: serverId), + ); result.fold( (failure) => emit(state.copyWith( isProgressLoading: false, @@ -195,8 +249,9 @@ class BudgetCubit extends Cubit { Future fetchPeriodTransactions(int serverId, {int limit = 50}) async { emit(state.copyWith(isPeriodTransactionsLoading: true)); - final result = - await _repository.fetchBudgetTransactions(serverId, limit: limit); + final result = await _fetchBudgetTransactionsUseCase( + FetchBudgetTransactionsParams(id: serverId, limit: limit), + ); result.fold( (failure) => emit(state.copyWith( isPeriodTransactionsLoading: false, @@ -212,7 +267,9 @@ class BudgetCubit extends Cubit { Future closeBudgetPeriod(int serverId) async { emit(state.copyWith(isClosingPeriod: true)); - final result = await _repository.closeBudgetPeriod(serverId); + final result = await _closeBudgetPeriodUseCase( + CloseBudgetPeriodParams(id: serverId), + ); result.fold( (failure) => emit(state.copyWith( isClosingPeriod: false, @@ -223,14 +280,14 @@ class BudgetCubit extends Cubit { isClosingPeriod: false, failure: const Failure.none(), )); - await _repository.refreshPeriodStates(); + await _refreshPeriodStatesUseCase(NoParams()); await fetchProgress(serverId); }, ); } Future refreshPeriodStates() async { - await _repository.refreshPeriodStates(); + await _refreshPeriodStatesUseCase(NoParams()); } @override From 34dfb6e97320cd6479f81679ef5f0ceca4c9e9c0 Mon Sep 17 00:00:00 2001 From: Fuh Austin Date: Fri, 5 Jun 2026 12:11:58 +0100 Subject: [PATCH 21/21] feat(budgets): Local progress compute self heal and currency handling - Compute budget progress locally (compute_local_progress) and persist it to the budgets.progress column via BudgetProgressRecomputer; recompute on transaction/target/budget mutations. - Add budget self-heal: recompute budgets when a fresh exchange rate is cached (ExchangeRateRepository.onExchangeRateUpdated), folding in foreign-currency transactions that were excluded while no rate was available. - Exclude un-convertible foreign-currency transactions from the bar instead of surfacing a misleading cross-currency total; drop currencyMismatchCount / currencyMismatchRawAmount from entity, DTO, mapper, UI, and translations. - Restrict the add-budget currency picker to wallet currencies + app default. - Persist progress through the datasource (updateBudgetProgressByServerId) rather than touching Drift directly in the repository. - Add read-only BudgetPeriodState sync handler + DTO. --- drift_schemas/default/drift_schema_v5.json | 1 + lib/bootstrap.dart | 3 + lib/core/module/sync_module.dart | 3 + .../interceptors/logger_interceptor.dart | 15 +- lib/data/budget/period_state_client_id.dart | 4 + lib/data/database/app_database.dart | 48 +- lib/data/database/app_database.g.dart | 153 +- lib/data/database/app_database.steps.dart | 597 ++ .../budget_progress_json_converter.dart | 53 + .../string_to_double_converter.dart | 26 + lib/data/database/tables/budgets.dart | 22 +- .../budget/budget_local_datasource.dart | 25 +- .../transaction_local_datasource.dart | 63 +- lib/data/mappers/budget_mapper.dart | 29 +- .../repositories/budget_repository_impl.dart | 71 +- lib/data/repositories/exchange_rate_imp.dart | 34 +- .../budget/budget_progress_recomputer.dart | 151 + .../budget/compute_local_progress.dart | 187 + .../budget/period_state_client_id.dart | 5 + .../budget_period_state_sync_handler.dart | 232 + lib/data/sync/budget_sync_handler.dart | 4 + lib/di/injection.config.dart | 244 +- .../repositories/budget_repository.dart | 2 - .../exchange_rate_repository.dart | 4 + .../budget/refresh_period_states_usecase.dart | 17 - lib/presentation/add_transaction_screen.dart | 170 +- .../budget/add_budget_screen.dart | 80 +- .../budget/budget_detail_screen.dart | 26 +- lib/presentation/budget/budget_screen.dart | 119 +- .../budget/cubit/budget_cubit.dart | 15 +- .../add_transaction_form_compact_layout.dart | 747 +- lib/presentation/utils/custom_drawer.dart | 29 +- .../utils/forms/add_transaction_form.dart | 986 +- pubspec.lock | 10 +- test/drift/default/generated/schema.dart | 5 +- test/drift/default/generated/schema_v5.dart | 7911 +++++++++++++++++ test/unit/compute_local_progress_test.dart | 380 + 37 files changed, 11120 insertions(+), 1351 deletions(-) create mode 100644 drift_schemas/default/drift_schema_v5.json create mode 100644 lib/data/budget/period_state_client_id.dart create mode 100644 lib/data/database/converters/budget_progress_json_converter.dart create mode 100644 lib/data/database/converters/string_to_double_converter.dart create mode 100644 lib/data/services/budget/budget_progress_recomputer.dart create mode 100644 lib/data/services/budget/compute_local_progress.dart create mode 100644 lib/data/services/budget/period_state_client_id.dart create mode 100644 lib/data/sync/budget_period_state_sync_handler.dart delete mode 100644 lib/domain/usecases/budget/refresh_period_states_usecase.dart create mode 100644 test/drift/default/generated/schema_v5.dart create mode 100644 test/unit/compute_local_progress_test.dart 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 a6f16888..3754fd91 100644 --- a/lib/core/module/sync_module.dart +++ b/lib/core/module/sync_module.dart @@ -1,6 +1,7 @@ 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'; @@ -26,6 +27,7 @@ abstract class SyncModule { TransferSyncHandler transferTypeHandler, MediaSyncHandler mediaSyncHandler, BudgetSyncHandler budgetTypeHandler, + BudgetPeriodStateSyncHandler budgetPeriodStateTypeHandler, ) { return { categoryTypeHandler, @@ -38,6 +40,7 @@ abstract class SyncModule { 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 2e31d2a8..de71cd40 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -1,41 +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 'package:trakli/data/database/tables/budgets.dart'; -import 'package:trakli/data/database/tables/budget_targets.dart'; -import 'package:trakli/data/database/tables/budget_period_states.dart'; + import 'app_database.steps.dart'; +import 'tables/sync_meta_data.dart'; part 'app_database.g.dart'; - @DriftDatabase(tables: [ Transactions, Parties, @@ -76,11 +80,6 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb { if (from < 4) { await _schemaUpgrade(m, from, 4); } - if (from < 5 && to >= 5) { - await m.createTable(budgets); - await m.createTable(budgetTargets); - await m.createTable(budgetPeriodStates); - } }, ); } @@ -292,9 +291,8 @@ class AppDatabase extends _$AppDatabase with SynchronizerDb { } 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); @@ -304,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 beba7d60..cabc908d 100644 --- a/lib/data/database/app_database.g.dart +++ b/lib/data/database/app_database.g.dart @@ -6548,9 +6548,10 @@ class $BudgetsTable extends Budgets with TableInfo<$BudgetsTable, Budget> { 'description', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); @override - late final GeneratedColumn amount = GeneratedColumn( - 'amount', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); + 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, @@ -6609,6 +6610,12 @@ class $BudgetsTable extends Budgets with TableInfo<$BudgetsTable, Budget> { '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, @@ -6631,7 +6638,8 @@ class $BudgetsTable extends Budgets with TableInfo<$BudgetsTable, Budget> { forecastAlertsEnabled, isActive, ownerType, - ownerId + ownerId, + progress ]; @override String get aliasedName => _alias ?? actualTableName; @@ -6666,8 +6674,9 @@ class $BudgetsTable extends Budgets with TableInfo<$BudgetsTable, Budget> { .read(DriftSqlType.string, data['${effectivePrefix}slug'])!, description: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}description']), - amount: attachedDatabase.typeMapping - .read(DriftSqlType.double, data['${effectivePrefix}amount'])!, + 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 @@ -6690,6 +6699,9 @@ class $BudgetsTable extends Budgets with TableInfo<$BudgetsTable, Budget> { .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'])), ); } @@ -6698,9 +6710,16 @@ class $BudgetsTable extends Budgets with TableInfo<$BudgetsTable, Budget> { 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 { @@ -6726,6 +6745,7 @@ class Budget extends DataClass implements Insertable { final bool isActive; final String ownerType; final int? ownerId; + final BudgetProgressEntity? progress; const Budget( {this.id, this.userId, @@ -6748,7 +6768,8 @@ class Budget extends DataClass implements Insertable { required this.forecastAlertsEnabled, required this.isActive, required this.ownerType, - this.ownerId}); + this.ownerId, + this.progress}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -6775,7 +6796,10 @@ class Budget extends DataClass implements Insertable { if (!nullToAbsent || description != null) { map['description'] = Variable(description); } - map['amount'] = Variable(amount); + { + map['amount'] = + Variable($BudgetsTable.$converteramount.toSql(amount)); + } map['currency'] = Variable(currency); { map['period_type'] = Variable( @@ -6793,6 +6817,10 @@ class Budget extends DataClass implements Insertable { if (!nullToAbsent || ownerId != null) { map['owner_id'] = Variable(ownerId); } + if (!nullToAbsent || progress != null) { + map['progress'] = + Variable($BudgetsTable.$converterprogressn.toSql(progress)); + } return map; } @@ -6831,6 +6859,9 @@ class Budget extends DataClass implements Insertable { ownerId: ownerId == null && nullToAbsent ? const Value.absent() : Value(ownerId), + progress: progress == null && nullToAbsent + ? const Value.absent() + : Value(progress), ); } @@ -6849,19 +6880,22 @@ class Budget extends DataClass implements Insertable { name: serializer.fromJson(json['name']), slug: serializer.fromJson(json['slug']), description: serializer.fromJson(json['description']), - amount: serializer.fromJson(json['amount']), + amount: $BudgetsTable.$converteramount + .fromJson(serializer.fromJson(json['amount'])), currency: serializer.fromJson(json['currency']), periodType: $BudgetsTable.$converterperiodType - .fromJson(serializer.fromJson(json['periodType'])), - startDate: serializer.fromJson(json['startDate']), - endDate: serializer.fromJson(json['endDate']), - rolloverEnabled: serializer.fromJson(json['rolloverEnabled']), - thresholdPercent: serializer.fromJson(json['thresholdPercent']), + .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['forecastAlertsEnabled']), - isActive: serializer.fromJson(json['isActive']), - ownerType: serializer.fromJson(json['ownerType']), - ownerId: serializer.fromJson(json['ownerId']), + 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?>(json['progress'])), ); } @override @@ -6879,18 +6913,21 @@ class Budget extends DataClass implements Insertable { 'name': serializer.toJson(name), 'slug': serializer.toJson(slug), 'description': serializer.toJson(description), - 'amount': serializer.toJson(amount), + 'amount': serializer + .toJson($BudgetsTable.$converteramount.toJson(amount)), 'currency': serializer.toJson(currency), - 'periodType': serializer.toJson( + 'period_type': serializer.toJson( $BudgetsTable.$converterperiodType.toJson(periodType)), - 'startDate': serializer.toJson(startDate), - 'endDate': serializer.toJson(endDate), - 'rolloverEnabled': serializer.toJson(rolloverEnabled), - 'thresholdPercent': serializer.toJson(thresholdPercent), - 'forecastAlertsEnabled': serializer.toJson(forecastAlertsEnabled), - 'isActive': serializer.toJson(isActive), - 'ownerType': serializer.toJson(ownerType), - 'ownerId': serializer.toJson(ownerId), + 'start_date': serializer.toJson(startDate), + 'end_date': serializer.toJson(endDate), + 'rollover_enabled': serializer.toJson(rolloverEnabled), + 'threshold_percent': serializer.toJson(thresholdPercent), + 'forecast_alerts_enabled': serializer.toJson(forecastAlertsEnabled), + 'is_active': serializer.toJson(isActive), + 'owner_type': serializer.toJson(ownerType), + 'owner_id': serializer.toJson(ownerId), + 'progress': serializer.toJson?>( + $BudgetsTable.$converterprogressn.toJson(progress)), }; } @@ -6916,7 +6953,8 @@ class Budget extends DataClass implements Insertable { bool? forecastAlertsEnabled, bool? isActive, String? ownerType, - Value ownerId = const Value.absent()}) => + Value ownerId = const Value.absent(), + Value progress = const Value.absent()}) => Budget( id: id.present ? id.value : this.id, userId: userId.present ? userId.value : this.userId, @@ -6942,6 +6980,7 @@ class Budget extends DataClass implements Insertable { isActive: isActive ?? this.isActive, ownerType: ownerType ?? this.ownerType, ownerId: ownerId.present ? ownerId.value : this.ownerId, + progress: progress.present ? progress.value : this.progress, ); Budget copyWithCompanion(BudgetsCompanion data) { return Budget( @@ -6977,6 +7016,7 @@ class Budget extends DataClass implements Insertable { isActive: data.isActive.present ? data.isActive.value : this.isActive, ownerType: data.ownerType.present ? data.ownerType.value : this.ownerType, ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + progress: data.progress.present ? data.progress.value : this.progress, ); } @@ -7004,7 +7044,8 @@ class Budget extends DataClass implements Insertable { ..write('forecastAlertsEnabled: $forecastAlertsEnabled, ') ..write('isActive: $isActive, ') ..write('ownerType: $ownerType, ') - ..write('ownerId: $ownerId') + ..write('ownerId: $ownerId, ') + ..write('progress: $progress') ..write(')')) .toString(); } @@ -7032,7 +7073,8 @@ class Budget extends DataClass implements Insertable { forecastAlertsEnabled, isActive, ownerType, - ownerId + ownerId, + progress ]); @override bool operator ==(Object other) => @@ -7059,7 +7101,8 @@ class Budget extends DataClass implements Insertable { other.forecastAlertsEnabled == this.forecastAlertsEnabled && other.isActive == this.isActive && other.ownerType == this.ownerType && - other.ownerId == this.ownerId); + other.ownerId == this.ownerId && + other.progress == this.progress); } class BudgetsCompanion extends UpdateCompanion { @@ -7085,6 +7128,7 @@ class BudgetsCompanion extends UpdateCompanion { final Value isActive; final Value ownerType; final Value ownerId; + final Value progress; final Value rowid; const BudgetsCompanion({ this.id = const Value.absent(), @@ -7109,6 +7153,7 @@ class BudgetsCompanion extends UpdateCompanion { this.isActive = const Value.absent(), this.ownerType = const Value.absent(), this.ownerId = const Value.absent(), + this.progress = const Value.absent(), this.rowid = const Value.absent(), }); BudgetsCompanion.insert({ @@ -7134,6 +7179,7 @@ class BudgetsCompanion extends UpdateCompanion { this.isActive = const Value.absent(), this.ownerType = const Value.absent(), this.ownerId = const Value.absent(), + this.progress = const Value.absent(), this.rowid = const Value.absent(), }) : name = Value(name), slug = Value(slug), @@ -7153,7 +7199,7 @@ class BudgetsCompanion extends UpdateCompanion { Expression? name, Expression? slug, Expression? description, - Expression? amount, + Expression? amount, Expression? currency, Expression? periodType, Expression? startDate, @@ -7164,6 +7210,7 @@ class BudgetsCompanion extends UpdateCompanion { Expression? isActive, Expression? ownerType, Expression? ownerId, + Expression? progress, Expression? rowid, }) { return RawValuesInsertable({ @@ -7190,6 +7237,7 @@ class BudgetsCompanion extends UpdateCompanion { if (isActive != null) 'is_active': isActive, if (ownerType != null) 'owner_type': ownerType, if (ownerId != null) 'owner_id': ownerId, + if (progress != null) 'progress': progress, if (rowid != null) 'rowid': rowid, }); } @@ -7217,6 +7265,7 @@ class BudgetsCompanion extends UpdateCompanion { Value? isActive, Value? ownerType, Value? ownerId, + Value? progress, Value? rowid}) { return BudgetsCompanion( id: id ?? this.id, @@ -7242,6 +7291,7 @@ class BudgetsCompanion extends UpdateCompanion { isActive: isActive ?? this.isActive, ownerType: ownerType ?? this.ownerType, ownerId: ownerId ?? this.ownerId, + progress: progress ?? this.progress, rowid: rowid ?? this.rowid, ); } @@ -7283,7 +7333,8 @@ class BudgetsCompanion extends UpdateCompanion { map['description'] = Variable(description.value); } if (amount.present) { - map['amount'] = Variable(amount.value); + map['amount'] = + Variable($BudgetsTable.$converteramount.toSql(amount.value)); } if (currency.present) { map['currency'] = Variable(currency.value); @@ -7317,6 +7368,10 @@ class BudgetsCompanion extends UpdateCompanion { if (ownerId.present) { map['owner_id'] = Variable(ownerId.value); } + if (progress.present) { + map['progress'] = Variable( + $BudgetsTable.$converterprogressn.toSql(progress.value)); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -7348,6 +7403,7 @@ class BudgetsCompanion extends UpdateCompanion { ..write('isActive: $isActive, ') ..write('ownerType: $ownerType, ') ..write('ownerId: $ownerId, ') + ..write('progress: $progress, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -12776,6 +12832,7 @@ typedef $$BudgetsTableCreateCompanionBuilder = BudgetsCompanion Function({ Value isActive, Value ownerType, Value ownerId, + Value progress, Value rowid, }); typedef $$BudgetsTableUpdateCompanionBuilder = BudgetsCompanion Function({ @@ -12801,6 +12858,7 @@ typedef $$BudgetsTableUpdateCompanionBuilder = BudgetsCompanion Function({ Value isActive, Value ownerType, Value ownerId, + Value progress, Value rowid, }); @@ -12885,8 +12943,10 @@ class $$BudgetsTableFilterComposer ColumnFilters get description => $composableBuilder( column: $table.description, builder: (column) => ColumnFilters(column)); - ColumnFilters get amount => $composableBuilder( - column: $table.amount, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters get amount => + $composableBuilder( + column: $table.amount, + builder: (column) => ColumnWithTypeConverterFilters(column)); ColumnFilters get currency => $composableBuilder( column: $table.currency, builder: (column) => ColumnFilters(column)); @@ -12923,6 +12983,12 @@ class $$BudgetsTableFilterComposer ColumnFilters get ownerId => $composableBuilder( column: $table.ownerId, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters + get progress => $composableBuilder( + column: $table.progress, + builder: (column) => ColumnWithTypeConverterFilters(column)); + Expression budgetTargetsRefs( Expression Function($$BudgetTargetsTableFilterComposer f) f) { final $$BudgetTargetsTableFilterComposer composer = $composerBuilder( @@ -13009,7 +13075,7 @@ class $$BudgetsTableOrderingComposer ColumnOrderings get description => $composableBuilder( column: $table.description, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get amount => $composableBuilder( + ColumnOrderings get amount => $composableBuilder( column: $table.amount, builder: (column) => ColumnOrderings(column)); ColumnOrderings get currency => $composableBuilder( @@ -13044,6 +13110,9 @@ class $$BudgetsTableOrderingComposer ColumnOrderings get ownerId => $composableBuilder( column: $table.ownerId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get progress => $composableBuilder( + column: $table.progress, builder: (column) => ColumnOrderings(column)); } class $$BudgetsTableAnnotationComposer @@ -13088,7 +13157,7 @@ class $$BudgetsTableAnnotationComposer GeneratedColumn get description => $composableBuilder( column: $table.description, builder: (column) => column); - GeneratedColumn get amount => + GeneratedColumnWithTypeConverter get amount => $composableBuilder(column: $table.amount, builder: (column) => column); GeneratedColumn get currency => @@ -13122,6 +13191,10 @@ class $$BudgetsTableAnnotationComposer GeneratedColumn get ownerId => $composableBuilder(column: $table.ownerId, builder: (column) => column); + GeneratedColumnWithTypeConverter + get progress => $composableBuilder( + column: $table.progress, builder: (column) => column); + Expression budgetTargetsRefs( Expression Function($$BudgetTargetsTableAnnotationComposer a) f) { final $$BudgetTargetsTableAnnotationComposer composer = $composerBuilder( @@ -13212,6 +13285,7 @@ class $$BudgetsTableTableManager extends RootTableManager< Value isActive = const Value.absent(), Value ownerType = const Value.absent(), Value ownerId = const Value.absent(), + Value progress = const Value.absent(), Value rowid = const Value.absent(), }) => BudgetsCompanion( @@ -13237,6 +13311,7 @@ class $$BudgetsTableTableManager extends RootTableManager< isActive: isActive, ownerType: ownerType, ownerId: ownerId, + progress: progress, rowid: rowid, ), createCompanionCallback: ({ @@ -13262,6 +13337,7 @@ class $$BudgetsTableTableManager extends RootTableManager< Value isActive = const Value.absent(), Value ownerType = const Value.absent(), Value ownerId = const Value.absent(), + Value progress = const Value.absent(), Value rowid = const Value.absent(), }) => BudgetsCompanion.insert( @@ -13287,6 +13363,7 @@ class $$BudgetsTableTableManager extends RootTableManager< isActive: isActive, ownerType: ownerType, ownerId: ownerId, + progress: progress, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/data/database/app_database.steps.dart b/lib/data/database/app_database.steps.dart index 1b913801..17c2b9d8 100644 --- a/lib/data/database/app_database.steps.dart +++ b/lib/data/database/app_database.steps.dart @@ -1551,10 +1551,600 @@ i1.GeneratedColumn _column_68(String aliasedName) => type: i1.DriftSqlType.string, defaultConstraints: i1.GeneratedColumn.constraintIsAlways( 'REFERENCES transactions (client_id)')); + +final class Schema5 extends i0.VersionedSchema { + Schema5({required super.database}) : super(version: 5); + @override + late final List entities = [ + wallets, + parties, + groups, + transactions, + categories, + configs, + users, + localChanges, + syncMetadata, + categorizables, + notifications, + mediaFiles, + transfers, + budgets, + budgetTargets, + budgetPeriodStates, + ]; + late final Shape0 wallets = Shape0( + source: i0.VersionedTable( + entityName: 'wallets', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 parties = Shape1( + source: i0.VersionedTable( + entityName: 'parties', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_12, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 groups = Shape2( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_12, + _column_14, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 transactions = Shape12( + source: i0.VersionedTable( + entityName: 'transactions', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_16, + _column_9, + _column_12, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_23, + _column_59, + _column_60, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape4 categories = Shape4( + source: i0.VersionedTable( + entityName: 'categories', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_24, + _column_25, + _column_12, + _column_9, + _column_14, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 configs = Shape5( + source: i0.VersionedTable( + entityName: 'configs', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_26, + _column_9, + _column_27, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 users = Shape6( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_28, + _column_29, + _column_30, + _column_31, + _column_32, + _column_33, + _column_34, + _column_35, + _column_36, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 localChanges = Shape7( + source: i0.VersionedTable( + entityName: 'local_changes', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(entity_id, entity_type)', + ], + columns: [ + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 syncMetadata = Shape8( + source: i0.VersionedTable( + entityName: 'sync_metadata', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(entity_type)', + ], + columns: [ + _column_37, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 categorizables = Shape9( + source: i0.VersionedTable( + entityName: 'categorizables', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(categorizable_id, categorizable_type, category_client_id)', + ], + columns: [ + _column_47, + _column_48, + _column_49, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 notifications = Shape10( + source: i0.VersionedTable( + entityName: 'notifications', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_9, + _column_50, + _column_51, + _column_41, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 mediaFiles = Shape11( + source: i0.VersionedTable( + entityName: 'media_files', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(path)', + ], + columns: [ + _column_53, + _column_54, + _column_15, + _column_55, + _column_56, + _column_57, + _column_58, + _column_35, + _column_36, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 transfers = Shape13( + source: i0.VersionedTable( + entityName: 'transfers', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_16, + _column_61, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + _column_68, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape14 budgets = Shape14( + source: i0.VersionedTable( + entityName: 'budgets', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_25, + _column_12, + _column_69, + _column_11, + _column_70, + _column_71, + _column_72, + _column_73, + _column_74, + _column_75, + _column_76, + _column_77, + _column_78, + _column_79, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape15 budgetTargets = Shape15( + source: i0.VersionedTable( + entityName: 'budget_targets', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(budget_client_id, target_type, target_client_id)', + ], + columns: [ + _column_80, + _column_81, + _column_82, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape16 budgetPeriodStates = Shape16( + source: i0.VersionedTable( + entityName: 'budget_period_states', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(client_id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_80, + _column_83, + _column_84, + _column_85, + _column_86, + _column_87, + _column_88, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape14 extends i0.VersionedTable { + Shape14({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get userId => + columnsByName['user_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get clientId => + columnsByName['client_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get rev => + columnsByName['rev']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => + columnsByName['deleted_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastSyncedAt => + columnsByName['last_synced_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get slug => + columnsByName['slug']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => + columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get amount => + columnsByName['amount']! as i1.GeneratedColumn; + i1.GeneratedColumn get currency => + columnsByName['currency']! as i1.GeneratedColumn; + i1.GeneratedColumn get periodType => + columnsByName['period_type']! as i1.GeneratedColumn; + i1.GeneratedColumn get startDate => + columnsByName['start_date']! as i1.GeneratedColumn; + i1.GeneratedColumn get endDate => + columnsByName['end_date']! as i1.GeneratedColumn; + i1.GeneratedColumn get rolloverEnabled => + columnsByName['rollover_enabled']! as i1.GeneratedColumn; + i1.GeneratedColumn get thresholdPercent => + columnsByName['threshold_percent']! as i1.GeneratedColumn; + i1.GeneratedColumn get forecastAlertsEnabled => + columnsByName['forecast_alerts_enabled']! as i1.GeneratedColumn; + i1.GeneratedColumn get isActive => + columnsByName['is_active']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerType => + columnsByName['owner_type']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => + columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get progress => + columnsByName['progress']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_69(String aliasedName) => + i1.GeneratedColumn('amount', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_70(String aliasedName) => + i1.GeneratedColumn('period_type', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_71(String aliasedName) => + i1.GeneratedColumn('start_date', aliasedName, false, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_72(String aliasedName) => + i1.GeneratedColumn('end_date', aliasedName, true, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_73(String aliasedName) => + i1.GeneratedColumn('rollover_enabled', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("rollover_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_74(String aliasedName) => + i1.GeneratedColumn('threshold_percent', aliasedName, false, + type: i1.DriftSqlType.int, defaultValue: const CustomExpression('80')); +i1.GeneratedColumn _column_75(String aliasedName) => + i1.GeneratedColumn('forecast_alerts_enabled', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("forecast_alerts_enabled" IN (0, 1))'), + defaultValue: const CustomExpression('0')); +i1.GeneratedColumn _column_76(String aliasedName) => + i1.GeneratedColumn('is_active', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_active" IN (0, 1))'), + defaultValue: const CustomExpression('1')); +i1.GeneratedColumn _column_77(String aliasedName) => + i1.GeneratedColumn('owner_type', aliasedName, false, + type: i1.DriftSqlType.string, + defaultValue: const CustomExpression('\'user\'')); +i1.GeneratedColumn _column_78(String aliasedName) => + i1.GeneratedColumn('owner_id', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_79(String aliasedName) => + i1.GeneratedColumn('progress', aliasedName, true, + type: i1.DriftSqlType.string); + +class Shape15 extends i0.VersionedTable { + Shape15({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get budgetClientId => + columnsByName['budget_client_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get targetType => + columnsByName['target_type']! as i1.GeneratedColumn; + i1.GeneratedColumn get targetClientId => + columnsByName['target_client_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_80(String aliasedName) => + i1.GeneratedColumn('budget_client_id', aliasedName, false, + type: i1.DriftSqlType.string, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES budgets (client_id)')); +i1.GeneratedColumn _column_81(String aliasedName) => + i1.GeneratedColumn('target_type', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_82(String aliasedName) => + i1.GeneratedColumn('target_client_id', aliasedName, false, + type: i1.DriftSqlType.string); + +class Shape16 extends i0.VersionedTable { + Shape16({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get userId => + columnsByName['user_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get clientId => + columnsByName['client_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get rev => + columnsByName['rev']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => + columnsByName['deleted_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastSyncedAt => + columnsByName['last_synced_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get budgetClientId => + columnsByName['budget_client_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get periodStart => + columnsByName['period_start']! as i1.GeneratedColumn; + i1.GeneratedColumn get periodEnd => + columnsByName['period_end']! as i1.GeneratedColumn; + i1.GeneratedColumn get netSpent => + columnsByName['net_spent']! as i1.GeneratedColumn; + i1.GeneratedColumn get rolloverIn => + columnsByName['rollover_in']! as i1.GeneratedColumn; + i1.GeneratedColumn get rolloverOut => + columnsByName['rollover_out']! as i1.GeneratedColumn; + i1.GeneratedColumn get closedAt => + columnsByName['closed_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_83(String aliasedName) => + i1.GeneratedColumn('period_start', aliasedName, false, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_84(String aliasedName) => + i1.GeneratedColumn('period_end', aliasedName, false, + type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_85(String aliasedName) => + i1.GeneratedColumn('net_spent', aliasedName, false, + type: i1.DriftSqlType.double, + defaultValue: const CustomExpression('0.0')); +i1.GeneratedColumn _column_86(String aliasedName) => + i1.GeneratedColumn('rollover_in', aliasedName, false, + type: i1.DriftSqlType.double, + defaultValue: const CustomExpression('0.0')); +i1.GeneratedColumn _column_87(String aliasedName) => + i1.GeneratedColumn('rollover_out', aliasedName, false, + type: i1.DriftSqlType.double, + defaultValue: const CustomExpression('0.0')); +i1.GeneratedColumn _column_88(String aliasedName) => + i1.GeneratedColumn('closed_at', aliasedName, true, + type: i1.DriftSqlType.dateTime); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, + required Future Function(i1.Migrator m, Schema5 schema) from4To5, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -1573,6 +2163,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from3To4(migrator, schema); return 4; + case 4: + final schema = Schema5(database: database); + final migrator = i1.Migrator(database, schema); + await from4To5(migrator, schema); + return 5; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -1583,10 +2178,12 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, + required Future Function(i1.Migrator m, Schema5 schema) from4To5, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, from2To3: from2To3, from3To4: from3To4, + from4To5: from4To5, )); diff --git a/lib/data/database/converters/budget_progress_json_converter.dart b/lib/data/database/converters/budget_progress_json_converter.dart new file mode 100644 index 00000000..0dc4e8d5 --- /dev/null +++ b/lib/data/database/converters/budget_progress_json_converter.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart'; +import 'package:trakli/data/mappers/budget_mapper.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; + +class BudgetProgressJsonConverter + extends TypeConverter + with + JsonTypeConverter2> { + const BudgetProgressJsonConverter(); + + @override + BudgetProgressEntity fromSql(String fromDb) { + final json = jsonDecode(fromDb) as Map; + return BudgetMapper.progressFromDto(BudgetProgressDto.fromJson(json)); + } + + @override + String toSql(BudgetProgressEntity value) { + return jsonEncode(_entityToDto(value).toJson()); + } + + @override + BudgetProgressEntity fromJson(Map json) { + return BudgetMapper.progressFromDto( + BudgetProgressDto.fromJson(Map.from(json)), + ); + } + + @override + Map toJson(BudgetProgressEntity value) { + return _entityToDto(value).toJson(); + } +} + +BudgetProgressDto _entityToDto(BudgetProgressEntity e) => BudgetProgressDto( + periodStart: e.periodStart, + periodEnd: e.periodEnd, + limit: e.limit, + grossSpent: e.grossSpent, + refunds: e.refunds, + netSpent: e.netSpent, + rolloverIn: e.rolloverIn, + effectiveLimit: e.effectiveLimit, + remaining: e.remaining, + percentUsed: e.percentUsed, + projectedSpend: e.projectedSpend, + status: e.status, + isThresholdCrossed: e.isThresholdCrossed, + isForecastBreach: e.isForecastBreach, + ); diff --git a/lib/data/database/converters/string_to_double_converter.dart b/lib/data/database/converters/string_to_double_converter.dart new file mode 100644 index 00000000..8bbec36e --- /dev/null +++ b/lib/data/database/converters/string_to_double_converter.dart @@ -0,0 +1,26 @@ +import 'package:drift/drift.dart'; + +class StringToDoubleConverter extends TypeConverter + with JsonTypeConverter2 { + const StringToDoubleConverter(); + + @override + double fromSql(String fromDb) { + return double.parse(fromDb); + } + + @override + String toSql(double value) { + return value.toString(); + } + + @override + double fromJson(String json) { + return double.parse(json); + } + + @override + String toJson(double value) { + return value.toString(); + } +} diff --git a/lib/data/database/tables/budgets.dart b/lib/data/database/tables/budgets.dart index d82d8e43..3e35f9b3 100644 --- a/lib/data/database/tables/budgets.dart +++ b/lib/data/database/tables/budgets.dart @@ -1,4 +1,6 @@ import 'package:drift/drift.dart'; +import 'package:trakli/data/database/converters/budget_progress_json_converter.dart'; +import 'package:trakli/data/database/converters/string_to_double_converter.dart'; import 'package:trakli/data/database/tables/sync_table.dart'; import 'package:trakli/presentation/utils/enums.dart'; @@ -7,18 +9,32 @@ class Budgets extends Table with SyncTable { TextColumn get name => text()(); TextColumn get slug => text()(); TextColumn get description => text().nullable()(); - RealColumn get amount => real()(); + TextColumn get amount => text().map( + const StringToDoubleConverter(), + )(); TextColumn get currency => text()(); + @JsonKey('period_type') TextColumn get periodType => textEnum()(); + @JsonKey('start_date') DateTimeColumn get startDate => dateTime()(); + @JsonKey('end_date') DateTimeColumn get endDate => dateTime().nullable()(); + @JsonKey('rollover_enabled') BoolColumn get rolloverEnabled => boolean().withDefault(const Constant(false))(); + @JsonKey('threshold_percent') IntColumn get thresholdPercent => integer().withDefault(const Constant(80))(); + @JsonKey('forecast_alerts_enabled') BoolColumn get forecastAlertsEnabled => boolean().withDefault(const Constant(false))(); + @JsonKey('is_active') BoolColumn get isActive => boolean().withDefault(const Constant(true))(); - TextColumn get ownerType => - text().withDefault(const Constant('user'))(); + @JsonKey('owner_type') + TextColumn get ownerType => text().withDefault(const Constant('user'))(); + @JsonKey('owner_id') IntColumn get ownerId => integer().nullable()(); + @JsonKey('progress') + TextColumn get progress => text() + .nullable() + .map(const BudgetProgressJsonConverter())(); } diff --git a/lib/data/datasources/budget/budget_local_datasource.dart b/lib/data/datasources/budget/budget_local_datasource.dart index c15c8c7f..f3805af4 100644 --- a/lib/data/datasources/budget/budget_local_datasource.dart +++ b/lib/data/datasources/budget/budget_local_datasource.dart @@ -3,7 +3,9 @@ import 'package:injectable/injectable.dart'; import 'package:trakli/core/error/exceptions.dart'; import 'package:trakli/core/utils/date_util.dart'; import 'package:trakli/core/utils/id_helper.dart'; +import 'package:trakli/data/services/budget/budget_progress_recomputer.dart'; import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; import 'package:trakli/presentation/utils/enums.dart'; class BudgetTargetInput { @@ -74,12 +76,16 @@ abstract class BudgetLocalDataSource { Future> getResolvedTargetsForBudget( String budgetClientId); + + Future updateBudgetProgressByServerId( + int id, BudgetProgressEntity progress); } @Injectable(as: BudgetLocalDataSource) class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { - BudgetLocalDataSourceImpl(this.database); + BudgetLocalDataSourceImpl(this.database, this._recomputer); final AppDatabase database; + final BudgetProgressRecomputer _recomputer; @override Future> getAllBudgets({bool? active}) async { @@ -166,7 +172,7 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { final now = getNewFormattedUtcDateTime(); final clientId = await generateDeviceScopedId(); - return database.transaction(() async { + final inserted = await database.transaction(() async { final inserted = await database.into(database.budgets).insertReturning( BudgetsCompanion.insert( clientId: Value(clientId), @@ -200,6 +206,9 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { return inserted; }); + + await _recomputer.recomputeFor(inserted.clientId); + return inserted; } @override @@ -231,7 +240,7 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { final now = getNewFormattedUtcDateTime(); - return database.transaction(() async { + final updated = await database.transaction(() async { final updated = await (database.update(database.budgets) ..where((b) => b.clientId.equals(clientId))) .writeReturning( @@ -280,6 +289,9 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { return updated.first; }); + + await _recomputer.recomputeFor(updated.clientId); + return updated; } @override @@ -323,6 +335,13 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource { return out; } + @override + Future updateBudgetProgressByServerId( + int id, BudgetProgressEntity progress) async { + await (database.update(database.budgets)..where((b) => b.id.equals(id))) + .write(BudgetsCompanion(progress: Value(progress))); + } + @override Future deleteBudget(String clientId) async { final row = await (database.select(database.budgets) diff --git a/lib/data/datasources/transaction/transaction_local_datasource.dart b/lib/data/datasources/transaction/transaction_local_datasource.dart index 09626b0f..b7b368d1 100644 --- a/lib/data/datasources/transaction/transaction_local_datasource.dart +++ b/lib/data/datasources/transaction/transaction_local_datasource.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:injectable/injectable.dart'; import 'package:trakli/core/constants/fileable_type_constants.dart'; import 'package:trakli/core/utils/date_util.dart'; +import 'package:trakli/data/services/budget/budget_progress_recomputer.dart'; import 'package:trakli/data/database/app_database.dart'; import 'package:trakli/data/datasources/media_file/media_file_local_datasource.dart'; import 'package:trakli/data/datasources/transaction/dto/transaction_complete_dto.dart'; @@ -42,10 +43,15 @@ abstract class TransactionLocalDataSource { @Injectable(as: TransactionLocalDataSource) class TransactionLocalDataSourceImpl implements TransactionLocalDataSource { - TransactionLocalDataSourceImpl(this.database, this._mediaFileLocalDataSource); + TransactionLocalDataSourceImpl( + this.database, + this._mediaFileLocalDataSource, + this._budgetProgressRecomputer, + ); final AppDatabase database; final MediaFileLocalDataSource _mediaFileLocalDataSource; + final BudgetProgressRecomputer _budgetProgressRecomputer; List mapTransactionAndComplete( List rows, @@ -306,7 +312,7 @@ class TransactionLocalDataSourceImpl implements TransactionLocalDataSource { } // Transaction: writes only + reads that depend on those writes. - return database.transaction(() async { + final result = await database.transaction(() async { final model = await database.into(database.transactions).insertReturning( TransactionsCompanion.insert( clientId: Value(clientId), @@ -384,6 +390,13 @@ class TransactionLocalDataSourceImpl implements TransactionLocalDataSource { .toList(), ); }); + + await _budgetProgressRecomputer.recomputeAffectedBy( + walletClientId: walletClientId, + groupClientId: groupClientId, + categoryClientIds: categoryIds.toSet(), + ); + return result; } @override @@ -398,7 +411,17 @@ class TransactionLocalDataSourceImpl implements TransactionLocalDataSource { String? groupClientId, String? transferClientId, }) async { - return database.transaction(() async { + // Capture original tag-set before the mutation so we can also recompute + // budgets the transaction *used to* affect. + final originalSnapshot = await (database.select(database.transactions) + ..where((t) => t.clientId.equals(id))) + .getSingleOrNull(); + final originalCategories = originalSnapshot == null + ? const [] + : await database.getCategoriesForTransaction( + id, CategorizableType.transaction); + + final result = await database.transaction(() async { final originalTransaction = await (database.select(database.transactions) ..where((t) => t.clientId.equals(id))) .getSingle(); @@ -566,11 +589,35 @@ class TransactionLocalDataSourceImpl implements TransactionLocalDataSource { files: files, ); }); + + final affectedCategoryIds = { + ...originalCategories.map((c) => c.clientId), + ...result.categories.map((c) => c.clientId), + }; + final affectedWallets = { + if (originalSnapshot != null) originalSnapshot.walletClientId, + result.wallet.clientId, + }; + final affectedGroups = { + originalSnapshot?.groupClientId, + result.group?.clientId, + }..removeWhere((g) => g == null); + + for (final walletId in affectedWallets) { + for (final groupId in affectedGroups.isEmpty ? {null} : affectedGroups) { + await _budgetProgressRecomputer.recomputeAffectedBy( + walletClientId: walletId, + groupClientId: groupId, + categoryClientIds: affectedCategoryIds, + ); + } + } + return result; } @override Future deleteTransaction(String id) async { - return database.transaction(() async { + final result = await database.transaction(() async { final transaction = await (database.select(database.transactions) ..where((t) => t.clientId.equals(id))) .getSingle(); @@ -629,6 +676,14 @@ class TransactionLocalDataSourceImpl implements TransactionLocalDataSource { files: files, ); }); + + await _budgetProgressRecomputer.recomputeAffectedBy( + walletClientId: result.wallet.clientId, + groupClientId: result.group?.clientId, + categoryClientIds: + result.categories.map((c) => c.clientId).toSet(), + ); + return result; } @override diff --git a/lib/data/mappers/budget_mapper.dart b/lib/data/mappers/budget_mapper.dart index a34c0436..d1e2f0b2 100644 --- a/lib/data/mappers/budget_mapper.dart +++ b/lib/data/mappers/budget_mapper.dart @@ -55,6 +55,21 @@ class BudgetMapper { ); } + static BudgetPeriodStateEntity periodStateToDomain(db.BudgetPeriodState row) { + return BudgetPeriodStateEntity( + clientId: row.clientId, + id: row.id, + budgetClientId: row.budgetClientId, + periodStart: row.periodStart, + periodEnd: row.periodEnd, + netSpent: row.netSpent, + rolloverIn: row.rolloverIn, + rolloverOut: row.rolloverOut, + closedAt: row.closedAt, + lastSyncedAt: row.lastSyncedAt, + ); + } + static BudgetProgressEntity progressFromDto(BudgetProgressDto dto) { return BudgetProgressEntity( periodStart: dto.periodStart, @@ -74,18 +89,4 @@ class BudgetMapper { ); } - static BudgetPeriodStateEntity periodStateToDomain(db.BudgetPeriodState row) { - return BudgetPeriodStateEntity( - clientId: row.clientId, - id: row.id, - budgetClientId: row.budgetClientId, - periodStart: row.periodStart, - periodEnd: row.periodEnd, - netSpent: row.netSpent, - rolloverIn: row.rolloverIn, - rolloverOut: row.rolloverOut, - closedAt: row.closedAt, - lastSyncedAt: row.lastSyncedAt, - ); - } } diff --git a/lib/data/repositories/budget_repository_impl.dart b/lib/data/repositories/budget_repository_impl.dart index ea716439..ca5b211f 100644 --- a/lib/data/repositories/budget_repository_impl.dart +++ b/lib/data/repositories/budget_repository_impl.dart @@ -1,8 +1,8 @@ import 'dart:async'; + +import 'package:drift_sync_core/drift_sync_core.dart'; import 'package:fpdart/fpdart.dart'; import 'package:injectable/injectable.dart'; -import 'package:drift/drift.dart'; -import 'package:drift_sync_core/drift_sync_core.dart'; import 'package:trakli/core/error/failures/failures.dart'; import 'package:trakli/core/error/repository_error_handler.dart'; import 'package:trakli/data/database/app_database.dart' as db; @@ -42,7 +42,8 @@ class BudgetRepositoryImpl final out = []; for (final row in rows) { final targets = await _domainTargets(row.clientId); - out.add(BudgetMapper.toDomain(row, targets: targets)); + out.add(BudgetMapper.toDomain(row, + targets: targets, progress: row.progress)); } return out; }); @@ -54,7 +55,8 @@ class BudgetRepositoryImpl final row = await localDataSource.getBudgetByClientId(clientId); if (row == null) return null; final targets = await _domainTargets(clientId); - return BudgetMapper.toDomain(row, targets: targets); + return BudgetMapper.toDomain(row, + targets: targets, progress: row.progress); }); } @@ -165,7 +167,8 @@ class BudgetRepositoryImpl final out = []; for (final row in rows) { final targets = await _domainTargets(row.clientId); - out.add(BudgetMapper.toDomain(row, targets: targets)); + out.add(BudgetMapper.toDomain(row, + targets: targets, progress: row.progress)); } return Right>(out); } catch (_) { @@ -207,13 +210,18 @@ class BudgetRepositoryImpl return RepositoryErrorHandler.handleApiCall(() async { final dto = await remoteDataSource.getBudgetProgress(id); if (dto == null) return null; - return BudgetMapper.progressFromDto(dto); + final progress = BudgetMapper.progressFromDto(dto); + + await localDataSource.updateBudgetProgressByServerId(id, progress); + + return progress; }); } @override - Future> - fetchBudgetTransactions(int id, {int limit = 50}) { + Future> fetchBudgetTransactions( + int id, + {int limit = 50}) { return RepositoryErrorHandler.handleApiCall(() async { return remoteDataSource.getBudgetTransactions(id, limit: limit); }); @@ -227,53 +235,6 @@ class BudgetRepositoryImpl }); } - @override - Future> refreshPeriodStates() { - return RepositoryErrorHandler.handleApiCall(() async { - final dtos = await remoteDataSource.getAllPeriodStates(); - final localBudgets = await localDataSource.getAllBudgets(); - final byServerId = { - for (final b in localBudgets) - if (b.id != null) b.id!: b.clientId, - }; - final byClientId = {for (final b in localBudgets) b.clientId: b}; - - for (final dto in dtos) { - String? budgetClientId; - if (dto.budgetClientGeneratedId != null && - byClientId.containsKey(dto.budgetClientGeneratedId)) { - budgetClientId = dto.budgetClientGeneratedId; - } else if (dto.budgetId != null) { - budgetClientId = byServerId[dto.budgetId]; - } - if (budgetClientId == null) continue; - - await super.db.into(super.db.budgetPeriodStates).insert( - db.BudgetPeriodStatesCompanion( - id: Value(dto.id), - clientId: Value(dto.clientId), - budgetClientId: Value(budgetClientId), - periodStart: Value(dto.periodStart), - periodEnd: Value(dto.periodEnd), - netSpent: Value(dto.netSpent), - rolloverIn: Value(dto.rolloverIn), - rolloverOut: Value(dto.rolloverOut), - closedAt: Value(dto.closedAt), - lastSyncedAt: Value(dto.lastSyncedAt), - createdAt: dto.createdAt != null - ? Value(dto.createdAt!) - : const Value.absent(), - updatedAt: dto.updatedAt != null - ? Value(dto.updatedAt!) - : const Value.absent(), - ), - mode: InsertMode.insertOrReplace, - ); - } - return unit; - }); - } - Future> _domainTargets(String budgetClientId) async { final resolved = await localDataSource.getResolvedTargetsForBudget(budgetClientId); diff --git a/lib/data/repositories/exchange_rate_imp.dart b/lib/data/repositories/exchange_rate_imp.dart index 7942f2ed..1c13e480 100644 --- a/lib/data/repositories/exchange_rate_imp.dart +++ b/lib/data/repositories/exchange_rate_imp.dart @@ -3,11 +3,11 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:fpdart/fpdart.dart'; import 'package:injectable/injectable.dart'; +import 'package:trakli/core/constants/config_constants.dart'; import 'package:trakli/core/constants/key_constants.dart'; import 'package:trakli/core/error/error_handler.dart'; import 'package:trakli/core/error/failures/failures.dart'; import 'package:trakli/core/error/repository_error_handler.dart'; -import 'package:trakli/core/constants/config_constants.dart'; import 'package:trakli/data/datasources/exchange-rate/exchange_rate_local_datasource.dart'; import 'package:trakli/data/datasources/exchange-rate/exchange_rate_remote_datasource.dart'; import 'package:trakli/data/mappers/exchange_rate_mapper.dart'; @@ -24,6 +24,9 @@ class ExchangeRateRepositoryImpl extends ExchangeRateRepository { final _exchangeRateController = StreamController.broadcast(); + final _rateCachedController = + StreamController.broadcast(); + var defaultCurrencyCode = KeyConstants.defaultCurrencyCode; ExchangeRateRepositoryImpl({ @@ -37,6 +40,10 @@ class ExchangeRateRepositoryImpl extends ExchangeRateRepository { yield* _getAndEmitExchangeRate(); } + @override + Stream get onExchangeRateUpdated => + _rateCachedController.stream; + Stream _getAndEmitExchangeRate() async* { String currencyCode = defaultCurrencyCode; @@ -69,6 +76,8 @@ class ExchangeRateRepositoryImpl extends ExchangeRateRepository { await localDataSource.saveExchangeRate( exchangeRateRemote.baseCode, exchangeRateRemote); + _rateCachedController.add(exchangeRateEntity); + yield exchangeRateEntity; } on DioException catch (err) { throw ErrorHandler.handleDioException(err); @@ -93,6 +102,7 @@ class ExchangeRateRepositoryImpl extends ExchangeRateRepository { ExchangeRateMapper.toDomain(exchangeRateRemote); _exchangeRateController.add(exchangeRateEntity); + _rateCachedController.add(exchangeRateEntity); return exchangeRateEntity; }); @@ -107,6 +117,27 @@ class ExchangeRateRepositoryImpl extends ExchangeRateRepository { }); } + @override + Future getCachedExchangeRate() async { + String currencyCode = defaultCurrencyCode; + + final configResult = await configRepository.getConfigByKey( + ConfigConstants.defaultCurrency, + ); + configResult.fold( + (_) {}, + (config) { + if (config.value != null) { + currencyCode = config.value as String; + } + }, + ); + + final cached = await localDataSource.getExchangeRate(currencyCode); + if (cached == null) return null; + return ExchangeRateMapper.toDomain(cached); + } + @override Future> updateDefaultCurrency( String currencyCode) { @@ -127,6 +158,7 @@ class ExchangeRateRepositoryImpl extends ExchangeRateRepository { final exchangeRateEntity = ExchangeRateMapper.toDomain(exchangeRateRemote); _exchangeRateController.add(exchangeRateEntity); + _rateCachedController.add(exchangeRateEntity); return exchangeRateEntity; } diff --git a/lib/data/services/budget/budget_progress_recomputer.dart b/lib/data/services/budget/budget_progress_recomputer.dart new file mode 100644 index 00000000..68ba3d86 --- /dev/null +++ b/lib/data/services/budget/budget_progress_recomputer.dart @@ -0,0 +1,151 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/data/services/budget/compute_local_progress.dart'; +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/domain/entities/exchange_rate_entity.dart'; +import 'package:trakli/domain/repositories/exchange_rate_repository.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +/// Computes and persists local budget progress, writing the result to the +/// `budgets.progress` column. Server reconciliation later overwrites it. +@lazySingleton +class BudgetProgressRecomputer { + BudgetProgressRecomputer(this._db, this._exchangeRateRepository); + + final AppDatabase _db; + final ExchangeRateRepository _exchangeRateRepository; + + StreamSubscription? _fxSub; + + /// Recomputes all budgets whenever a fresh rate is cached, re-folding + /// foreign-currency transactions that an earlier offline recompute excluded. + /// Call once at app boot; idempotent. + void attachFxSelfHeal() { + if (_fxSub != null) return; + _fxSub = _exchangeRateRepository.onExchangeRateUpdated.listen((_) { + recomputeAll(); + }); + } + + /// Cancels the self-heal subscription (mainly for tests). + Future dispose() async { + await _fxSub?.cancel(); + _fxSub = null; + } + + /// Recompute progress for a single budget by its client id. + Future recomputeFor(String budgetClientId) async { + final budget = await (_db.select(_db.budgets) + ..where((b) => b.clientId.equals(budgetClientId))) + .getSingleOrNull(); + if (budget == null) return; + await _recomputeAndWrite(budget); + } + + /// Recompute every active budget affected by the given transaction (empty + /// targets = catch-all, else a target must match its wallet/group/category). + Future recomputeAffectedBy({ + required String walletClientId, + String? groupClientId, + required Set categoryClientIds, + }) async { + final activeBudgets = await (_db.select(_db.budgets) + ..where((b) => b.isActive.equals(true))) + .get(); + + for (final b in activeBudgets) { + final targets = await _targetsFor(b.clientId); + final affects = targets.isEmpty || + targets.any((t) { + switch (t.targetType) { + case BudgetTargetType.wallet: + return t.targetClientId == walletClientId; + case BudgetTargetType.group: + return groupClientId != null && + t.targetClientId == groupClientId; + case BudgetTargetType.category: + return categoryClientIds.contains(t.targetClientId); + } + }); + if (!affects) continue; + await _recomputeAndWrite(b, targets: targets); + } + } + + /// Recompute progress for every active budget. + Future recomputeAll() async { + final activeBudgets = await (_db.select(_db.budgets) + ..where((b) => b.isActive.equals(true))) + .get(); + for (final b in activeBudgets) { + await _recomputeAndWrite(b); + } + } + + Future _recomputeAndWrite( + Budget budget, { + List? targets, + }) async { + final ts = targets ?? await _targetsFor(budget.clientId); + final txns = await _txnInputs(); + // Cache-only, offline-safe read; when absent, foreign-currency txns are + // excluded until a rate is cached (see attachFxSelfHeal). + final exchangeRate = await _exchangeRateRepository.getCachedExchangeRate(); + final progress = computeLocalProgress( + budget: budget, + targets: ts, + txns: txns, + lastKnownProgress: budget.progress, + now: DateTime.now().toUtc(), + exchangeRate: exchangeRate, + ); + await (_db.update(_db.budgets) + ..where((b) => b.clientId.equals(budget.clientId))) + .write(BudgetsCompanion(progress: Value(progress))); + } + + Future> _targetsFor(String budgetClientId) { + return (_db.select(_db.budgetTargets) + ..where((t) => t.budgetClientId.equals(budgetClientId))) + .get(); + } + + /// Joins transactions with their category client ids and wallet currency. + Future> _txnInputs() async { + final txns = await _db.select(_db.transactions).get(); + if (txns.isEmpty) return const []; + + final catRows = await (_db.select(_db.categorizables) + ..where((c) => + c.categorizableType.equals(CategorizableType.transaction.name))) + .get(); + final catsByTxn = >{}; + for (final row in catRows) { + catsByTxn + .putIfAbsent(row.categorizableId, () => {}) + .add(row.categoryClientId); + } + + // One-shot wallet → currency lookup so the per-txn cost stays O(1). + final wallets = await _db.select(_db.wallets).get(); + final currencyByWallet = { + for (final w in wallets) w.clientId: w.currency, + }; + + return [ + for (final t in txns) + BudgetTxnInput( + type: t.type, + amount: t.amount, + datetime: t.datetime, + transferId: t.transferId, + walletClientId: t.walletClientId, + walletCurrency: currencyByWallet[t.walletClientId], + groupClientId: t.groupClientId, + categoryClientIds: catsByTxn[t.clientId] ?? const {}, + ), + ]; + } +} diff --git a/lib/data/services/budget/compute_local_progress.dart b/lib/data/services/budget/compute_local_progress.dart new file mode 100644 index 00000000..4f5afe9c --- /dev/null +++ b/lib/data/services/budget/compute_local_progress.dart @@ -0,0 +1,187 @@ +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/domain/entities/budget_progress_entity.dart'; +import 'package:trakli/domain/entities/exchange_rate_entity.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +/// A transaction enriched with the category client ids it is tagged with. +class BudgetTxnInput { + final TransactionType type; + final double amount; + final DateTime? datetime; + final int? transferId; + final String walletClientId; + final String? walletCurrency; + final String? groupClientId; + final Set categoryClientIds; + + const BudgetTxnInput({ + required this.type, + required this.amount, + required this.datetime, + required this.transferId, + required this.walletClientId, + required this.walletCurrency, + required this.groupClientId, + required this.categoryClientIds, + }); +} + +BudgetProgressEntity computeLocalProgress({ + required Budget budget, + required List targets, + required List txns, + required BudgetProgressEntity? lastKnownProgress, + required DateTime now, + ExchangeRateEntity? exchangeRate, +}) { + final (start, end) = _periodWindow(budget, now); + + final categoryTargetIds = { + for (final t in targets) + if (t.targetType == BudgetTargetType.category) t.targetClientId, + }; + final groupTargetIds = { + for (final t in targets) + if (t.targetType == BudgetTargetType.group) t.targetClientId, + }; + final walletTargetIds = { + for (final t in targets) + if (t.targetType == BudgetTargetType.wallet) t.targetClientId, + }; + final emptyTargets = targets.isEmpty; + + double grossSpent = 0; + + for (final t in txns) { + if (t.transferId != null) continue; + if (t.type != TransactionType.expense) continue; + final dt = t.datetime; + if (dt == null) continue; + if (dt.isBefore(start) || dt.isAfter(end)) continue; + + final matches = emptyTargets || + walletTargetIds.contains(t.walletClientId) || + (t.groupClientId != null && groupTargetIds.contains(t.groupClientId)) || + t.categoryClientIds.any(categoryTargetIds.contains); + if (!matches) continue; + + // Skip unresolved currencies — under-count rather than misattribute. + final txnCurrency = t.walletCurrency; + if (txnCurrency == null) continue; + + if (txnCurrency != budget.currency) { + final converted = exchangeRate == null + ? null + : _convert(t.amount, txnCurrency, budget.currency, exchangeRate); + if (converted == null) { + // No usable rate — exclude until one is cached (see attachFxSelfHeal). + continue; + } + grossSpent += converted; + continue; + } + + grossSpent += t.amount; + } + + // Refunds aren't available locally; server reconciliation corrects this. + const refunds = 0.0; + final netSpent = grossSpent; + + final rolloverIn = lastKnownProgress?.rolloverIn ?? 0.0; + final effectiveLimit = budget.amount + rolloverIn; + final remaining = effectiveLimit - netSpent; + + final double percentUsed; + if (effectiveLimit <= 0) { + percentUsed = netSpent > 0 ? 100.0 : 0.0; + } else { + final raw = (netSpent / effectiveLimit) * 100; + percentUsed = raw > 999.0 ? 999.0 : raw; + } + + final isThresholdCrossed = percentUsed >= budget.thresholdPercent; + final status = _deriveStatus( + netSpent: netSpent, + effectiveLimit: effectiveLimit, + percentUsed: percentUsed, + isThresholdCrossed: isThresholdCrossed, + ); + + return BudgetProgressEntity( + periodStart: start, + periodEnd: end, + limit: budget.amount, + grossSpent: grossSpent, + refunds: refunds, + netSpent: netSpent, + rolloverIn: rolloverIn, + effectiveLimit: effectiveLimit, + remaining: remaining, + percentUsed: percentUsed, + // Projection/forecast are server-only signals; default locally. + projectedSpend: netSpent, + status: status, + isThresholdCrossed: isThresholdCrossed, + isForecastBreach: false, + ); +} + +/// Converts [amount] from [from] to [to] via the base-relative snapshot +/// (`amount / rate(from) * rate(to)`). Returns `null` when a rate is missing +/// or the source rate is zero, so the caller can exclude the transaction. +double? _convert(double amount, String from, String to, ExchangeRateEntity fx) { + if (from == to) return amount; + final rateFrom = from == fx.baseCode ? 1.0 : fx.rates[from]; + final rateTo = to == fx.baseCode ? 1.0 : fx.rates[to]; + if (rateFrom == null || rateTo == null || rateFrom == 0) return null; + return amount / rateFrom * rateTo; +} + +BudgetStatus _deriveStatus({ + required double netSpent, + required double effectiveLimit, + required double percentUsed, + required bool isThresholdCrossed, +}) { + final overBudget = + (effectiveLimit <= 0 && netSpent > 0) || netSpent > effectiveLimit; + if (overBudget) return BudgetStatus.overBudget; + if (isThresholdCrossed) return BudgetStatus.nearLimit; + return BudgetStatus.onTrack; +} + +(DateTime, DateTime) _periodWindow(Budget budget, DateTime now) { + // Clamp the reference to start_date so pre-start dates still yield a window. + final ref = now.isBefore(budget.startDate) ? budget.startDate : now; + + switch (budget.periodType) { + case BudgetPeriodType.weekly: + final dayOnly = DateTime.utc(ref.year, ref.month, ref.day); + final monday = dayOnly.subtract(Duration(days: ref.weekday - 1)); + final sundayEnd = DateTime.utc(monday.year, monday.month, monday.day) + .add(const Duration(days: 7)) + .subtract(const Duration(microseconds: 1)); + return (monday, sundayEnd); + + case BudgetPeriodType.monthly: + final start = DateTime.utc(ref.year, ref.month, 1); + final nextMonth = ref.month == 12 + ? DateTime.utc(ref.year + 1, 1, 1) + : DateTime.utc(ref.year, ref.month + 1, 1); + final end = nextMonth.subtract(const Duration(microseconds: 1)); + return (start, end); + + case BudgetPeriodType.yearly: + final start = DateTime.utc(ref.year, 1, 1); + final end = DateTime.utc(ref.year + 1, 1, 1) + .subtract(const Duration(microseconds: 1)); + return (start, end); + + case BudgetPeriodType.custom: + final start = budget.startDate; + final end = budget.endDate ?? + DateTime.utc(start.year + 100, start.month, start.day); + return (start, end); + } +} diff --git a/lib/data/services/budget/period_state_client_id.dart b/lib/data/services/budget/period_state_client_id.dart new file mode 100644 index 00000000..e2f0a904 --- /dev/null +++ b/lib/data/services/budget/period_state_client_id.dart @@ -0,0 +1,5 @@ +/// Deterministic local client id for a server-authored `BudgetPeriodState`, +/// minted from its immutable server `id` so re-syncs `insertOrReplace` +/// idempotently. The `bps:server-` prefix distinguishes these from real +/// device-scoped client ids. +String periodStateClientId(int serverId) => 'bps:server-$serverId'; diff --git a/lib/data/sync/budget_period_state_sync_handler.dart b/lib/data/sync/budget_period_state_sync_handler.dart new file mode 100644 index 00000000..5a900524 --- /dev/null +++ b/lib/data/sync/budget_period_state_sync_handler.dart @@ -0,0 +1,232 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sync_core/drift_sync_core.dart'; +import 'package:injectable/injectable.dart'; +import 'package:trakli/data/services/budget/period_state_client_id.dart'; +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/data/database/tables/budget_period_states.dart'; +import 'package:trakli/data/datasources/budget/budget_remote_datasource.dart'; +import 'package:trakli/data/datasources/budget/dtos/budget_period_state_dto.dart'; + +/// Read-only sync handler for `BudgetPeriodState`. The backend is the sole +/// writer ([CloseBudgetPeriodJob]); the mobile never pushes. Server responses + +@lazySingleton +class BudgetPeriodStateSyncHandler + extends SyncTypeHandler + with RestSyncTypeHandler { + static const String entity = 'budget_period_state'; + + BudgetPeriodStateSyncHandler(this.db, this.remoteDataSource); + + final AppDatabase db; + final BudgetRemoteDataSource remoteDataSource; + + TableInfo get table => + db.budgetPeriodStates; + + @override + String get entityType => BudgetPeriodStateSyncHandler.entity; + + @override + String getClientId(BudgetPeriodStateDto entity) => entity.clientId; + + @override + int? getServerId(BudgetPeriodStateDto entity) => entity.id; + + @override + String getRev(BudgetPeriodStateDto entity) => '1'; + + @override + DateTime? getLastSyncedAt(BudgetPeriodStateDto entity) => entity.lastSyncedAt; + + @override + Future unmarshal( + Map entityJson) async { + return BudgetPeriodStateDto.fromJson(entityJson); + } + + @override + Map marshal(BudgetPeriodStateDto entity) { + // Read-only entity from the client's perspective — never pushed upstream. + return const {}; + } + + @override + Future shouldPersistRemote(BudgetPeriodStateDto entity) async => false; + + @override + Future> restGetAllRemote({ + bool? noClientId, + DateTime? syncedSince, + }) { + // Budget period state doesn't have a clientId + // handles all our persistence. + if (noClientId == true) { + return Future.value(const []); + } + return remoteDataSource.getAllPeriodStates( + syncedSince: syncedSince, + noClientId: noClientId, + ); + } + + @override + Future restGetRemote(int id) async { + // Server exposes only a list endpoint; fall back to scanning the latest + // page. In practice the sync flow uses restGetAllRemote, so this is rarely + // exercised. + final all = await remoteDataSource.getAllPeriodStates(); + for (final dto in all) { + if (dto.id == id) return dto; + } + return null; + } + + @override + Future restPutRemote( + BudgetPeriodStateDto entity) async { + // Server is the sole writer for period states — no-op. + return entity; + } + + @override + Future restDeleteRemote(BudgetPeriodStateDto entity) async { + // Server is the sole writer for period states — no-op. + } + + @override + Future assignClientId(BudgetPeriodStateDto item) async { + final budgetClientId = await _resolveBudgetClientId(item); + if (budgetClientId == null || budgetClientId.isEmpty) { + // Orphan period state — no local budget matches. Leave clientId empty; + // upsertLocal will skip it. Next sync will pick it up once the parent + // budget is also synced. + return item.copyWith(clientId: ''); + } + final serverId = item.id; + if (serverId == null) { + // Period states only originate server-side, so a missing server id is + // unexpected — defensively treat it as a skip and let the next sync + // pick it up once the server replays it with an id. + return item.copyWith(clientId: ''); + } + return item.copyWith( + budgetClientGeneratedId: budgetClientId, + clientId: periodStateClientId(serverId), + ); + } + + @override + Future deleteAllLocal() async { + await table.deleteAll(); + } + + @override + Future deleteLocalNotIn(Set clientIds) async { + if (clientIds.isEmpty) return; + await (db.delete(table)..where((t) => t.clientId.isNotIn(clientIds))).go(); + } + + @override + Future deleteLocal(BudgetPeriodStateDto entity) async { + if (entity.clientId.isEmpty) return; + await table.deleteWhere((t) => t.clientId.equals(entity.clientId)); + } + + @override + Future upsertLocal(BudgetPeriodStateDto dto) async { + // The framework's down-sync path (`_timeBasedPartialResync`) does NOT call + // `assignClientId` before persisting — and the server never sends a + // `client_generated_id` for period states. Assign one inline so the row + // gets persisted on every regular sync. + final resolved = dto.clientId.isEmpty ? await assignClientId(dto) : dto; + + final budgetClientId = resolved.budgetClientGeneratedId; + if (budgetClientId == null || budgetClientId.isEmpty) return; + if (resolved.clientId.isEmpty) { + // orphan — parent budget not synced yet, or server id missing + return; + } + + await db.into(table).insert( + BudgetPeriodStatesCompanion( + id: Value(resolved.id), + clientId: Value(resolved.clientId), + budgetClientId: Value(budgetClientId), + periodStart: Value(resolved.periodStart), + periodEnd: Value(resolved.periodEnd), + netSpent: Value(resolved.netSpent), + rolloverIn: Value(resolved.rolloverIn), + rolloverOut: Value(resolved.rolloverOut), + closedAt: Value(resolved.closedAt), + lastSyncedAt: Value(resolved.lastSyncedAt), + createdAt: resolved.createdAt != null + ? Value(resolved.createdAt!) + : const Value.absent(), + updatedAt: resolved.updatedAt != null + ? Value(resolved.updatedAt!) + : const Value.absent(), + ), + mode: InsertMode.insertOrReplace, + ); + } + + @override + Future upsertAllLocal(List list) async { + for (final dto in list) { + await upsertLocal(dto); + } + } + + @override + Future getLocalByClientId(String clientId) async { + final row = await (db.select(table) + ..where((t) => t.clientId.equals(clientId))) + .getSingleOrNull(); + if (row == null) { + throw Exception('BudgetPeriodState not found'); + } + return _rowToDto(row); + } + + @override + Future getLocalByServerId(int serverId) async { + final row = await (db.select(table)..where((t) => t.id.equals(serverId))) + .getSingleOrNull(); + if (row == null) return null; + return _rowToDto(row); + } + + Future _resolveBudgetClientId(BudgetPeriodStateDto item) async { + final fromDto = item.budgetClientGeneratedId; + if (fromDto != null && fromDto.isNotEmpty) { + final localBudget = await (db.select(db.budgets) + ..where((b) => b.clientId.equals(fromDto))) + .getSingleOrNull(); + if (localBudget != null) return fromDto; + } + final serverId = item.budgetId; + if (serverId != null) { + final localBudget = await (db.select(db.budgets) + ..where((b) => b.id.equals(serverId))) + .getSingleOrNull(); + return localBudget?.clientId; + } + return null; + } + + BudgetPeriodStateDto _rowToDto(BudgetPeriodState row) { + return BudgetPeriodStateDto( + id: row.id, + budgetClientGeneratedId: row.budgetClientId, + clientId: row.clientId, + periodStart: row.periodStart, + periodEnd: row.periodEnd, + netSpent: row.netSpent, + rolloverIn: row.rolloverIn, + rolloverOut: row.rolloverOut, + closedAt: row.closedAt, + lastSyncedAt: row.lastSyncedAt, + ); + } +} diff --git a/lib/data/sync/budget_sync_handler.dart b/lib/data/sync/budget_sync_handler.dart index d4719061..c2e60b55 100644 --- a/lib/data/sync/budget_sync_handler.dart +++ b/lib/data/sync/budget_sync_handler.dart @@ -7,6 +7,7 @@ import 'package:trakli/data/database/tables/budgets.dart'; import 'package:trakli/data/database/tables/sync_table.dart'; import 'package:trakli/data/datasources/budget/budget_remote_datasource.dart'; import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart'; +import 'package:trakli/data/mappers/budget_mapper.dart'; @lazySingleton class BudgetSyncHandler @@ -185,6 +186,9 @@ class BudgetSyncHandler isActive: Value(budget.isActive), ownerType: Value(budget.ownerType), ownerId: Value(budget.ownerId), + progress: Value(entity.progress != null + ? BudgetMapper.progressFromDto(entity.progress!) + : null), createdAt: Value(budget.createdAt), updatedAt: Value(budget.updatedAt), lastSyncedAt: Value(budget.lastSyncedAt), diff --git a/lib/di/injection.config.dart b/lib/di/injection.config.dart index 3e0dc0fc..c8d61288 100644 --- a/lib/di/injection.config.dart +++ b/lib/di/injection.config.dart @@ -94,6 +94,8 @@ import '../data/repositories/subscription_repository_imp.dart' as _i1047; import '../data/repositories/transaction_repository_impl.dart' as _i114; import '../data/repositories/transfer_repository_impl.dart' as _i268; import '../data/repositories/wallet_repository_impl.dart' as _i305; +import '../data/services/budget/budget_progress_recomputer.dart' as _i176; +import '../data/sync/budget_period_state_sync_handler.dart' as _i161; import '../data/sync/budget_sync_handler.dart' as _i918; import '../data/sync/category_sync_handler.dart' as _i463; import '../data/sync/config_sync_handler.dart' as _i480; @@ -156,7 +158,6 @@ import '../domain/usecases/budget/listen_to_budgets_usecase.dart' as _i377; import '../domain/usecases/budget/listen_to_period_states_usecase.dart' as _i920; import '../domain/usecases/budget/listen_to_targets_usecase.dart' as _i442; -import '../domain/usecases/budget/refresh_period_states_usecase.dart' as _i141; import '../domain/usecases/budget/update_budget_usecase.dart' as _i617; import '../domain/usecases/category/add_category_usecase.dart' as _i445; import '../domain/usecases/category/delete_category_usecase.dart' as _i292; @@ -323,8 +324,6 @@ _i174.GetIt $initGetIt( () => _i849.WalletLocalDataSourceImpl(database: gh<_i704.AppDatabase>())); gh.factory<_i514.ConfigLocalDataSource>( () => _i514.ConfigLocalDataSourceImpl(database: gh<_i704.AppDatabase>())); - gh.factory<_i293.BudgetLocalDataSource>( - () => _i293.BudgetLocalDataSourceImpl(gh<_i704.AppDatabase>())); gh.factory<_i148.CategoryLocalDataSource>( () => _i148.CategoryLocalDataSourceImpl(gh<_i704.AppDatabase>())); gh.lazySingleton<_i361.Dio>( @@ -382,11 +381,6 @@ _i174.GetIt $initGetIt( () => _i481.UserContextService(gh<_i538.CrashReportingService>())); gh.lazySingleton<_i542.AiRepository>( () => _i841.AiRepositoryImpl(remote: gh<_i514.AiRemoteDataSource>())); - gh.factory<_i662.TransactionLocalDataSource>( - () => _i662.TransactionLocalDataSourceImpl( - gh<_i704.AppDatabase>(), - gh<_i216.MediaFileLocalDataSource>(), - )); gh.lazySingleton<_i32.ImportRepository>(() => _i337.ImportRepositoryImpl( remoteDataSource: gh<_i76.ImportRemoteDataSource>())); gh.factory<_i587.ConfigRemoteDataSource>( @@ -464,11 +458,6 @@ _i174.GetIt $initGetIt( db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.lazySingleton<_i225.TransferSyncHandler>(() => _i225.TransferSyncHandler( - gh<_i704.AppDatabase>(), - gh<_i783.TransferRemoteDataSource>(), - gh<_i662.TransactionLocalDataSource>(), - )); gh.singleton<_i800.AuthRepository>(() => _i135.AuthRepositoryImpl( remoteDataSource: gh<_i496.AuthRemoteDataSource>(), localDataSource: gh<_i276.AuthLocalDataSource>(), @@ -508,13 +497,11 @@ _i174.GetIt $initGetIt( gh<_i704.AppDatabase>(), gh<_i760.BudgetRemoteDataSource>(), )); - gh.lazySingleton<_i118.TransactionRepository>(() => - _i114.TransactionRepositoryImpl( - syncHandler: gh<_i893.TransactionSyncHandler>(), - localDataSource: gh<_i662.TransactionLocalDataSource>(), - db: gh<_i704.AppDatabase>(), - requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), - )); + gh.lazySingleton<_i161.BudgetPeriodStateSyncHandler>( + () => _i161.BudgetPeriodStateSyncHandler( + gh<_i704.AppDatabase>(), + gh<_i760.BudgetRemoteDataSource>(), + )); gh.factory<_i929.GetImportSessionsUseCase>( () => _i929.GetImportSessionsUseCase(gh<_i32.ImportRepository>())); gh.factory<_i36.ConfirmSessionUseCase>( @@ -523,13 +510,6 @@ _i174.GetIt $initGetIt( () => _i661.GetImportSessionUseCase(gh<_i32.ImportRepository>())); gh.factory<_i60.AnalyzeDocumentUseCase>( () => _i60.AnalyzeDocumentUseCase(gh<_i32.ImportRepository>())); - gh.lazySingleton<_i340.BudgetRepository>(() => _i81.BudgetRepositoryImpl( - syncHandler: gh<_i918.BudgetSyncHandler>(), - localDataSource: gh<_i293.BudgetLocalDataSource>(), - remoteDataSource: gh<_i760.BudgetRemoteDataSource>(), - db: gh<_i704.AppDatabase>(), - requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), - )); gh.factory<_i56.DeletePartyUseCase>( () => _i56.DeletePartyUseCase(gh<_i661.PartyRepository>())); gh.factory<_i84.AddPartyUseCase>( @@ -579,14 +559,6 @@ _i174.GetIt $initGetIt( db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), )); - gh.factory<_i163.DeleteTransactionUseCase>( - () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i973.ListenToTransactionsUseCase>(() => - _i973.ListenToTransactionsUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i241.UpdateTransactionUseCase>( - () => _i241.UpdateTransactionUseCase(gh<_i118.TransactionRepository>())); - gh.factory<_i947.GetAllTransactionsUseCase>( - () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); gh.factory<_i415.AiChatCubit>(() => _i415.AiChatCubit( listSessionsUseCase: gh<_i505.ListSessionsUseCase>(), getSessionUseCase: gh<_i752.GetSessionUseCase>(), @@ -632,14 +604,6 @@ _i174.GetIt $initGetIt( () => _i436.UpdateConfigUseCase(gh<_i899.ConfigRepository>())); gh.factory<_i536.DeleteConfigUseCase>( () => _i536.DeleteConfigUseCase(gh<_i899.ConfigRepository>())); - gh.lazySingleton<_i55.TransferRepository>(() => _i268.TransferRepositoryImpl( - syncHandler: gh<_i225.TransferSyncHandler>(), - localDataSource: gh<_i432.TransferLocalDataSource>(), - transactionLocalDataSource: gh<_i662.TransactionLocalDataSource>(), - transactionRepository: gh<_i114.TransactionRepositoryImpl>(), - db: gh<_i704.AppDatabase>(), - requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), - )); gh.singleton<_i1057.ExchangeRateRepository>( () => _i827.ExchangeRateRepositoryImpl( remoteDataSource: gh<_i632.ExchangeRateRemoteDataSource>(), @@ -677,30 +641,6 @@ _i174.GetIt $initGetIt( gh<_i542.PasswordResetCodeUseCase>(), gh<_i494.PasswordResetUseCase>(), )); - gh.factory<_i920.ListenToPeriodStatesUseCase>( - () => _i920.ListenToPeriodStatesUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i102.GetBudgetUseCase>( - () => _i102.GetBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i377.ListenToBudgetsUseCase>( - () => _i377.ListenToBudgetsUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i893.CloseBudgetPeriodUseCase>( - () => _i893.CloseBudgetPeriodUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i442.ListenToTargetsUseCase>( - () => _i442.ListenToTargetsUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i617.UpdateBudgetUseCase>( - () => _i617.UpdateBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i141.RefreshPeriodStatesUseCase>( - () => _i141.RefreshPeriodStatesUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i363.InsertBudgetUseCase>( - () => _i363.InsertBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i59.FetchBudgetTransactionsUseCase>( - () => _i59.FetchBudgetTransactionsUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i748.DeleteBudgetUseCase>( - () => _i748.DeleteBudgetUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i598.FetchBudgetProgressUseCase>( - () => _i598.FetchBudgetProgressUseCase(gh<_i340.BudgetRepository>())); - gh.factory<_i884.GetAllBudgetsUseCase>( - () => _i884.GetAllBudgetsUseCase(gh<_i340.BudgetRepository>())); gh.factory<_i831.RegisterCubit>(() => _i831.RegisterCubit( gh<_i705.RegisterUseCase>(), gh<_i402.GetOtpCodeUseCase>(), @@ -732,13 +672,6 @@ _i174.GetIt $initGetIt( ensureDefaultWalletExistsUseCase: gh<_i225.EnsureDefaultWalletExistsUseCase>(), )); - gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => - _i1004.CreateTransferWithTransactionsUseCase( - gh<_i55.TransferRepository>())); - gh.factory<_i453.AddTransferUseCase>( - () => _i453.AddTransferUseCase(gh<_i55.TransferRepository>())); - gh.factory<_i744.ListenToTransfersUseCase>( - () => _i744.ListenToTransfersUseCase(gh<_i55.TransferRepository>())); gh.factory<_i397.ListenExchangeRate>( () => _i397.ListenExchangeRate(gh<_i1057.ExchangeRateRepository>())); gh.factory<_i759.DeleteGroupUseCase>( @@ -751,23 +684,10 @@ _i174.GetIt $initGetIt( () => _i353.AddGroupUseCase(gh<_i957.GroupRepository>())); gh.factory<_i820.UpdateGroupUseCase>( () => _i820.UpdateGroupUseCase(gh<_i957.GroupRepository>())); - gh.factory<_i611.TransferCubit>(() => _i611.TransferCubit( - createTransferWithTransactionsUseCase: - gh<_i1004.CreateTransferWithTransactionsUseCase>(), - listenToTransfersUseCase: gh<_i744.ListenToTransfersUseCase>(), - )); - gh.lazySingleton>>( - () => syncModule.provideSyncTypeHandlers( - gh<_i463.CategorySyncHandler>(), - gh<_i480.ConfigSyncHandler>(), - gh<_i849.WalletSyncHandler>(), - gh<_i280.PartySyncHandler>(), - gh<_i235.GroupSyncHandler>(), - gh<_i217.NotificationSyncHandler>(), - gh<_i893.TransactionSyncHandler>(), - gh<_i225.TransferSyncHandler>(), - gh<_i382.MediaSyncHandler>(), - gh<_i918.BudgetSyncHandler>(), + gh.lazySingleton<_i176.BudgetProgressRecomputer>( + () => _i176.BudgetProgressRecomputer( + gh<_i704.AppDatabase>(), + gh<_i1057.ExchangeRateRepository>(), )); gh.factory<_i408.ConfigCubit>(() => _i408.ConfigCubit( gh<_i132.GetConfigsUseCase>(), @@ -784,20 +704,6 @@ _i174.GetIt $initGetIt( )); gh.factory<_i977.PlansCubit>( () => _i977.PlansCubit(gh<_i314.FetchSubscriptionPlans>())); - gh.factory<_i1064.BudgetCubit>(() => _i1064.BudgetCubit( - getAllBudgetsUseCase: gh<_i884.GetAllBudgetsUseCase>(), - insertBudgetUseCase: gh<_i363.InsertBudgetUseCase>(), - updateBudgetUseCase: gh<_i617.UpdateBudgetUseCase>(), - deleteBudgetUseCase: gh<_i748.DeleteBudgetUseCase>(), - fetchBudgetProgressUseCase: gh<_i598.FetchBudgetProgressUseCase>(), - fetchBudgetTransactionsUseCase: - gh<_i59.FetchBudgetTransactionsUseCase>(), - closeBudgetPeriodUseCase: gh<_i893.CloseBudgetPeriodUseCase>(), - listenToBudgetsUseCase: gh<_i377.ListenToBudgetsUseCase>(), - listenToTargetsUseCase: gh<_i442.ListenToTargetsUseCase>(), - listenToPeriodStatesUseCase: gh<_i920.ListenToPeriodStatesUseCase>(), - refreshPeriodStatesUseCase: gh<_i141.RefreshPeriodStatesUseCase>(), - )); gh.factory<_i798.UpdateDefaultCurrencyUseCase>(() => _i798.UpdateDefaultCurrencyUseCase(gh<_i1057.ExchangeRateRepository>())); gh.factory<_i676.GroupCubit>(() => _i676.GroupCubit( @@ -808,11 +714,21 @@ _i174.GetIt $initGetIt( gh<_i146.ListenToGroupsUseCase>(), gh<_i833.SaveConfigUseCase>(), )); - gh.factory<_i669.CreateTransactionUseCase>( - () => _i669.CreateTransactionUseCase( - gh<_i118.TransactionRepository>(), - gh<_i1057.ExchangeRateRepository>(), + gh.factory<_i662.TransactionLocalDataSource>( + () => _i662.TransactionLocalDataSourceImpl( + gh<_i704.AppDatabase>(), + gh<_i216.MediaFileLocalDataSource>(), + gh<_i176.BudgetProgressRecomputer>(), )); + gh.lazySingleton<_i225.TransferSyncHandler>(() => _i225.TransferSyncHandler( + gh<_i704.AppDatabase>(), + gh<_i783.TransferRemoteDataSource>(), + gh<_i662.TransactionLocalDataSource>(), + )); + gh.factory<_i293.BudgetLocalDataSource>(() => _i293.BudgetLocalDataSourceImpl( + gh<_i704.AppDatabase>(), + gh<_i176.BudgetProgressRecomputer>(), + )); gh.factory<_i150.GetFileContentUseCase>( () => _i150.GetFileContentUseCase(gh<_i442.MediaRepository>())); gh.factory<_i706.DeleteMediaUseCase>( @@ -827,17 +743,57 @@ _i174.GetIt $initGetIt( gh<_i608.ListenToConfigsUseCase>(), gh<_i798.UpdateDefaultCurrencyUseCase>(), )); + gh.lazySingleton<_i118.TransactionRepository>(() => + _i114.TransactionRepositoryImpl( + syncHandler: gh<_i893.TransactionSyncHandler>(), + localDataSource: gh<_i662.TransactionLocalDataSource>(), + db: gh<_i704.AppDatabase>(), + requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), + )); gh.factory<_i311.ExchangeRateCubit>( () => _i311.ExchangeRateCubit(gh<_i397.ListenExchangeRate>())); - gh.lazySingleton<_i646.SynchAppDatabase>(() => _i646.SynchAppDatabase( - appDatabase: gh<_i704.AppDatabase>(), - typeHandlers: - gh>>(), - dependencyManager: gh<_i877.SyncDependencyManagerBase>(), + gh.lazySingleton<_i340.BudgetRepository>(() => _i81.BudgetRepositoryImpl( + syncHandler: gh<_i918.BudgetSyncHandler>(), + localDataSource: gh<_i293.BudgetLocalDataSource>(), + remoteDataSource: gh<_i760.BudgetRemoteDataSource>(), + db: gh<_i704.AppDatabase>(), requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), - logger: gh<_i877.SyncLogger>(), - crashReporter: gh<_i877.SyncCrashReporter>(), )); + gh.factory<_i163.DeleteTransactionUseCase>( + () => _i163.DeleteTransactionUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i973.ListenToTransactionsUseCase>(() => + _i973.ListenToTransactionsUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i241.UpdateTransactionUseCase>( + () => _i241.UpdateTransactionUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i947.GetAllTransactionsUseCase>( + () => _i947.GetAllTransactionsUseCase(gh<_i118.TransactionRepository>())); + gh.factory<_i669.CreateTransactionUseCase>( + () => _i669.CreateTransactionUseCase( + gh<_i118.TransactionRepository>(), + gh<_i1057.ExchangeRateRepository>(), + )); + gh.lazySingleton<_i55.TransferRepository>(() => _i268.TransferRepositoryImpl( + syncHandler: gh<_i225.TransferSyncHandler>(), + localDataSource: gh<_i432.TransferLocalDataSource>(), + transactionLocalDataSource: gh<_i662.TransactionLocalDataSource>(), + transactionRepository: gh<_i114.TransactionRepositoryImpl>(), + db: gh<_i704.AppDatabase>(), + requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), + )); + gh.lazySingleton>>( + () => syncModule.provideSyncTypeHandlers( + gh<_i463.CategorySyncHandler>(), + gh<_i480.ConfigSyncHandler>(), + gh<_i849.WalletSyncHandler>(), + gh<_i280.PartySyncHandler>(), + gh<_i235.GroupSyncHandler>(), + gh<_i217.NotificationSyncHandler>(), + gh<_i893.TransactionSyncHandler>(), + gh<_i225.TransferSyncHandler>(), + gh<_i382.MediaSyncHandler>(), + gh<_i918.BudgetSyncHandler>(), + gh<_i161.BudgetPeriodStateSyncHandler>(), + )); gh.factory<_i117.TransactionCubit>(() => _i117.TransactionCubit( getAllTransactionsUseCase: gh<_i1022.GetAllTransactionsUseCase>(), createTransactionUseCase: gh<_i1022.CreateTransactionUseCase>(), @@ -851,6 +807,62 @@ _i174.GetIt $initGetIt( listenToTransactionsUseCase: gh<_i1022.ListenToTransactionsUseCase>(), getWalletsUseCase: gh<_i713.GetWalletsUseCase>(), )); + gh.factory<_i102.GetBudgetUseCase>( + () => _i102.GetBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i377.ListenToBudgetsUseCase>( + () => _i377.ListenToBudgetsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i893.CloseBudgetPeriodUseCase>( + () => _i893.CloseBudgetPeriodUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i442.ListenToTargetsUseCase>( + () => _i442.ListenToTargetsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i617.UpdateBudgetUseCase>( + () => _i617.UpdateBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i363.InsertBudgetUseCase>( + () => _i363.InsertBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i59.FetchBudgetTransactionsUseCase>( + () => _i59.FetchBudgetTransactionsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i748.DeleteBudgetUseCase>( + () => _i748.DeleteBudgetUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i598.FetchBudgetProgressUseCase>( + () => _i598.FetchBudgetProgressUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i884.GetAllBudgetsUseCase>( + () => _i884.GetAllBudgetsUseCase(gh<_i340.BudgetRepository>())); + gh.factory<_i920.ListenToPeriodStatesUseCase>( + () => _i920.ListenToPeriodStatesUseCase(gh<_i340.BudgetRepository>())); + gh.lazySingleton<_i646.SynchAppDatabase>(() => _i646.SynchAppDatabase( + appDatabase: gh<_i704.AppDatabase>(), + typeHandlers: + gh>>(), + dependencyManager: gh<_i877.SyncDependencyManagerBase>(), + requestAuthorizationService: gh<_i877.RequestAuthorizationService>(), + logger: gh<_i877.SyncLogger>(), + crashReporter: gh<_i877.SyncCrashReporter>(), + )); + gh.factory<_i1004.CreateTransferWithTransactionsUseCase>(() => + _i1004.CreateTransferWithTransactionsUseCase( + gh<_i55.TransferRepository>())); + gh.factory<_i453.AddTransferUseCase>( + () => _i453.AddTransferUseCase(gh<_i55.TransferRepository>())); + gh.factory<_i744.ListenToTransfersUseCase>( + () => _i744.ListenToTransfersUseCase(gh<_i55.TransferRepository>())); + gh.factory<_i611.TransferCubit>(() => _i611.TransferCubit( + createTransferWithTransactionsUseCase: + gh<_i1004.CreateTransferWithTransactionsUseCase>(), + listenToTransfersUseCase: gh<_i744.ListenToTransfersUseCase>(), + )); + gh.factory<_i1064.BudgetCubit>(() => _i1064.BudgetCubit( + getAllBudgetsUseCase: gh<_i884.GetAllBudgetsUseCase>(), + insertBudgetUseCase: gh<_i363.InsertBudgetUseCase>(), + updateBudgetUseCase: gh<_i617.UpdateBudgetUseCase>(), + deleteBudgetUseCase: gh<_i748.DeleteBudgetUseCase>(), + fetchBudgetProgressUseCase: gh<_i598.FetchBudgetProgressUseCase>(), + fetchBudgetTransactionsUseCase: + gh<_i59.FetchBudgetTransactionsUseCase>(), + closeBudgetPeriodUseCase: gh<_i893.CloseBudgetPeriodUseCase>(), + listenToBudgetsUseCase: gh<_i377.ListenToBudgetsUseCase>(), + listenToTargetsUseCase: gh<_i442.ListenToTargetsUseCase>(), + listenToPeriodStatesUseCase: gh<_i920.ListenToPeriodStatesUseCase>(), + )); return getIt; } diff --git a/lib/domain/repositories/budget_repository.dart b/lib/domain/repositories/budget_repository.dart index 5be3d8c3..9817975e 100644 --- a/lib/domain/repositories/budget_repository.dart +++ b/lib/domain/repositories/budget_repository.dart @@ -61,6 +61,4 @@ abstract class BudgetRepository { int id, {int limit = 50}); Future> closeBudgetPeriod(int id); - - Future> refreshPeriodStates(); } diff --git a/lib/domain/repositories/exchange_rate_repository.dart b/lib/domain/repositories/exchange_rate_repository.dart index ed9b5b98..50bb62b7 100644 --- a/lib/domain/repositories/exchange_rate_repository.dart +++ b/lib/domain/repositories/exchange_rate_repository.dart @@ -5,10 +5,14 @@ import 'package:trakli/domain/entities/exchange_rate_entity.dart'; abstract class ExchangeRateRepository { Stream get listenToExchangeRate; + Stream get onExchangeRateUpdated; + Future> refreshExchangeRate(); Future> getExchangeRate(); + Future getCachedExchangeRate(); + Future> updateDefaultCurrency( String currencyCode, ); diff --git a/lib/domain/usecases/budget/refresh_period_states_usecase.dart b/lib/domain/usecases/budget/refresh_period_states_usecase.dart deleted file mode 100644 index 57bae1a9..00000000 --- a/lib/domain/usecases/budget/refresh_period_states_usecase.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:injectable/injectable.dart'; -import 'package:trakli/core/error/failures/failures.dart'; -import 'package:trakli/core/usecases/usecase.dart'; -import 'package:trakli/domain/repositories/budget_repository.dart'; - -@injectable -class RefreshPeriodStatesUseCase implements UseCase { - final BudgetRepository _repository; - - RefreshPeriodStatesUseCase(this._repository); - - @override - Future> call(NoParams params) async { - return await _repository.refreshPeriodStates(); - } -} diff --git a/lib/presentation/add_transaction_screen.dart b/lib/presentation/add_transaction_screen.dart index e71000f2..3971444b 100644 --- a/lib/presentation/add_transaction_screen.dart +++ b/lib/presentation/add_transaction_screen.dart @@ -1,9 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:trakli/domain/entities/transaction_complete_entity.dart'; import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/transactions/add_transaction_form_compact_layout.dart'; +import 'package:trakli/presentation/transactions/cubit/transaction_cubit.dart'; import 'package:trakli/presentation/utils/colors.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; import 'package:trakli/presentation/utils/page_app_bar.dart'; @@ -68,90 +70,100 @@ class _AddTransactionScreenState extends State ? LocaleKeys.editTransaction.tr() : LocaleKeys.addTransaction.tr(), ), - body: Column( - children: [ - if (widget.transaction == null) - Padding( - padding: EdgeInsets.fromLTRB(16.w, 14.h, 16.w, 10.h), - child: _TypeSegmented( - controller: tabController, + body: BlocListener( + listenWhen: (previous, current) => + previous.isSaving && !current.isSaving, + listener: (context, state) { + if (!state.failure.hasError && mounted) { + Navigator.pop(context); + } + }, + child: Column( + children: [ + if (widget.transaction == null) + Padding( + padding: EdgeInsets.fromLTRB(16.w, 14.h, 16.w, 10.h), + child: _TypeSegmented( + controller: tabController, + ), ), - ), - Expanded( - child: ColoredBox( - color: context.tones.bgPage, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onHorizontalDragEnd: (details) { - final v = details.primaryVelocity ?? 0; - if (v.abs() < 200) return; - if (v < 0 && tabController.index < tabController.length - 1) { - tabController.animateTo(tabController.index + 1); - } else if (v > 0 && tabController.index > 0) { - tabController.animateTo(tabController.index - 1); - } - }, - child: AnimatedBuilder( - animation: tabController, - builder: (_, __) { - final showExpense = widget.transaction == null || - widget.transaction!.transaction.type == - TransactionType.expense; - final showIncome = widget.transaction == null || - widget.transaction!.transaction.type == - TransactionType.income; - final children = [ - if (showExpense) - KeyedSubtree( - key: const ValueKey('tx-form-expense'), - child: formDisplay == 'full' - ? AddTransactionForm( - transactionType: TransactionType.expense, - accentColor: appDangerColor, - transactionCompleteEntity: - widget.transaction, - ) - : AddTransactionFormCompactLayout( - transactionType: TransactionType.expense, - accentColor: appDangerColor, - transactionCompleteEntity: - widget.transaction, - ), - ), - if (showIncome) - KeyedSubtree( - key: const ValueKey('tx-form-income'), - child: formDisplay == 'full' - ? AddTransactionForm( - accentColor: appPrimaryColor, - transactionCompleteEntity: - widget.transaction, - ) - : AddTransactionFormCompactLayout( - accentColor: appPrimaryColor, - transactionCompleteEntity: - widget.transaction, - ), - ), - if (widget.transaction == null) - const KeyedSubtree( - key: ValueKey('tx-form-transfer'), - child: WalletTransferScreen(embedded: true), - ), - ]; - return IndexedStack( - index: tabController.index.clamp( - 0, - children.length - 1, - ), - children: children, - ); + Expanded( + child: ColoredBox( + color: context.tones.bgPage, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onHorizontalDragEnd: (details) { + final v = details.primaryVelocity ?? 0; + if (v.abs() < 200) return; + if (v < 0 && + tabController.index < tabController.length - 1) { + tabController.animateTo(tabController.index + 1); + } else if (v > 0 && tabController.index > 0) { + tabController.animateTo(tabController.index - 1); + } }, + child: AnimatedBuilder( + animation: tabController, + builder: (_, __) { + final showExpense = widget.transaction == null || + widget.transaction!.transaction.type == + TransactionType.expense; + final showIncome = widget.transaction == null || + widget.transaction!.transaction.type == + TransactionType.income; + final children = [ + if (showExpense) + KeyedSubtree( + key: const ValueKey('tx-form-expense'), + child: formDisplay == 'full' + ? AddTransactionForm( + transactionType: TransactionType.expense, + accentColor: appDangerColor, + transactionCompleteEntity: + widget.transaction, + ) + : AddTransactionFormCompactLayout( + transactionType: TransactionType.expense, + accentColor: appDangerColor, + transactionCompleteEntity: + widget.transaction, + ), + ), + if (showIncome) + KeyedSubtree( + key: const ValueKey('tx-form-income'), + child: formDisplay == 'full' + ? AddTransactionForm( + accentColor: appPrimaryColor, + transactionCompleteEntity: + widget.transaction, + ) + : AddTransactionFormCompactLayout( + accentColor: appPrimaryColor, + transactionCompleteEntity: + widget.transaction, + ), + ), + if (widget.transaction == null) + const KeyedSubtree( + key: ValueKey('tx-form-transfer'), + child: WalletTransferScreen(embedded: true), + ), + ]; + return IndexedStack( + index: tabController.index.clamp( + 0, + children.length - 1, + ), + children: children, + ); + }, + ), ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/presentation/budget/add_budget_screen.dart b/lib/presentation/budget/add_budget_screen.dart index 90e57f1f..93c1fd3e 100644 --- a/lib/presentation/budget/add_budget_screen.dart +++ b/lib/presentation/budget/add_budget_screen.dart @@ -1,3 +1,4 @@ +import 'package:currency_picker/currency_picker.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,6 +13,7 @@ import 'package:trakli/gen/translations/codegen_loader.g.dart'; import 'package:trakli/presentation/utils/helpers.dart' show showSnackBar; import 'package:trakli/presentation/budget/cubit/budget_cubit.dart'; import 'package:trakli/presentation/category/cubit/category_cubit.dart'; +import 'package:trakli/presentation/exchange_rate/cubit/exchange_rate_cubit.dart'; import 'package:trakli/presentation/groups/cubit/group_cubit.dart'; import 'package:trakli/presentation/utils/app_navigator.dart'; import 'package:trakli/presentation/utils/design_tokens.dart'; @@ -168,7 +170,8 @@ class _AddBudgetScreenState extends State { appBar: AppBar( title: Text(_isEdit ? LocaleKeys.editBudget.tr() : LocaleKeys.newBudget.tr()), ), - body: BlocBuilder( + body: SafeArea( + child: BlocBuilder( builder: (context, state) { return Stack( children: [ @@ -209,14 +212,9 @@ class _AddBudgetScreenState extends State { ), SizedBox(width: 10.w), Expanded( - child: TextField( + child: _CurrencyPickerField( controller: _currency, - textCapitalization: TextCapitalization.characters, - maxLength: 3, - decoration: const InputDecoration( - counterText: '', - border: OutlineInputBorder(), - ), + onChanged: () => setState(() {}), ), ), ], @@ -359,6 +357,7 @@ class _AddBudgetScreenState extends State { ], ); }, + ), ), ); } @@ -713,3 +712,68 @@ class _TargetTile extends StatelessWidget { ); } } + +/// Tap-to-open currency picker bound to a `TextEditingController` so the +/// existing `_submit()` flow (which reads `_currency.text`) keeps working +/// unchanged. Restricted to the user's wallet currencies plus the app default +/// (and the current value) so a usable exchange rate exists for conversion. +class _CurrencyPickerField extends StatelessWidget { + final TextEditingController controller; + final VoidCallback onChanged; + const _CurrencyPickerField({ + required this.controller, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final code = controller.text.trim().toUpperCase(); + return InkWell( + borderRadius: BorderRadius.circular(4.r), + onTap: () { + final wallets = context.read().state.wallets; + final defaultCode = + context.read().state.entity?.baseCode; + final filter = { + for (final w in wallets) w.currencyCode.toUpperCase(), + if (defaultCode != null && defaultCode.isNotEmpty) + defaultCode.toUpperCase(), + if (code.isNotEmpty) code, + }; + showCurrencyPicker( + context: context, + currencyFilter: filter.isEmpty ? null : filter.toList(), + theme: CurrencyPickerThemeData( + bottomSheetHeight: 0.7.sh, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + flagSize: 24.sp, + subtitleTextStyle: TextStyle( + fontSize: 12.sp, + color: Theme.of(context).primaryColor, + ), + ), + onSelect: (Currency currency) { + controller.text = currency.code; + onChanged(); + }, + ); + }, + child: InputDecorator( + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + code.isEmpty ? '—' : code, + style: TextStyle(fontSize: 14.sp), + ), + Icon(Icons.arrow_drop_down, size: 20.sp), + ], + ), + ), + ); + } +} + diff --git a/lib/presentation/budget/budget_detail_screen.dart b/lib/presentation/budget/budget_detail_screen.dart index 8d59ff33..9ca19340 100644 --- a/lib/presentation/budget/budget_detail_screen.dart +++ b/lib/presentation/budget/budget_detail_screen.dart @@ -34,7 +34,6 @@ class _BudgetDetailScreenState extends State { final id = widget.budget.id; if (id != null) { cubit.fetchProgress(id); - cubit.refreshPeriodStates(); } } @@ -108,7 +107,8 @@ class _BudgetDetailScreenState extends State { ), ], ), - body: BlocListener( + body: SafeArea( + child: BlocListener( listenWhen: (prev, curr) { final deletionCompleted = prev.isDeleting && !curr.isDeleting; final closingCompleted = @@ -143,17 +143,18 @@ class _BudgetDetailScreenState extends State { child: BlocBuilder( builder: (context, state) { final budget = _currentBudget(state); - final progress = state.selectedBudgetProgress; + // Prefer the locally-streamed value (kept fresh by the recomputer + // on edits and by the sync handler on server reconciliation). + // Fall back to the cubit's selectedBudgetProgress only if the + // budget row hasn't been written yet. + final progress = budget.progress ?? state.selectedBudgetProgress; final targets = state.selectedBudgetTargets; final periods = state.selectedBudgetPeriodStates; return RefreshIndicator( onRefresh: () { final id = budget.id; if (id != null) { - return Future.wait([ - context.read().fetchProgress(id), - context.read().refreshPeriodStates(), - ]); + return context.read().fetchProgress(id); } return Future.value(); }, @@ -173,10 +174,12 @@ class _BudgetDetailScreenState extends State { SizedBox(height: 20.h), _SectionHeader(text: LocaleKeys.budgetTargetsLabel.tr()), _TargetsBlock(budget: budget, targets: targets), - SizedBox(height: 20.h), - _SectionHeader( - text: LocaleKeys.budgetPeriodHistoryLabel.tr()), - _PeriodHistoryBlock(periods: periods), + if (budget.rolloverEnabled) ...[ + SizedBox(height: 20.h), + _SectionHeader( + text: LocaleKeys.budgetPeriodHistoryLabel.tr()), + _PeriodHistoryBlock(periods: periods), + ], if (budget.rolloverEnabled && budget.id != null) ...[ SizedBox(height: 24.h), FilledButton.icon( @@ -198,6 +201,7 @@ class _BudgetDetailScreenState extends State { ); }, ), + ), ), ); } diff --git a/lib/presentation/budget/budget_screen.dart b/lib/presentation/budget/budget_screen.dart index 516434ee..a5206a5d 100644 --- a/lib/presentation/budget/budget_screen.dart +++ b/lib/presentation/budget/budget_screen.dart @@ -47,63 +47,68 @@ class _BudgetScreenState extends State { PageAppBarAction(icon: Icons.add, onTap: _add, primary: true), ], ), - body: BlocBuilder( - builder: (context, state) { - if (state.isLoading && state.budgets.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading && state.budgets.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } - final q = _query.trim().toLowerCase(); - final filtered = state.budgets.where((b) { - if (_onlyActive && !b.isActive) return false; - if (q.isEmpty) return true; - return '${b.name} ${b.description ?? ''}' - .toLowerCase() - .contains(q); - }).toList(); + final q = _query.trim().toLowerCase(); + final filtered = state.budgets.where((b) { + if (_onlyActive && !b.isActive) return false; + if (q.isEmpty) return true; + return '${b.name} ${b.description ?? ''}' + .toLowerCase() + .contains(q); + }).toList(); - if (state.budgets.isEmpty) { - return _EmptyState(onAdd: _add); - } + if (state.budgets.isEmpty) { + return _EmptyState(onAdd: _add); + } - return Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 4.h), - child: _FilterChips( - onlyActive: _onlyActive, - onChange: (v) => setState(() => _onlyActive = v), - ), - ), - Expanded( - child: filtered.isEmpty - ? _NoMatches(query: _query) - : ListView.separated( - padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 24.h), - itemCount: filtered.length, - separatorBuilder: (_, __) => SizedBox(height: 10.h), - itemBuilder: (_, i) { - final budget = filtered[i]; - return _BudgetCard( - budget: budget, - onTap: () => AppNavigator.push( - context, - BudgetDetailScreen(budget: budget), - ), - onEdit: () => AppNavigator.push( - context, - AddBudgetScreen(budget: budget), - ), - onDelete: () => context - .read() - .deleteBudget(budget.clientId), - ); - }, - ), + return SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 4.h), + child: _FilterChips( + onlyActive: _onlyActive, + onChange: (v) => setState(() => _onlyActive = v), + ), + ), + if (filtered.isEmpty) + _NoMatches(query: _query) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 24.h), + itemCount: filtered.length, + separatorBuilder: (_, __) => SizedBox(height: 10.h), + itemBuilder: (_, i) { + final budget = filtered[i]; + return _BudgetCard( + budget: budget, + onTap: () => AppNavigator.push( + context, + BudgetDetailScreen(budget: budget), + ), + onEdit: () => AppNavigator.push( + context, + AddBudgetScreen(budget: budget), + ), + onDelete: () => context + .read() + .deleteBudget(budget.clientId), + ); + }, + ), + ], ), - ], - ); - }, + ); + }, + ), ), ); } @@ -248,7 +253,8 @@ class _BudgetCard extends StatelessWidget { padding: EdgeInsets.zero, iconSize: 18.sp, itemBuilder: (_) => [ - PopupMenuItem(value: 'edit', child: Text(LocaleKeys.edit.tr())), + PopupMenuItem( + value: 'edit', child: Text(LocaleKeys.edit.tr())), PopupMenuItem( value: 'delete', child: Text(LocaleKeys.delete.tr())), ], @@ -299,9 +305,8 @@ class _MiniChip extends StatelessWidget { return Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), decoration: BoxDecoration( - color: muted - ? tones.bgPage - : tones.brand.accent.withValues(alpha: 0.12), + color: + muted ? tones.bgPage : tones.brand.accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(AppRadii.pill), ), child: Text( diff --git a/lib/presentation/budget/cubit/budget_cubit.dart b/lib/presentation/budget/cubit/budget_cubit.dart index 5f8d3a66..723a50a8 100644 --- a/lib/presentation/budget/cubit/budget_cubit.dart +++ b/lib/presentation/budget/cubit/budget_cubit.dart @@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:trakli/core/error/failures/failures.dart'; -import 'package:trakli/core/usecases/usecase.dart'; import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart'; import 'package:trakli/domain/entities/budget_entity.dart'; import 'package:trakli/domain/entities/budget_period_state_entity.dart'; @@ -19,7 +18,6 @@ import 'package:trakli/domain/usecases/budget/insert_budget_usecase.dart'; import 'package:trakli/domain/usecases/budget/listen_to_budgets_usecase.dart'; import 'package:trakli/domain/usecases/budget/listen_to_period_states_usecase.dart'; import 'package:trakli/domain/usecases/budget/listen_to_targets_usecase.dart'; -import 'package:trakli/domain/usecases/budget/refresh_period_states_usecase.dart'; import 'package:trakli/domain/usecases/budget/update_budget_usecase.dart'; import 'package:trakli/domain/repositories/budget_repository.dart'; import 'package:trakli/presentation/utils/enums.dart'; @@ -39,7 +37,6 @@ class BudgetCubit extends Cubit { final ListenToBudgetsUseCase _listenToBudgetsUseCase; final ListenToTargetsUseCase _listenToTargetsUseCase; final ListenToPeriodStatesUseCase _listenToPeriodStatesUseCase; - final RefreshPeriodStatesUseCase _refreshPeriodStatesUseCase; StreamSubscription? _budgetsSubscription; StreamSubscription? _targetsSubscription; @@ -57,7 +54,6 @@ class BudgetCubit extends Cubit { required ListenToBudgetsUseCase listenToBudgetsUseCase, required ListenToTargetsUseCase listenToTargetsUseCase, required ListenToPeriodStatesUseCase listenToPeriodStatesUseCase, - required RefreshPeriodStatesUseCase refreshPeriodStatesUseCase, }) : _getAllBudgetsUseCase = getAllBudgetsUseCase, _insertBudgetUseCase = insertBudgetUseCase, _updateBudgetUseCase = updateBudgetUseCase, @@ -68,7 +64,6 @@ class BudgetCubit extends Cubit { _listenToBudgetsUseCase = listenToBudgetsUseCase, _listenToTargetsUseCase = listenToTargetsUseCase, _listenToPeriodStatesUseCase = listenToPeriodStatesUseCase, - _refreshPeriodStatesUseCase = refreshPeriodStatesUseCase, super(BudgetState.initial()) { listenToBudgets(); } @@ -217,8 +212,9 @@ class BudgetCubit extends Cubit { }); _periodStatesSubscription?.cancel(); - _periodStatesSubscription = - _listenToPeriodStatesUseCase(ListenToPeriodStatesParams(budgetClientId: clientId)).listen((either) { + _periodStatesSubscription = _listenToPeriodStatesUseCase( + ListenToPeriodStatesParams(budgetClientId: clientId), + ).listen((either) { either.fold( (failure) => emit(state.copyWith(failure: failure)), (states) => emit(state.copyWith( @@ -280,16 +276,11 @@ class BudgetCubit extends Cubit { isClosingPeriod: false, failure: const Failure.none(), )); - await _refreshPeriodStatesUseCase(NoParams()); await fetchProgress(serverId); }, ); } - Future refreshPeriodStates() async { - await _refreshPeriodStatesUseCase(NoParams()); - } - @override Future close() { _budgetsSubscription?.cancel(); diff --git a/lib/presentation/transactions/add_transaction_form_compact_layout.dart b/lib/presentation/transactions/add_transaction_form_compact_layout.dart index 3bc4cf5c..5691d25e 100644 --- a/lib/presentation/transactions/add_transaction_form_compact_layout.dart +++ b/lib/presentation/transactions/add_transaction_form_compact_layout.dart @@ -189,188 +189,28 @@ class _AddTransactionFormCompactLayoutState @override Widget build(BuildContext context) { super.build(context); - return BlocListener( - listenWhen: (previous, current) { - // Listen when saving completes (isSaving goes from true to false) - return previous.isSaving && !current.isSaving; - }, - listener: (context, state) { - // Only navigate if save was successful (no error) - if (!state.failure.hasError && mounted) { - Navigator.pop(context); - } - }, - child: SingleChildScrollView( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, - ), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextFormField( - controller: amountController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: LocaleKeys.exampleAmount.tr(), - labelText: LocaleKeys.transactionAmount.tr(), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: widget.accentColor, - ), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return LocaleKeys.amountIsRequired.tr(); - } - final number = double.tryParse(value); - if (number == null) { - return LocaleKeys.mustBeNumber.tr(); - } - if (number == 0) { - return LocaleKeys.amountMustNotBeZero.tr(); - } - return null; - }, - ), - ), - GestureDetector( - onTap: () { - showCurrencyPicker( - context: context, - theme: CurrencyPickerThemeData( - bottomSheetHeight: 0.7.sh, - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - flagSize: 24.sp, - subtitleTextStyle: TextStyle( - fontSize: 12.sp, - color: Theme.of(context).primaryColor, - ), - ), - onSelect: (Currency currencyValue) { - setCurrency(currencyValue); - }, - ); - }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: widget.accentColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: widget.accentColor.withValues(alpha: 0.35), - width: 1, - ), - ), - child: Center( - child: Text( - currentCurrency?.code ?? "XAF", - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w700, - color: widget.accentColor, - ), - ), - ), - ), - ) - ], - ), - ), - SizedBox(height: 16.h), - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: BlocBuilder( - builder: (context, state) { - return CustomAutoCompleteSearch( - key: ValueKey( - 'wallet_${currentCurrency?.code ?? 'none'}'), - label: LocaleKeys.wallet.tr(), - accentColor: widget.accentColor, - initialValue: _selectedWallet, - optionsBuilder: - (TextEditingValue textEditingValue) { - Iterable filteredWallets = - state.wallets; - if (currentCurrency != null) { - filteredWallets = - filteredWallets.where((wallet) { - return wallet.currencyCode == - currentCurrency!.code; - }); - } - - if (textEditingValue.text.isNotEmpty) { - filteredWallets = - filteredWallets.where((wallet) { - return wallet.name.toLowerCase().contains( - textEditingValue.text.toLowerCase()); - }); - } - return filteredWallets; - }, - displayStringForOption: (WalletEntity option) { - return option.name.capitalizeFirst(); - }, - onSelected: (value) { - setState(() { - _selectedWallet = value; - setCurrencyFromWallet(value); - }); - }, - validator: (value) { - if (_selectedWallet == null) { - return LocaleKeys.walletIsRequired.tr(); - } - return null; - }, - ); - }, - ), - ), - _AddBesideField( - onTap: () async { - AppNavigator.push(context, const AddWalletScreen()); - }, - accent: widget.accentColor, - ), - ], - ), - ), - SizedBox(height: 16.h), - Row( - spacing: 8.w, + return SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 16.h, + ), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntrinsicHeight( + child: Row( + spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: TextFormField( - readOnly: true, - controller: dateController, + controller: amountController, + keyboardType: TextInputType.number, decoration: InputDecoration( - labelText: LocaleKeys.date.tr(), - suffixIcon: Padding( - padding: const EdgeInsets.all(12), - child: SvgPicture.asset( - Assets.images.calendar, - ), - ), + hintText: LocaleKeys.exampleAmount.tr(), + labelText: LocaleKeys.transactionAmount.tr(), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( @@ -378,249 +218,391 @@ class _AddTransactionFormCompactLayoutState ), ), ), - onTap: () async { - final selectDate = await showDatePicker( - context: context, - firstDate: DateTime.now().subtract( - const Duration(days: 1000), - ), - lastDate: DateTime.now().add( - const Duration(days: 1000), - ), - ); - if (selectDate != null) { - setState(() { - date = selectDate; - dateController.text = dateFormat.format(date); - }); + validator: (value) { + if (value == null || value.isEmpty) { + return LocaleKeys.amountIsRequired.tr(); + } + final number = double.tryParse(value); + if (number == null) { + return LocaleKeys.mustBeNumber.tr(); } + if (number == 0) { + return LocaleKeys.amountMustNotBeZero.tr(); + } + return null; }, ), ), - Expanded( - child: TextFormField( - readOnly: true, - controller: timeController, - decoration: InputDecoration( - labelText: LocaleKeys.time.tr(), - suffixIcon: Padding( - padding: const EdgeInsets.all(12), - child: SvgPicture.asset( - Assets.images.clock, + GestureDetector( + onTap: () { + showCurrencyPicker( + context: context, + theme: CurrencyPickerThemeData( + bottomSheetHeight: 0.7.sh, + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + flagSize: 24.sp, + subtitleTextStyle: TextStyle( + fontSize: 12.sp, + color: Theme.of(context).primaryColor, ), ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( + onSelect: (Currency currencyValue) { + setCurrency(currencyValue); + }, + ); + }, + child: Container( + width: 60.w, + constraints: BoxConstraints( + maxHeight: 50.h, + ), + decoration: BoxDecoration( + color: widget.accentColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: widget.accentColor.withValues(alpha: 0.35), + width: 1, + ), + ), + child: Center( + child: Text( + currentCurrency?.code ?? "XAF", + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w700, color: widget.accentColor, ), ), ), - onTap: () async { - final selectTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), + ), + ) + ], + ), + ), + SizedBox(height: 16.h), + IntrinsicHeight( + child: Row( + spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) { + return CustomAutoCompleteSearch( + key: ValueKey( + 'wallet_${currentCurrency?.code ?? 'none'}'), + label: LocaleKeys.wallet.tr(), + accentColor: widget.accentColor, + initialValue: _selectedWallet, + optionsBuilder: (TextEditingValue textEditingValue) { + Iterable filteredWallets = + state.wallets; + if (currentCurrency != null) { + filteredWallets = filteredWallets.where((wallet) { + return wallet.currencyCode == + currentCurrency!.code; + }); + } + + if (textEditingValue.text.isNotEmpty) { + filteredWallets = filteredWallets.where((wallet) { + return wallet.name.toLowerCase().contains( + textEditingValue.text.toLowerCase()); + }); + } + return filteredWallets; + }, + displayStringForOption: (WalletEntity option) { + return option.name.capitalizeFirst(); + }, + onSelected: (value) { + setState(() { + _selectedWallet = value; + setCurrencyFromWallet(value); + }); + }, + validator: (value) { + if (_selectedWallet == null) { + return LocaleKeys.walletIsRequired.tr(); + } + return null; + }, ); - if (selectTime != null) { - setState(() { - time = selectTime; - date = DateTime( - date.year, - date.month, - date.day, - selectTime.hour, - selectTime.minute, - ); - timeController.text = timeFormat.format(date); - }); - } }, ), ), + _AddBesideField( + onTap: () async { + AppNavigator.push(context, const AddWalletScreen()); + }, + accent: widget.accentColor, + ), ], ), - SizedBox(height: 16.h), - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: BlocBuilder( - builder: (context, state) { - return CustomAutoCompleteSearch( - label: LocaleKeys.party.tr(), - accentColor: widget.accentColor, - optionsBuilder: - (TextEditingValue textEditingValue) { - if (textEditingValue.text.isEmpty) { - return state.parties; - } - return state.parties.where((category) { - return category.name.toLowerCase().contains( - textEditingValue.text.toLowerCase()); - }); - }, - displayStringForOption: (PartyEntity option) { - return option.name.capitalizeFirst(); - }, - onSelected: (value) { - setState(() { - debugPrint(value.name); - _selectedParty = value; - }); - }, - // selectedItem: _selectedCategory, - ); - }, + ), + SizedBox(height: 16.h), + Row( + spacing: 8.w, + children: [ + Expanded( + child: TextFormField( + readOnly: true, + controller: dateController, + decoration: InputDecoration( + labelText: LocaleKeys.date.tr(), + suffixIcon: Padding( + padding: const EdgeInsets.all(12), + child: SvgPicture.asset( + Assets.images.calendar, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, + ), ), ), - _AddBesideField( - onTap: () async { - AppNavigator.push(context, const AddPartyScreen()); - }, - accent: widget.accentColor, - ), - ], + onTap: () async { + final selectDate = await showDatePicker( + context: context, + firstDate: DateTime.now().subtract( + const Duration(days: 1000), + ), + lastDate: DateTime.now().add( + const Duration(days: 1000), + ), + ); + if (selectDate != null) { + setState(() { + date = selectDate; + dateController.text = dateFormat.format(date); + }); + } + }, + ), ), - ), - SizedBox(height: 16.h), - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: BlocBuilder( - builder: (context, state) { - //Category by transaction type - final searchCategories = state.categories.where( - (element) => - element.type == widget.transactionType); - - return CustomAutoCompleteSearch( - label: LocaleKeys.category.tr(), - accentColor: widget.accentColor, - optionsBuilder: - (TextEditingValue textEditingValue) { - if (textEditingValue.text.isEmpty) { - return searchCategories; - } - return searchCategories.where((category) { - return category.name.toLowerCase().contains( - textEditingValue.text.toLowerCase()); - }); - }, - displayStringForOption: (CategoryEntity option) { - return option.name.capitalizeFirst(); - }, - onSelected: (value) { - setState(() { - debugPrint(value.name); - _selectedCategory = value; - }); - }, - // selectedItem: _selectedCategory, - ); - }, + Expanded( + child: TextFormField( + readOnly: true, + controller: timeController, + decoration: InputDecoration( + labelText: LocaleKeys.time.tr(), + suffixIcon: Padding( + padding: const EdgeInsets.all(12), + child: SvgPicture.asset( + Assets.images.clock, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, + ), ), ), - _AddBesideField( - onTap: () async { - AppNavigator.push( - context, - AddCategoryScreen( - accentColor: widget.accentColor, - type: widget.transactionType, - ), + onTap: () async { + final selectTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (selectTime != null) { + setState(() { + time = selectTime; + date = DateTime( + date.year, + date.month, + date.day, + selectTime.hour, + selectTime.minute, + ); + timeController.text = timeFormat.format(date); + }); + } + }, + ), + ), + ], + ), + SizedBox(height: 16.h), + IntrinsicHeight( + child: Row( + spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) { + return CustomAutoCompleteSearch( + label: LocaleKeys.party.tr(), + accentColor: widget.accentColor, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return state.parties; + } + return state.parties.where((category) { + return category.name.toLowerCase().contains( + textEditingValue.text.toLowerCase()); + }); + }, + displayStringForOption: (PartyEntity option) { + return option.name.capitalizeFirst(); + }, + onSelected: (value) { + setState(() { + debugPrint(value.name); + _selectedParty = value; + }); + }, + // selectedItem: _selectedCategory, ); }, - accent: widget.accentColor, ), - ], - ), + ), + _AddBesideField( + onTap: () async { + AppNavigator.push(context, const AddPartyScreen()); + }, + accent: widget.accentColor, + ), + ], ), - SizedBox(height: 16.h), - TextFormField( - controller: descriptionController, - maxLines: 2, - decoration: InputDecoration( - labelText: LocaleKeys.transactionDescription.tr(), - hintText: LocaleKeys.transactionTypeHere.tr(), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: widget.accentColor, + ), + SizedBox(height: 16.h), + IntrinsicHeight( + child: Row( + spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) { + //Category by transaction type + final searchCategories = state.categories.where( + (element) => + element.type == widget.transactionType); + + return CustomAutoCompleteSearch( + label: LocaleKeys.category.tr(), + accentColor: widget.accentColor, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return searchCategories; + } + return searchCategories.where((category) { + return category.name.toLowerCase().contains( + textEditingValue.text.toLowerCase()); + }); + }, + displayStringForOption: (CategoryEntity option) { + return option.name.capitalizeFirst(); + }, + onSelected: (value) { + setState(() { + debugPrint(value.name); + _selectedCategory = value; + }); + }, + // selectedItem: _selectedCategory, + ); + }, ), ), - ), - onTap: () async {}, - ), - SizedBox(height: 16.h), - AttachmentSourceRow( - accentColor: widget.accentColor, - onFileAdded: _addAttachmentPath, + _AddBesideField( + onTap: () async { + AppNavigator.push( + context, + AddCategoryScreen( + accentColor: widget.accentColor, + type: widget.transactionType, + ), + ); + }, + accent: widget.accentColor, + ), + ], ), - if (_allAttachments.isNotEmpty) ...[ - SizedBox(height: 12.h), - SizedBox( - height: 72.h, - child: AttachmentListView( - items: _allAttachments, - showRemoveButton: true, - accentColor: widget.accentColor, - onRemove: _handleRemove, + ), + SizedBox(height: 16.h), + TextFormField( + controller: descriptionController, + maxLines: 2, + decoration: InputDecoration( + labelText: LocaleKeys.transactionDescription.tr(), + hintText: LocaleKeys.transactionTypeHere.tr(), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, ), ), - ], - SizedBox(height: 20.h), + ), + onTap: () async {}, + ), + SizedBox(height: 16.h), + AttachmentSourceRow( + accentColor: widget.accentColor, + onFileAdded: _addAttachmentPath, + ), + if (_allAttachments.isNotEmpty) ...[ + SizedBox(height: 12.h), SizedBox( - height: 56.h, - width: double.infinity, - child: Builder( - builder: (context) { - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(11.r), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ + height: 72.h, + child: AttachmentListView( + items: _allAttachments, + showRemoveButton: true, + accentColor: widget.accentColor, + onRemove: _handleRemove, + ), + ), + ], + SizedBox(height: 20.h), + SizedBox( + height: 56.h, + width: double.infinity, + child: Builder( + builder: (context) { + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(11.r), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + widget.accentColor, + Color.alphaBlend( + Colors.black.withValues(alpha: 0.08), widget.accentColor, - Color.alphaBlend( - Colors.black.withValues(alpha: 0.08), - widget.accentColor, - ), - ], - ), - border: Border.all( - color: Colors.white.withValues(alpha: 0.35), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: widget.accentColor.withValues(alpha: 0.4), - blurRadius: 18, - offset: const Offset(0, 8), ), ], ), - child: ElevatedButton( + border: Border.all( + color: Colors.white.withValues(alpha: 0.35), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: widget.accentColor.withValues(alpha: 0.4), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: ElevatedButton( style: ButtonStyle( backgroundColor: const WidgetStatePropertyAll(Colors.transparent), foregroundColor: const WidgetStatePropertyAll(Colors.white), - elevation: - const WidgetStatePropertyAll(0), - shadowColor: const WidgetStatePropertyAll( - Colors.transparent), + elevation: const WidgetStatePropertyAll(0), + shadowColor: + const WidgetStatePropertyAll(Colors.transparent), overlayColor: WidgetStatePropertyAll( Colors.white.withValues(alpha: 0.16)), shape: WidgetStatePropertyAll( RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(14.r), + borderRadius: BorderRadius.circular(14.r), ), ), textStyle: WidgetStatePropertyAll( @@ -696,13 +678,12 @@ class _AddTransactionFormCompactLayoutState ], ), ), - ); - }, - ), + ); + }, ), - SizedBox(height: 20.h), - ], - ), + ), + SizedBox(height: 20.h), + ], ), ), ); diff --git a/lib/presentation/utils/custom_drawer.dart b/lib/presentation/utils/custom_drawer.dart index 41b919c2..ce7d321c 100644 --- a/lib/presentation/utils/custom_drawer.dart +++ b/lib/presentation/utils/custom_drawer.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -53,12 +54,14 @@ class CustomDrawer extends StatelessWidget { } } - Future _copyEmailAndShowSnackbar(BuildContext context, String email) async { + Future _copyEmailAndShowSnackbar( + BuildContext context, String email) async { await Clipboard.setData(ClipboardData(text: email)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(LocaleKeys.supportEmailCopied.tr().replaceFirst('{0}', email)), + content: Text( + LocaleKeys.supportEmailCopied.tr().replaceFirst('{0}', email)), ), ); } @@ -82,9 +85,7 @@ class CustomDrawer extends StatelessWidget { child: Row( children: [ SvgPicture.asset( - isDark - ? Assets.images.appLogo - : Assets.images.appLogoGreen, + isDark ? Assets.images.appLogo : Assets.images.appLogoGreen, height: 28.h, ), SizedBox(width: 10.w), @@ -209,8 +210,7 @@ class CustomDrawer extends StatelessWidget { Icon( Icons.savings_outlined, size: 20.sp, - color: - Theme.of(context).colorScheme.onSurface, + color: Theme.of(context).colorScheme.onSurface, ), SizedBox(width: 14.w), Expanded( @@ -219,9 +219,8 @@ class CustomDrawer extends StatelessWidget { style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, - color: Theme.of(context) - .colorScheme - .onSurface, + color: + Theme.of(context).colorScheme.onSurface, ), ), ), @@ -244,8 +243,7 @@ class CustomDrawer extends StatelessWidget { Icon( Icons.file_upload_outlined, size: 20.sp, - color: - Theme.of(context).colorScheme.onSurface, + color: Theme.of(context).colorScheme.onSurface, ), SizedBox(width: 14.w), Expanded( @@ -254,9 +252,8 @@ class CustomDrawer extends StatelessWidget { style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, - color: Theme.of(context) - .colorScheme - .onSurface, + color: + Theme.of(context).colorScheme.onSurface, ), ), ), @@ -282,7 +279,7 @@ class CustomDrawer extends StatelessWidget { ), BlocBuilder( builder: (context, state) { - if (state.showDebug == true) { + if (kDebugMode || state.showDebug == true) { return Column( children: [ const Divider(), diff --git a/lib/presentation/utils/forms/add_transaction_form.dart b/lib/presentation/utils/forms/add_transaction_form.dart index e469dfd6..e62e8ae1 100644 --- a/lib/presentation/utils/forms/add_transaction_form.dart +++ b/lib/presentation/utils/forms/add_transaction_form.dart @@ -176,575 +176,560 @@ class _AddTransactionFormState extends State @override Widget build(BuildContext context) { super.build(context); - return BlocListener( - listenWhen: (previous, current) { - // Listen when saving completes (isSaving goes from true to false) - return previous.isSaving && !current.isSaving; - }, - listener: (context, state) { - // Only navigate if save was successful (no error) - if (!state.failure.hasError && mounted) { - Navigator.pop(context); - } - }, - child: SingleChildScrollView( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 16.h, - ), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.transactionAmount.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 8.h), - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextFormField( - controller: amountController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: LocaleKeys.exampleAmount.tr(), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: widget.accentColor, - ), + return SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 16.h, + ), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.transactionAmount.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + IntrinsicHeight( + child: Row( + spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextFormField( + controller: amountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: LocaleKeys.exampleAmount.tr(), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return LocaleKeys.amountIsRequired.tr(); - } - final number = double.tryParse(value); - if (number == null) { - return LocaleKeys.mustBeNumber.tr(); - } - if (number == 0) { - return LocaleKeys.amountMustNotBeZero.tr(); - } - return null; + ), + validator: (value) { + if (value == null || value.isEmpty) { + return LocaleKeys.amountIsRequired.tr(); + } + final number = double.tryParse(value); + if (number == null) { + return LocaleKeys.mustBeNumber.tr(); + } + if (number == 0) { + return LocaleKeys.amountMustNotBeZero.tr(); + } + return null; + }, + ), + ), + GestureDetector( + onTap: () { + showCurrencyPicker( + context: context, + theme: CurrencyPickerThemeData( + bottomSheetHeight: 0.7.sh, + backgroundColor: Colors.white, + flagSize: 24.sp, + subtitleTextStyle: TextStyle( + fontSize: 12.sp, + color: Theme.of(context).primaryColor, + )), + onSelect: (Currency currencyValue) { + setState(() { + currency = currencyValue; + setAmountController(currency); + }); }, + ); + }, + child: Container( + width: 60.w, + constraints: BoxConstraints( + maxHeight: 50.h, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text(currency?.code ?? "XAF"), ), ), - GestureDetector( + ) + ], + ), + ), + SizedBox(height: 16.h), + Text( + LocaleKeys.wallet.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + IntrinsicHeight( + child: Row( + spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextFormField( + readOnly: true, + controller: walletController, onTap: () { - showCurrencyPicker( - context: context, - theme: CurrencyPickerThemeData( - bottomSheetHeight: 0.7.sh, - backgroundColor: Colors.white, - flagSize: 24.sp, - subtitleTextStyle: TextStyle( - fontSize: 12.sp, - color: Theme.of(context).primaryColor, - )), - onSelect: (Currency currencyValue) { - setState(() { - currency = currencyValue; - setAmountController(currency); - }); - }, + showCustomBottomSheet( + context, + color: Theme.of(context).scaffoldBackgroundColor, + widget: BlocBuilder( + builder: (context, state) { + return SelectWalletBottomSheet( + wallets: state.wallets, + onSelect: (wallet) { + setState(() { + selectedWallet = wallet; + walletController.text = wallet.name; + // Update currency when wallet changes + currency = wallet.currency; + }); + Navigator.pop(context); + }, + ); + }, + ), ); }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + decoration: InputDecoration( + hintText: LocaleKeys.selectWallet.tr(), + focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, + ), ), - child: Center( - child: Text(currency?.code ?? "XAF"), + suffixIcon: Padding( + padding: const EdgeInsets.all(12), + child: SvgPicture.asset( + Assets.images.arrowDown, + colorFilter: ColorFilter.mode( + Colors.grey.shade500, + BlendMode.srcIn, + ), + ), ), ), - ) - ], - ), - ), - SizedBox(height: 16.h), - Text( - LocaleKeys.wallet.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, + validator: (value) { + if (value == null || value.isEmpty) { + return LocaleKeys.walletIsRequired.tr(); + } + return null; + }, ), + ), + GestureDetector( + onTap: () { + AppNavigator.push(context, const AddWalletScreen()); + }, + child: Container( + width: 60.w, + constraints: BoxConstraints( + maxHeight: 50.h, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Icon(Icons.add), + ), + ), + ) + ], ), - SizedBox(height: 8.h), - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextFormField( - readOnly: true, - controller: walletController, - onTap: () { - showCustomBottomSheet( - context, - color: Theme.of(context).scaffoldBackgroundColor, - widget: BlocBuilder( - builder: (context, state) { - return SelectWalletBottomSheet( - wallets: state.wallets, - onSelect: (wallet) { - setState(() { - selectedWallet = wallet; - walletController.text = wallet.name; - // Update currency when wallet changes - currency = wallet.currency; - }); - Navigator.pop(context); - }, - ); - }, + ), + SizedBox(height: 16.h), + Row( + spacing: 16.w, + children: [ + Expanded( + child: Column( + spacing: 8.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.transactionDate.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, ), - ); - }, + overflow: TextOverflow.ellipsis, + ), + TextFormField( + readOnly: true, + controller: dateController, decoration: InputDecoration( - hintText: LocaleKeys.selectWallet.tr(), + suffixIcon: Padding( + padding: const EdgeInsets.all(12), + child: SvgPicture.asset( + Assets.images.calendar, + ), + ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: widget.accentColor, ), ), + ), + onTap: () async { + final selectDate = await showDatePicker( + context: context, + firstDate: DateTime.now().subtract( + const Duration(days: 1000), + ), + lastDate: DateTime.now().add( + const Duration(days: 1000), + ), + ); + if (selectDate != null) { + setState(() { + date = selectDate; + dateController.text = dateFormat.format(date); + }); + } + }, + ), + ], + ), + ), + Expanded( + child: Column( + spacing: 8.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.transactionTime.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + TextFormField( + readOnly: true, + controller: timeController, + decoration: InputDecoration( suffixIcon: Padding( padding: const EdgeInsets.all(12), child: SvgPicture.asset( - Assets.images.arrowDown, - colorFilter: ColorFilter.mode( - Colors.grey.shade500, - BlendMode.srcIn, - ), + Assets.images.clock, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, ), ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return LocaleKeys.walletIsRequired.tr(); + onTap: () async { + final selectTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (selectTime != null) { + setState(() { + time = selectTime; + date = DateTime( + date.year, + date.month, + date.day, + selectTime.hour, + selectTime.minute, + ); + timeController.text = timeFormat.format(date); + }); } - return null; }, ), - ), - GestureDetector( - onTap: () { - AppNavigator.push(context, const AddWalletScreen()); - }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - ), - child: const Center( - child: Icon(Icons.add), - ), - ), - ) - ], + ], + ), ), - ), - SizedBox(height: 16.h), - Row( + ], + ), + SizedBox(height: 16.h), + Text( + widget.transactionType == TransactionType.expense + ? '${LocaleKeys.transactionSentTo.tr()} (${LocaleKeys.party.tr()})' + : '${LocaleKeys.transactionReceivedFrom.tr()} (${LocaleKeys.party.tr()})', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + IntrinsicHeight( + child: Row( spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Column( - spacing: 8.h, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.transactionDate.tr(), - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.ellipsis, - ), - TextFormField( - readOnly: true, - controller: dateController, - decoration: InputDecoration( - suffixIcon: Padding( - padding: const EdgeInsets.all(12), - child: SvgPicture.asset( - Assets.images.calendar, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: widget.accentColor, - ), - ), - ), - onTap: () async { - final selectDate = await showDatePicker( - context: context, - firstDate: DateTime.now().subtract( - const Duration(days: 1000), - ), - lastDate: DateTime.now().add( - const Duration(days: 1000), - ), - ); - if (selectDate != null) { - setState(() { - date = selectDate; - dateController.text = dateFormat.format(date); - }); - } + child: BlocBuilder( + builder: (context, state) { + return CustomDropdownSearch( + label: "", + accentColor: widget.accentColor, + selectedItem: selectedParty, + items: (filter, infiniteScrollProps) { + return state.parties; }, - ), - ], - ), - ), - Expanded( - child: Column( - spacing: 8.h, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.transactionTime.tr(), - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.ellipsis, - ), - TextFormField( - readOnly: true, - controller: timeController, - decoration: InputDecoration( - suffixIcon: Padding( - padding: const EdgeInsets.all(12), - child: SvgPicture.asset( - Assets.images.clock, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: widget.accentColor, - ), - ), - ), - onTap: () async { - final selectTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (selectTime != null) { - setState(() { - time = selectTime; - date = DateTime( - date.year, - date.month, - date.day, - selectTime.hour, - selectTime.minute, - ); - timeController.text = timeFormat.format(date); - }); - } + itemAsString: (item) => item.name, + onChanged: (value) { + setState(() { + selectedParty = value; + }); }, - ), - ], + compareFn: (i1, i2) => i1.clientId == i2.clientId, + filterFn: (el, filter) => el.name + .toLowerCase() + .contains(filter.toLowerCase()), + ); + }, ), ), - ], - ), - SizedBox(height: 16.h), - Text( - widget.transactionType == TransactionType.expense - ? '${LocaleKeys.transactionSentTo.tr()} (${LocaleKeys.party.tr()})' - : '${LocaleKeys.transactionReceivedFrom.tr()} (${LocaleKeys.party.tr()})', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 8.h), - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: BlocBuilder( - builder: (context, state) { - return CustomDropdownSearch( - label: "", - accentColor: widget.accentColor, - selectedItem: selectedParty, - items: (filter, infiniteScrollProps) { - return state.parties; - }, - itemAsString: (item) => item.name, - onChanged: (value) { - setState(() { - selectedParty = value; - }); - }, - compareFn: (i1, i2) => i1.clientId == i2.clientId, - filterFn: (el, filter) => el.name - .toLowerCase() - .contains(filter.toLowerCase()), - ); + GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: (context) { + return const AddPartyDialog(); }, + ); + }, + child: Container( + width: 60.w, + constraints: BoxConstraints( + maxHeight: 50.h, ), - ), - GestureDetector( - onTap: () async { - await showDialog( - context: context, - builder: (context) { - return const AddPartyDialog(); - }, - ); - }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - ), - child: const Center( - child: Icon(Icons.add), - ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Icon(Icons.add), ), ), - ], - ), - ), - SizedBox(height: 16.h), - Text( - LocaleKeys.transactionCategory.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), + ), + ], ), - SizedBox(height: 8.h), - IntrinsicHeight( - child: Row( - spacing: 16.w, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextFormField( - controller: categoryController, - readOnly: true, - onTap: () { - showCustomBottomSheet( - context, - widget: BlocBuilder( - builder: (context, state) { - //Category by transaction type - final searchCategories = state.categories.where( - (element) => - element.type == widget.transactionType); + ), + SizedBox(height: 16.h), + Text( + LocaleKeys.transactionCategory.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + IntrinsicHeight( + child: Row( + spacing: 16.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextFormField( + controller: categoryController, + readOnly: true, + onTap: () { + showCustomBottomSheet( + context, + widget: BlocBuilder( + builder: (context, state) { + //Category by transaction type + final searchCategories = state.categories.where( + (element) => + element.type == widget.transactionType); - return CustomDropdownSearch( - label: "", - accentColor: widget.accentColor, - selectedItem: selectedCategory, - items: (filter, infiniteScrollProps) { - return searchCategories - .map((data) => data) - .toList() - .where((CategoryEntity el) => el.name - .toLowerCase() - .contains(filter.toLowerCase())) - .toList(); - }, - itemAsString: (item) => item.name, - onChanged: (value) { - setState(() { - selectedCategory = value; - if (value != null) { - categoryController.text = value.name; - } + return CustomDropdownSearch( + label: "", + accentColor: widget.accentColor, + selectedItem: selectedCategory, + items: (filter, infiniteScrollProps) { + return searchCategories + .map((data) => data) + .toList() + .where((CategoryEntity el) => el.name + .toLowerCase() + .contains(filter.toLowerCase())) + .toList(); + }, + itemAsString: (item) => item.name, + onChanged: (value) { + setState(() { + selectedCategory = value; + if (value != null) { + categoryController.text = value.name; + } - Navigator.pop(context); - }); - }, - compareFn: (i1, i2) => - i1.clientId == i2.clientId, - filterFn: (el, filter) { - return el.name.toLowerCase().contains( - filter.toLowerCase(), - ); - }, - ); - }, - ), - ); - }, - decoration: InputDecoration( - hintText: LocaleKeys.selectCategory.tr(), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: widget.accentColor, - ), - ), - suffixIcon: Padding( - padding: const EdgeInsets.all(12), - child: SvgPicture.asset( - Assets.images.arrowDown, - colorFilter: ColorFilter.mode( - Colors.grey.shade500, - BlendMode.srcIn, - ), - ), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return LocaleKeys.categoryIsRequired.tr(); - } - return null; - }, - ), - ), - GestureDetector( - onTap: () async { - AppNavigator.push( - context, - AddCategoryScreen( - accentColor: widget.accentColor, - type: widget.transactionType, + Navigator.pop(context); + }); + }, + compareFn: (i1, i2) => + i1.clientId == i2.clientId, + filterFn: (el, filter) { + return el.name.toLowerCase().contains( + filter.toLowerCase(), + ); + }, + ); + }, ), ); }, - child: Container( - width: 60.w, - constraints: BoxConstraints( - maxHeight: 50.h, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + decoration: InputDecoration( + hintText: LocaleKeys.selectCategory.tr(), + focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, + ), ), - child: const Center( - child: Icon(Icons.add), + suffixIcon: Padding( + padding: const EdgeInsets.all(12), + child: SvgPicture.asset( + Assets.images.arrowDown, + colorFilter: ColorFilter.mode( + Colors.grey.shade500, + BlendMode.srcIn, + ), + ), ), ), + validator: (value) { + if (value == null || value.isEmpty) { + return LocaleKeys.categoryIsRequired.tr(); + } + return null; + }, ), - ], - ), - ), - SizedBox(height: 16.h), - Text( - LocaleKeys.transactionDescription.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, + ), + GestureDetector( + onTap: () async { + AppNavigator.push( + context, + AddCategoryScreen( + accentColor: widget.accentColor, + type: widget.transactionType, + ), + ); + }, + child: Container( + width: 60.w, + constraints: BoxConstraints( + maxHeight: 50.h, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Icon(Icons.add), + ), ), + ), + ], ), - SizedBox(height: 8.h), - TextFormField( - controller: descriptionController, - decoration: InputDecoration( - hintText: LocaleKeys.transactionTypeHere.tr(), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: widget.accentColor, - ), + ), + SizedBox(height: 16.h), + Text( + LocaleKeys.transactionDescription.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + TextFormField( + controller: descriptionController, + decoration: InputDecoration( + hintText: LocaleKeys.transactionTypeHere.tr(), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: widget.accentColor, ), ), ), - SizedBox(height: 16.h), - Text( - LocaleKeys.transactionAttachment.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 8.h), - AttachmentSourceRow( - accentColor: widget.accentColor, - onFileAdded: _addAttachmentPath, - ), - if (_allAttachments.isNotEmpty) ...[ - SizedBox(height: 12.h), - SizedBox( - height: 72.h, - child: AttachmentListView( - items: _allAttachments, - showRemoveButton: true, - accentColor: widget.accentColor, - onRemove: _handleRemove, + ), + SizedBox(height: 16.h), + Text( + LocaleKeys.transactionAttachment.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, ), - ), - ], - SizedBox(height: 20.h), + ), + SizedBox(height: 8.h), + AttachmentSourceRow( + accentColor: widget.accentColor, + onFileAdded: _addAttachmentPath, + ), + if (_allAttachments.isNotEmpty) ...[ + SizedBox(height: 12.h), SizedBox( - height: 56.h, - width: double.infinity, - child: Builder( - builder: (context) { - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(11.r), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ + height: 72.h, + child: AttachmentListView( + items: _allAttachments, + showRemoveButton: true, + accentColor: widget.accentColor, + onRemove: _handleRemove, + ), + ), + ], + SizedBox(height: 20.h), + SizedBox( + height: 56.h, + width: double.infinity, + child: Builder( + builder: (context) { + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(11.r), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + widget.accentColor, + Color.alphaBlend( + Colors.black.withValues(alpha: 0.08), widget.accentColor, - Color.alphaBlend( - Colors.black.withValues(alpha: 0.08), - widget.accentColor, - ), - ], - ), - border: Border.all( - color: Colors.white.withValues(alpha: 0.35), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: widget.accentColor.withValues(alpha: 0.4), - blurRadius: 18, - offset: const Offset(0, 8), ), ], ), - child: ElevatedButton( + border: Border.all( + color: Colors.white.withValues(alpha: 0.35), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: widget.accentColor.withValues(alpha: 0.4), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: ElevatedButton( style: ButtonStyle( backgroundColor: const WidgetStatePropertyAll(Colors.transparent), foregroundColor: const WidgetStatePropertyAll(Colors.white), - elevation: - const WidgetStatePropertyAll(0), - shadowColor: const WidgetStatePropertyAll( - Colors.transparent), + elevation: const WidgetStatePropertyAll(0), + shadowColor: + const WidgetStatePropertyAll(Colors.transparent), overlayColor: WidgetStatePropertyAll( Colors.white.withValues(alpha: 0.16)), shape: WidgetStatePropertyAll( RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(14.r), + borderRadius: BorderRadius.circular(14.r), ), ), textStyle: WidgetStatePropertyAll( @@ -822,13 +807,12 @@ class _AddTransactionFormState extends State ], ), ), - ); - }, - ), + ); + }, ), - SizedBox(height: 20.h), - ], - ), + ), + SizedBox(height: 20.h), + ], ), ), ); diff --git a/pubspec.lock b/pubspec.lock index 43d0659f..93295228 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" charcode: dependency: transitive description: @@ -1276,10 +1276,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -2150,5 +2150,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.32.0" diff --git a/test/drift/default/generated/schema.dart b/test/drift/default/generated/schema.dart index 22131b11..c42542af 100644 --- a/test/drift/default/generated/schema.dart +++ b/test/drift/default/generated/schema.dart @@ -7,6 +7,7 @@ import 'schema_v1.dart' as v1; import 'schema_v2.dart' as v2; import 'schema_v3.dart' as v3; import 'schema_v4.dart' as v4; +import 'schema_v5.dart' as v5; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -20,10 +21,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v3.DatabaseAtV3(db); case 4: return v4.DatabaseAtV4(db); + case 5: + return v5.DatabaseAtV5(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4]; + static const versions = const [1, 2, 3, 4, 5]; } diff --git a/test/drift/default/generated/schema_v5.dart b/test/drift/default/generated/schema_v5.dart new file mode 100644 index 00000000..b1104adf --- /dev/null +++ b/test/drift/default/generated/schema_v5.dart @@ -0,0 +1,7911 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class Wallets extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Wallets(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn balance = GeneratedColumn( + 'balance', aliasedName, false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0.0')); + late final GeneratedColumn currency = GeneratedColumn( + 'currency', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn stats = GeneratedColumn( + 'stats', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn icon = GeneratedColumn( + 'icon', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + name, + type, + balance, + currency, + description, + stats, + icon + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'wallets'; + @override + Set get $primaryKey => {clientId}; + @override + WalletsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return WalletsData( + 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'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + balance: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}balance'])!, + currency: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}currency'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + stats: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}stats']), + icon: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}icon']), + ); + } + + @override + Wallets createAlias(String alias) { + return Wallets(attachedDatabase, alias); + } +} + +class WalletsData 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 type; + final double balance; + final String currency; + final String? description; + final String? stats; + final String? icon; + const WalletsData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.name, + required this.type, + required this.balance, + required this.currency, + this.description, + this.stats, + this.icon}); + @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['type'] = Variable(type); + map['balance'] = Variable(balance); + map['currency'] = Variable(currency); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || stats != null) { + map['stats'] = Variable(stats); + } + if (!nullToAbsent || icon != null) { + map['icon'] = Variable(icon); + } + return map; + } + + WalletsCompanion toCompanion(bool nullToAbsent) { + return WalletsCompanion( + 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), + type: Value(type), + balance: Value(balance), + currency: Value(currency), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + stats: + stats == null && nullToAbsent ? const Value.absent() : Value(stats), + icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), + ); + } + + factory WalletsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return WalletsData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + balance: serializer.fromJson(json['balance']), + currency: serializer.fromJson(json['currency']), + description: serializer.fromJson(json['description']), + stats: serializer.fromJson(json['stats']), + icon: serializer.fromJson(json['icon']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'balance': serializer.toJson(balance), + 'currency': serializer.toJson(currency), + 'description': serializer.toJson(description), + 'stats': serializer.toJson(stats), + 'icon': serializer.toJson(icon), + }; + } + + WalletsData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? name, + String? type, + double? balance, + String? currency, + Value description = const Value.absent(), + Value stats = const Value.absent(), + Value icon = const Value.absent()}) => + WalletsData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + name: name ?? this.name, + type: type ?? this.type, + balance: balance ?? this.balance, + currency: currency ?? this.currency, + description: description.present ? description.value : this.description, + stats: stats.present ? stats.value : this.stats, + icon: icon.present ? icon.value : this.icon, + ); + WalletsData copyWithCompanion(WalletsCompanion data) { + return WalletsData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + balance: data.balance.present ? data.balance.value : this.balance, + currency: data.currency.present ? data.currency.value : this.currency, + description: + data.description.present ? data.description.value : this.description, + stats: data.stats.present ? data.stats.value : this.stats, + icon: data.icon.present ? data.icon.value : this.icon, + ); + } + + @override + String toString() { + return (StringBuffer('WalletsData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('balance: $balance, ') + ..write('currency: $currency, ') + ..write('description: $description, ') + ..write('stats: $stats, ') + ..write('icon: $icon') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + name, + type, + balance, + currency, + description, + stats, + icon); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is WalletsData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.name == this.name && + other.type == this.type && + other.balance == this.balance && + other.currency == this.currency && + other.description == this.description && + other.stats == this.stats && + other.icon == this.icon); +} + +class WalletsCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value name; + final Value type; + final Value balance; + final Value currency; + final Value description; + final Value stats; + final Value icon; + final Value rowid; + const WalletsCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.name = const Value.absent(), + this.type = const Value.absent(), + this.balance = const Value.absent(), + this.currency = const Value.absent(), + this.description = const Value.absent(), + this.stats = const Value.absent(), + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }); + WalletsCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String name, + required String type, + this.balance = const Value.absent(), + required String currency, + this.description = const Value.absent(), + this.stats = const Value.absent(), + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name), + type = Value(type), + currency = Value(currency); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? name, + Expression? type, + Expression? balance, + Expression? currency, + Expression? description, + Expression? stats, + Expression? icon, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (name != null) 'name': name, + if (type != null) 'type': type, + if (balance != null) 'balance': balance, + if (currency != null) 'currency': currency, + if (description != null) 'description': description, + if (stats != null) 'stats': stats, + if (icon != null) 'icon': icon, + if (rowid != null) 'rowid': rowid, + }); + } + + WalletsCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? name, + Value? type, + Value? balance, + Value? currency, + Value? description, + Value? stats, + Value? icon, + Value? rowid}) { + return WalletsCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + name: name ?? this.name, + type: type ?? this.type, + balance: balance ?? this.balance, + currency: currency ?? this.currency, + description: description ?? this.description, + stats: stats ?? this.stats, + icon: icon ?? this.icon, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (balance.present) { + map['balance'] = Variable(balance.value); + } + if (currency.present) { + map['currency'] = Variable(currency.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (stats.present) { + map['stats'] = Variable(stats.value); + } + if (icon.present) { + map['icon'] = Variable(icon.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('WalletsCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('balance: $balance, ') + ..write('currency: $currency, ') + ..write('description: $description, ') + ..write('stats: $stats, ') + ..write('icon: $icon, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Parties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Parties(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn icon = GeneratedColumn( + 'icon', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + name, + description, + icon, + type + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'parties'; + @override + Set get $primaryKey => {clientId}; + @override + PartiesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartiesData( + 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'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + icon: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}icon']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type']), + ); + } + + @override + Parties createAlias(String alias) { + return Parties(attachedDatabase, alias); + } +} + +class PartiesData 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? description; + final String? icon; + final String? type; + const PartiesData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.name, + this.description, + this.icon, + this.type}); + @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); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || icon != null) { + map['icon'] = Variable(icon); + } + if (!nullToAbsent || type != null) { + map['type'] = Variable(type); + } + return map; + } + + PartiesCompanion toCompanion(bool nullToAbsent) { + return PartiesCompanion( + 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), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), + type: type == null && nullToAbsent ? const Value.absent() : Value(type), + ); + } + + factory PartiesData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartiesData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + icon: serializer.fromJson(json['icon']), + type: serializer.fromJson(json['type']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'icon': serializer.toJson(icon), + 'type': serializer.toJson(type), + }; + } + + PartiesData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? name, + Value description = const Value.absent(), + Value icon = const Value.absent(), + Value type = const Value.absent()}) => + PartiesData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + name: name ?? this.name, + description: description.present ? description.value : this.description, + icon: icon.present ? icon.value : this.icon, + type: type.present ? type.value : this.type, + ); + PartiesData copyWithCompanion(PartiesCompanion data) { + return PartiesData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + icon: data.icon.present ? data.icon.value : this.icon, + type: data.type.present ? data.type.value : this.type, + ); + } + + @override + String toString() { + return (StringBuffer('PartiesData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('icon: $icon, ') + ..write('type: $type') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, userId, clientId, rev, createdAt, + updatedAt, deletedAt, lastSyncedAt, name, description, icon, type); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartiesData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.name == this.name && + other.description == this.description && + other.icon == this.icon && + other.type == this.type); +} + +class PartiesCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value name; + final Value description; + final Value icon; + final Value type; + final Value rowid; + const PartiesCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.icon = const Value.absent(), + this.type = const Value.absent(), + this.rowid = const Value.absent(), + }); + PartiesCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String name, + this.description = const Value.absent(), + this.icon = const Value.absent(), + this.type = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? name, + Expression? description, + Expression? icon, + Expression? type, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (icon != null) 'icon': icon, + if (type != null) 'type': type, + if (rowid != null) 'rowid': rowid, + }); + } + + PartiesCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? name, + Value? description, + Value? icon, + Value? type, + Value? rowid}) { + return PartiesCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + name: name ?? this.name, + description: description ?? this.description, + icon: icon ?? this.icon, + type: type ?? this.type, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (icon.present) { + map['icon'] = Variable(icon.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartiesCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('icon: $icon, ') + ..write('type: $type, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Groups extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Groups(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn icon = GeneratedColumn( + 'icon', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + name, + description, + icon + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + Set get $primaryKey => {clientId}; + @override + GroupsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupsData( + 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'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + icon: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}icon']), + ); + } + + @override + Groups createAlias(String alias) { + return Groups(attachedDatabase, alias); + } +} + +class GroupsData 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? description; + final String? icon; + const GroupsData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.name, + this.description, + this.icon}); + @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); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || icon != null) { + map['icon'] = Variable(icon); + } + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + 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), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), + ); + } + + factory GroupsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupsData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + icon: serializer.fromJson(json['icon']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'icon': serializer.toJson(icon), + }; + } + + GroupsData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? name, + Value description = const Value.absent(), + Value icon = const Value.absent()}) => + GroupsData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + name: name ?? this.name, + description: description.present ? description.value : this.description, + icon: icon.present ? icon.value : this.icon, + ); + GroupsData copyWithCompanion(GroupsCompanion data) { + return GroupsData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + icon: data.icon.present ? data.icon.value : this.icon, + ); + } + + @override + String toString() { + return (StringBuffer('GroupsData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('icon: $icon') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, userId, clientId, rev, createdAt, + updatedAt, deletedAt, lastSyncedAt, name, description, icon); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupsData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.name == this.name && + other.description == this.description && + other.icon == this.icon); +} + +class GroupsCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value name; + final Value description; + final Value icon; + final Value rowid; + const GroupsCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupsCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String name, + this.description = const Value.absent(), + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? name, + Expression? description, + Expression? icon, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (icon != null) 'icon': icon, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupsCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? name, + Value? description, + Value? icon, + Value? rowid}) { + return GroupsCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + name: name ?? this.name, + description: description ?? this.description, + icon: icon ?? this.icon, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (icon.present) { + map['icon'] = Variable(icon.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('icon: $icon, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Transactions extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Transactions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn amount = GeneratedColumn( + 'amount', aliasedName, false, + type: DriftSqlType.double, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn datetime = GeneratedColumn( + 'datetime', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn partyId = GeneratedColumn( + 'party_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn walletId = GeneratedColumn( + 'wallet_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn walletClientId = GeneratedColumn( + 'wallet_client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES wallets (client_id)')); + late final GeneratedColumn partyClientId = GeneratedColumn( + 'party_client_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES parties (client_id)')); + late final GeneratedColumn groupClientId = GeneratedColumn( + 'group_client_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (client_id)')); + late final GeneratedColumn transferId = GeneratedColumn( + 'transfer_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn transferClientId = GeneratedColumn( + 'transfer_client_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + amount, + type, + description, + datetime, + partyId, + walletId, + groupId, + walletClientId, + partyClientId, + groupClientId, + transferId, + transferClientId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'transactions'; + @override + Set get $primaryKey => {clientId}; + @override + TransactionsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TransactionsData( + 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']), + amount: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}amount'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + datetime: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}datetime']), + partyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}party_id']), + walletId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}wallet_id']), + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}group_id']), + walletClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}wallet_client_id'])!, + partyClientId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}party_client_id']), + groupClientId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_client_id']), + transferId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}transfer_id']), + transferClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}transfer_client_id']), + ); + } + + @override + Transactions createAlias(String alias) { + return Transactions(attachedDatabase, alias); + } +} + +class TransactionsData 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 double amount; + final String type; + final String? description; + final DateTime? datetime; + final int? partyId; + final int? walletId; + final int? groupId; + final String walletClientId; + final String? partyClientId; + final String? groupClientId; + final int? transferId; + final String? transferClientId; + const TransactionsData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.amount, + required this.type, + this.description, + this.datetime, + this.partyId, + this.walletId, + this.groupId, + required this.walletClientId, + this.partyClientId, + this.groupClientId, + this.transferId, + this.transferClientId}); + @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['amount'] = Variable(amount); + map['type'] = Variable(type); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || datetime != null) { + map['datetime'] = Variable(datetime); + } + if (!nullToAbsent || partyId != null) { + map['party_id'] = Variable(partyId); + } + if (!nullToAbsent || walletId != null) { + map['wallet_id'] = Variable(walletId); + } + if (!nullToAbsent || groupId != null) { + map['group_id'] = Variable(groupId); + } + map['wallet_client_id'] = Variable(walletClientId); + if (!nullToAbsent || partyClientId != null) { + map['party_client_id'] = Variable(partyClientId); + } + if (!nullToAbsent || groupClientId != null) { + map['group_client_id'] = Variable(groupClientId); + } + if (!nullToAbsent || transferId != null) { + map['transfer_id'] = Variable(transferId); + } + if (!nullToAbsent || transferClientId != null) { + map['transfer_client_id'] = Variable(transferClientId); + } + return map; + } + + TransactionsCompanion toCompanion(bool nullToAbsent) { + return TransactionsCompanion( + 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), + amount: Value(amount), + type: Value(type), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + datetime: datetime == null && nullToAbsent + ? const Value.absent() + : Value(datetime), + partyId: partyId == null && nullToAbsent + ? const Value.absent() + : Value(partyId), + walletId: walletId == null && nullToAbsent + ? const Value.absent() + : Value(walletId), + groupId: groupId == null && nullToAbsent + ? const Value.absent() + : Value(groupId), + walletClientId: Value(walletClientId), + partyClientId: partyClientId == null && nullToAbsent + ? const Value.absent() + : Value(partyClientId), + groupClientId: groupClientId == null && nullToAbsent + ? const Value.absent() + : Value(groupClientId), + transferId: transferId == null && nullToAbsent + ? const Value.absent() + : Value(transferId), + transferClientId: transferClientId == null && nullToAbsent + ? const Value.absent() + : Value(transferClientId), + ); + } + + factory TransactionsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TransactionsData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + amount: serializer.fromJson(json['amount']), + type: serializer.fromJson(json['type']), + description: serializer.fromJson(json['description']), + datetime: serializer.fromJson(json['datetime']), + partyId: serializer.fromJson(json['partyId']), + walletId: serializer.fromJson(json['walletId']), + groupId: serializer.fromJson(json['groupId']), + walletClientId: serializer.fromJson(json['walletClientId']), + partyClientId: serializer.fromJson(json['partyClientId']), + groupClientId: serializer.fromJson(json['groupClientId']), + transferId: serializer.fromJson(json['transferId']), + transferClientId: serializer.fromJson(json['transferClientId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'amount': serializer.toJson(amount), + 'type': serializer.toJson(type), + 'description': serializer.toJson(description), + 'datetime': serializer.toJson(datetime), + 'partyId': serializer.toJson(partyId), + 'walletId': serializer.toJson(walletId), + 'groupId': serializer.toJson(groupId), + 'walletClientId': serializer.toJson(walletClientId), + 'partyClientId': serializer.toJson(partyClientId), + 'groupClientId': serializer.toJson(groupClientId), + 'transferId': serializer.toJson(transferId), + 'transferClientId': serializer.toJson(transferClientId), + }; + } + + TransactionsData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + double? amount, + String? type, + Value description = const Value.absent(), + Value datetime = const Value.absent(), + Value partyId = const Value.absent(), + Value walletId = const Value.absent(), + Value groupId = const Value.absent(), + String? walletClientId, + Value partyClientId = const Value.absent(), + Value groupClientId = const Value.absent(), + Value transferId = const Value.absent(), + Value transferClientId = const Value.absent()}) => + TransactionsData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + amount: amount ?? this.amount, + type: type ?? this.type, + description: description.present ? description.value : this.description, + datetime: datetime.present ? datetime.value : this.datetime, + partyId: partyId.present ? partyId.value : this.partyId, + walletId: walletId.present ? walletId.value : this.walletId, + groupId: groupId.present ? groupId.value : this.groupId, + walletClientId: walletClientId ?? this.walletClientId, + partyClientId: + partyClientId.present ? partyClientId.value : this.partyClientId, + groupClientId: + groupClientId.present ? groupClientId.value : this.groupClientId, + transferId: transferId.present ? transferId.value : this.transferId, + transferClientId: transferClientId.present + ? transferClientId.value + : this.transferClientId, + ); + TransactionsData copyWithCompanion(TransactionsCompanion data) { + return TransactionsData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + amount: data.amount.present ? data.amount.value : this.amount, + type: data.type.present ? data.type.value : this.type, + description: + data.description.present ? data.description.value : this.description, + datetime: data.datetime.present ? data.datetime.value : this.datetime, + partyId: data.partyId.present ? data.partyId.value : this.partyId, + walletId: data.walletId.present ? data.walletId.value : this.walletId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + walletClientId: data.walletClientId.present + ? data.walletClientId.value + : this.walletClientId, + partyClientId: data.partyClientId.present + ? data.partyClientId.value + : this.partyClientId, + groupClientId: data.groupClientId.present + ? data.groupClientId.value + : this.groupClientId, + transferId: + data.transferId.present ? data.transferId.value : this.transferId, + transferClientId: data.transferClientId.present + ? data.transferClientId.value + : this.transferClientId, + ); + } + + @override + String toString() { + return (StringBuffer('TransactionsData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('amount: $amount, ') + ..write('type: $type, ') + ..write('description: $description, ') + ..write('datetime: $datetime, ') + ..write('partyId: $partyId, ') + ..write('walletId: $walletId, ') + ..write('groupId: $groupId, ') + ..write('walletClientId: $walletClientId, ') + ..write('partyClientId: $partyClientId, ') + ..write('groupClientId: $groupClientId, ') + ..write('transferId: $transferId, ') + ..write('transferClientId: $transferClientId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + amount, + type, + description, + datetime, + partyId, + walletId, + groupId, + walletClientId, + partyClientId, + groupClientId, + transferId, + transferClientId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TransactionsData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.amount == this.amount && + other.type == this.type && + other.description == this.description && + other.datetime == this.datetime && + other.partyId == this.partyId && + other.walletId == this.walletId && + other.groupId == this.groupId && + other.walletClientId == this.walletClientId && + other.partyClientId == this.partyClientId && + other.groupClientId == this.groupClientId && + other.transferId == this.transferId && + other.transferClientId == this.transferClientId); +} + +class TransactionsCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value amount; + final Value type; + final Value description; + final Value datetime; + final Value partyId; + final Value walletId; + final Value groupId; + final Value walletClientId; + final Value partyClientId; + final Value groupClientId; + final Value transferId; + final Value transferClientId; + final Value rowid; + const TransactionsCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.amount = const Value.absent(), + this.type = const Value.absent(), + this.description = const Value.absent(), + this.datetime = const Value.absent(), + this.partyId = const Value.absent(), + this.walletId = const Value.absent(), + this.groupId = const Value.absent(), + this.walletClientId = const Value.absent(), + this.partyClientId = const Value.absent(), + this.groupClientId = const Value.absent(), + this.transferId = const Value.absent(), + this.transferClientId = const Value.absent(), + this.rowid = const Value.absent(), + }); + TransactionsCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required double amount, + required String type, + this.description = const Value.absent(), + this.datetime = const Value.absent(), + this.partyId = const Value.absent(), + this.walletId = const Value.absent(), + this.groupId = const Value.absent(), + required String walletClientId, + this.partyClientId = const Value.absent(), + this.groupClientId = const Value.absent(), + this.transferId = const Value.absent(), + this.transferClientId = const Value.absent(), + this.rowid = const Value.absent(), + }) : amount = Value(amount), + type = Value(type), + walletClientId = Value(walletClientId); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? amount, + Expression? type, + Expression? description, + Expression? datetime, + Expression? partyId, + Expression? walletId, + Expression? groupId, + Expression? walletClientId, + Expression? partyClientId, + Expression? groupClientId, + Expression? transferId, + Expression? transferClientId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (amount != null) 'amount': amount, + if (type != null) 'type': type, + if (description != null) 'description': description, + if (datetime != null) 'datetime': datetime, + if (partyId != null) 'party_id': partyId, + if (walletId != null) 'wallet_id': walletId, + if (groupId != null) 'group_id': groupId, + if (walletClientId != null) 'wallet_client_id': walletClientId, + if (partyClientId != null) 'party_client_id': partyClientId, + if (groupClientId != null) 'group_client_id': groupClientId, + if (transferId != null) 'transfer_id': transferId, + if (transferClientId != null) 'transfer_client_id': transferClientId, + if (rowid != null) 'rowid': rowid, + }); + } + + TransactionsCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? amount, + Value? type, + Value? description, + Value? datetime, + Value? partyId, + Value? walletId, + Value? groupId, + Value? walletClientId, + Value? partyClientId, + Value? groupClientId, + Value? transferId, + Value? transferClientId, + Value? rowid}) { + return TransactionsCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + amount: amount ?? this.amount, + type: type ?? this.type, + description: description ?? this.description, + datetime: datetime ?? this.datetime, + partyId: partyId ?? this.partyId, + walletId: walletId ?? this.walletId, + groupId: groupId ?? this.groupId, + walletClientId: walletClientId ?? this.walletClientId, + partyClientId: partyClientId ?? this.partyClientId, + groupClientId: groupClientId ?? this.groupClientId, + transferId: transferId ?? this.transferId, + transferClientId: transferClientId ?? this.transferClientId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (amount.present) { + map['amount'] = Variable(amount.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (datetime.present) { + map['datetime'] = Variable(datetime.value); + } + if (partyId.present) { + map['party_id'] = Variable(partyId.value); + } + if (walletId.present) { + map['wallet_id'] = Variable(walletId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (walletClientId.present) { + map['wallet_client_id'] = Variable(walletClientId.value); + } + if (partyClientId.present) { + map['party_client_id'] = Variable(partyClientId.value); + } + if (groupClientId.present) { + map['group_client_id'] = Variable(groupClientId.value); + } + if (transferId.present) { + map['transfer_id'] = Variable(transferId.value); + } + if (transferClientId.present) { + map['transfer_client_id'] = Variable(transferClientId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TransactionsCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('amount: $amount, ') + ..write('type: $type, ') + ..write('description: $description, ') + ..write('datetime: $datetime, ') + ..write('partyId: $partyId, ') + ..write('walletId: $walletId, ') + ..write('groupId: $groupId, ') + ..write('walletClientId: $walletClientId, ') + ..write('partyClientId: $partyClientId, ') + ..write('groupClientId: $groupClientId, ') + ..write('transferId: $transferId, ') + ..write('transferClientId: $transferClientId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Categories extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Categories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn slug = GeneratedColumn( + 'slug', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn icon = GeneratedColumn( + 'icon', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + name, + slug, + description, + type, + icon + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'categories'; + @override + Set get $primaryKey => {clientId}; + @override + CategoriesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CategoriesData( + 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']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + icon: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}icon']), + ); + } + + @override + Categories createAlias(String alias) { + return Categories(attachedDatabase, alias); + } +} + +class CategoriesData 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 String type; + final String? icon; + const CategoriesData( + {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.type, + this.icon}); + @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['type'] = Variable(type); + if (!nullToAbsent || icon != null) { + map['icon'] = Variable(icon); + } + return map; + } + + CategoriesCompanion toCompanion(bool nullToAbsent) { + return CategoriesCompanion( + 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), + type: Value(type), + icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), + ); + } + + factory CategoriesData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CategoriesData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + name: serializer.fromJson(json['name']), + slug: serializer.fromJson(json['slug']), + description: serializer.fromJson(json['description']), + type: serializer.fromJson(json['type']), + icon: serializer.fromJson(json['icon']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'name': serializer.toJson(name), + 'slug': serializer.toJson(slug), + 'description': serializer.toJson(description), + 'type': serializer.toJson(type), + 'icon': serializer.toJson(icon), + }; + } + + CategoriesData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? name, + String? slug, + Value description = const Value.absent(), + String? type, + Value icon = const Value.absent()}) => + CategoriesData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + name: name ?? this.name, + slug: slug ?? this.slug, + description: description.present ? description.value : this.description, + type: type ?? this.type, + icon: icon.present ? icon.value : this.icon, + ); + CategoriesData copyWithCompanion(CategoriesCompanion data) { + return CategoriesData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + name: data.name.present ? data.name.value : this.name, + slug: data.slug.present ? data.slug.value : this.slug, + description: + data.description.present ? data.description.value : this.description, + type: data.type.present ? data.type.value : this.type, + icon: data.icon.present ? data.icon.value : this.icon, + ); + } + + @override + String toString() { + return (StringBuffer('CategoriesData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('slug: $slug, ') + ..write('description: $description, ') + ..write('type: $type, ') + ..write('icon: $icon') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, userId, clientId, rev, createdAt, + updatedAt, deletedAt, lastSyncedAt, name, slug, description, type, icon); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CategoriesData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.name == this.name && + other.slug == this.slug && + other.description == this.description && + other.type == this.type && + other.icon == this.icon); +} + +class CategoriesCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value name; + final Value slug; + final Value description; + final Value type; + final Value icon; + final Value rowid; + const CategoriesCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.name = const Value.absent(), + this.slug = const Value.absent(), + this.description = const Value.absent(), + this.type = const Value.absent(), + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }); + CategoriesCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String name, + required String slug, + this.description = const Value.absent(), + required String type, + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name), + slug = Value(slug), + type = Value(type); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? name, + Expression? slug, + Expression? description, + Expression? type, + Expression? icon, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (name != null) 'name': name, + if (slug != null) 'slug': slug, + if (description != null) 'description': description, + if (type != null) 'type': type, + if (icon != null) 'icon': icon, + if (rowid != null) 'rowid': rowid, + }); + } + + CategoriesCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? name, + Value? slug, + Value? description, + Value? type, + Value? icon, + Value? rowid}) { + return CategoriesCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + name: name ?? this.name, + slug: slug ?? this.slug, + description: description ?? this.description, + type: type ?? this.type, + icon: icon ?? this.icon, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (slug.present) { + map['slug'] = Variable(slug.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (icon.present) { + map['icon'] = Variable(icon.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CategoriesCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('slug: $slug, ') + ..write('description: $description, ') + ..write('type: $type, ') + ..write('icon: $icon, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Configs extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Configs(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn key = GeneratedColumn( + 'key', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn value = GeneratedColumn( + 'value', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + key, + type, + value + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'configs'; + @override + Set get $primaryKey => {clientId}; + @override + ConfigsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ConfigsData( + 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']), + key: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}key'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + value: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}value'])!, + ); + } + + @override + Configs createAlias(String alias) { + return Configs(attachedDatabase, alias); + } +} + +class ConfigsData 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 key; + final String type; + final String value; + const ConfigsData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.key, + required this.type, + required this.value}); + @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['key'] = Variable(key); + map['type'] = Variable(type); + map['value'] = Variable(value); + return map; + } + + ConfigsCompanion toCompanion(bool nullToAbsent) { + return ConfigsCompanion( + 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), + key: Value(key), + type: Value(type), + value: Value(value), + ); + } + + factory ConfigsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ConfigsData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + key: serializer.fromJson(json['key']), + type: serializer.fromJson(json['type']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'key': serializer.toJson(key), + 'type': serializer.toJson(type), + 'value': serializer.toJson(value), + }; + } + + ConfigsData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? key, + String? type, + String? value}) => + ConfigsData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + key: key ?? this.key, + type: type ?? this.type, + value: value ?? this.value, + ); + ConfigsData copyWithCompanion(ConfigsCompanion data) { + return ConfigsData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + key: data.key.present ? data.key.value : this.key, + type: data.type.present ? data.type.value : this.type, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('ConfigsData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('key: $key, ') + ..write('type: $type, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, userId, clientId, rev, createdAt, + updatedAt, deletedAt, lastSyncedAt, key, type, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ConfigsData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.key == this.key && + other.type == this.type && + other.value == this.value); +} + +class ConfigsCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value key; + final Value type; + final Value value; + final Value rowid; + const ConfigsCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.key = const Value.absent(), + this.type = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + ConfigsCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String key, + required String type, + required String value, + this.rowid = const Value.absent(), + }) : key = Value(key), + type = Value(type), + value = Value(value); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? key, + Expression? type, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (key != null) 'key': key, + if (type != null) 'type': type, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + ConfigsCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? key, + Value? type, + Value? value, + Value? rowid}) { + return ConfigsCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + key: key ?? this.key, + type: type ?? this.type, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ConfigsCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('key: $key, ') + ..write('type: $type, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Users extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Users(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn email = GeneratedColumn( + 'email', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn firstName = GeneratedColumn( + 'first_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn lastName = GeneratedColumn( + 'last_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn phone = GeneratedColumn( + 'phone', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn avatar = GeneratedColumn( + 'avatar', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + email, + firstName, + lastName, + username, + phone, + avatar, + createdAt, + updatedAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'users'; + @override + Set get $primaryKey => {id}; + @override + UsersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UsersData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + email: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}email'])!, + firstName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}first_name'])!, + lastName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}last_name']), + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username']), + phone: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}phone']), + avatar: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}avatar']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']), + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at']), + ); + } + + @override + Users createAlias(String alias) { + return Users(attachedDatabase, alias); + } +} + +class UsersData extends DataClass implements Insertable { + final int id; + final String email; + final String firstName; + final String? lastName; + final String? username; + final String? phone; + final String? avatar; + final DateTime? createdAt; + final DateTime? updatedAt; + const UsersData( + {required this.id, + required this.email, + required this.firstName, + this.lastName, + this.username, + this.phone, + this.avatar, + this.createdAt, + this.updatedAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['email'] = Variable(email); + map['first_name'] = Variable(firstName); + if (!nullToAbsent || lastName != null) { + map['last_name'] = Variable(lastName); + } + if (!nullToAbsent || username != null) { + map['username'] = Variable(username); + } + if (!nullToAbsent || phone != null) { + map['phone'] = Variable(phone); + } + if (!nullToAbsent || avatar != null) { + map['avatar'] = Variable(avatar); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || updatedAt != null) { + map['updated_at'] = Variable(updatedAt); + } + return map; + } + + UsersCompanion toCompanion(bool nullToAbsent) { + return UsersCompanion( + id: Value(id), + email: Value(email), + firstName: Value(firstName), + lastName: lastName == null && nullToAbsent + ? const Value.absent() + : Value(lastName), + username: username == null && nullToAbsent + ? const Value.absent() + : Value(username), + phone: + phone == null && nullToAbsent ? const Value.absent() : Value(phone), + avatar: + avatar == null && nullToAbsent ? const Value.absent() : Value(avatar), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + updatedAt: updatedAt == null && nullToAbsent + ? const Value.absent() + : Value(updatedAt), + ); + } + + factory UsersData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UsersData( + id: serializer.fromJson(json['id']), + email: serializer.fromJson(json['email']), + firstName: serializer.fromJson(json['firstName']), + lastName: serializer.fromJson(json['lastName']), + username: serializer.fromJson(json['username']), + phone: serializer.fromJson(json['phone']), + avatar: serializer.fromJson(json['avatar']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'email': serializer.toJson(email), + 'firstName': serializer.toJson(firstName), + 'lastName': serializer.toJson(lastName), + 'username': serializer.toJson(username), + 'phone': serializer.toJson(phone), + 'avatar': serializer.toJson(avatar), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + UsersData copyWith( + {int? id, + String? email, + String? firstName, + Value lastName = const Value.absent(), + Value username = const Value.absent(), + Value phone = const Value.absent(), + Value avatar = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent()}) => + UsersData( + id: id ?? this.id, + email: email ?? this.email, + firstName: firstName ?? this.firstName, + lastName: lastName.present ? lastName.value : this.lastName, + username: username.present ? username.value : this.username, + phone: phone.present ? phone.value : this.phone, + avatar: avatar.present ? avatar.value : this.avatar, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + updatedAt: updatedAt.present ? updatedAt.value : this.updatedAt, + ); + UsersData copyWithCompanion(UsersCompanion data) { + return UsersData( + id: data.id.present ? data.id.value : this.id, + email: data.email.present ? data.email.value : this.email, + firstName: data.firstName.present ? data.firstName.value : this.firstName, + lastName: data.lastName.present ? data.lastName.value : this.lastName, + username: data.username.present ? data.username.value : this.username, + phone: data.phone.present ? data.phone.value : this.phone, + avatar: data.avatar.present ? data.avatar.value : this.avatar, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('UsersData(') + ..write('id: $id, ') + ..write('email: $email, ') + ..write('firstName: $firstName, ') + ..write('lastName: $lastName, ') + ..write('username: $username, ') + ..write('phone: $phone, ') + ..write('avatar: $avatar, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, email, firstName, lastName, username, + phone, avatar, createdAt, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UsersData && + other.id == this.id && + other.email == this.email && + other.firstName == this.firstName && + other.lastName == this.lastName && + other.username == this.username && + other.phone == this.phone && + other.avatar == this.avatar && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class UsersCompanion extends UpdateCompanion { + final Value id; + final Value email; + final Value firstName; + final Value lastName; + final Value username; + final Value phone; + final Value avatar; + final Value createdAt; + final Value updatedAt; + const UsersCompanion({ + this.id = const Value.absent(), + this.email = const Value.absent(), + this.firstName = const Value.absent(), + this.lastName = const Value.absent(), + this.username = const Value.absent(), + this.phone = const Value.absent(), + this.avatar = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + }); + UsersCompanion.insert({ + this.id = const Value.absent(), + required String email, + required String firstName, + this.lastName = const Value.absent(), + this.username = const Value.absent(), + this.phone = const Value.absent(), + this.avatar = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + }) : email = Value(email), + firstName = Value(firstName); + static Insertable custom({ + Expression? id, + Expression? email, + Expression? firstName, + Expression? lastName, + Expression? username, + Expression? phone, + Expression? avatar, + Expression? createdAt, + Expression? updatedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (email != null) 'email': email, + if (firstName != null) 'first_name': firstName, + if (lastName != null) 'last_name': lastName, + if (username != null) 'username': username, + if (phone != null) 'phone': phone, + if (avatar != null) 'avatar': avatar, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + }); + } + + UsersCompanion copyWith( + {Value? id, + Value? email, + Value? firstName, + Value? lastName, + Value? username, + Value? phone, + Value? avatar, + Value? createdAt, + Value? updatedAt}) { + return UsersCompanion( + id: id ?? this.id, + email: email ?? this.email, + firstName: firstName ?? this.firstName, + lastName: lastName ?? this.lastName, + username: username ?? this.username, + phone: phone ?? this.phone, + avatar: avatar ?? this.avatar, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (firstName.present) { + map['first_name'] = Variable(firstName.value); + } + if (lastName.present) { + map['last_name'] = Variable(lastName.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (phone.present) { + map['phone'] = Variable(phone.value); + } + if (avatar.present) { + map['avatar'] = Variable(avatar.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UsersCompanion(') + ..write('id: $id, ') + ..write('email: $email, ') + ..write('firstName: $firstName, ') + ..write('lastName: $lastName, ') + ..write('username: $username, ') + ..write('phone: $phone, ') + ..write('avatar: $avatar, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } +} + +class LocalChanges extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalChanges(this.attachedDatabase, [this._alias]); + late final GeneratedColumn entityType = GeneratedColumn( + 'entity_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn entityId = GeneratedColumn( + 'entity_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn entityRev = GeneratedColumn( + 'entity_rev', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn deleted = GeneratedColumn( + 'deleted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))')); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createAt = GeneratedColumn( + 'create_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn concluded = GeneratedColumn( + 'concluded', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("concluded" IN (0, 1))')); + late final GeneratedColumn concludedMoment = + GeneratedColumn('concluded_moment', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn error = GeneratedColumn( + 'error', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn dismissed = GeneratedColumn( + 'dismissed', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("dismissed" IN (0, 1))')); + @override + List get $columns => [ + entityType, + entityId, + entityRev, + deleted, + data, + createAt, + concluded, + concludedMoment, + error, + dismissed + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_changes'; + @override + Set get $primaryKey => {entityId, entityType}; + @override + LocalChangesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalChangesData( + entityType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entity_type'])!, + entityId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entity_id'])!, + entityRev: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entity_rev'])!, + deleted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + createAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}create_at'])!, + concluded: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}concluded'])!, + concludedMoment: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}concluded_moment']), + error: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}error']), + dismissed: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}dismissed'])!, + ); + } + + @override + LocalChanges createAlias(String alias) { + return LocalChanges(attachedDatabase, alias); + } +} + +class LocalChangesData extends DataClass + implements Insertable { + final String entityType; + final String entityId; + final String entityRev; + final bool deleted; + final String data; + final DateTime createAt; + final bool concluded; + final DateTime? concludedMoment; + final String? error; + final bool dismissed; + const LocalChangesData( + {required this.entityType, + required this.entityId, + required this.entityRev, + required this.deleted, + required this.data, + required this.createAt, + required this.concluded, + this.concludedMoment, + this.error, + required this.dismissed}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['entity_type'] = Variable(entityType); + map['entity_id'] = Variable(entityId); + map['entity_rev'] = Variable(entityRev); + map['deleted'] = Variable(deleted); + map['data'] = Variable(data); + map['create_at'] = Variable(createAt); + map['concluded'] = Variable(concluded); + if (!nullToAbsent || concludedMoment != null) { + map['concluded_moment'] = Variable(concludedMoment); + } + if (!nullToAbsent || error != null) { + map['error'] = Variable(error); + } + map['dismissed'] = Variable(dismissed); + return map; + } + + LocalChangesCompanion toCompanion(bool nullToAbsent) { + return LocalChangesCompanion( + entityType: Value(entityType), + entityId: Value(entityId), + entityRev: Value(entityRev), + deleted: Value(deleted), + data: Value(data), + createAt: Value(createAt), + concluded: Value(concluded), + concludedMoment: concludedMoment == null && nullToAbsent + ? const Value.absent() + : Value(concludedMoment), + error: + error == null && nullToAbsent ? const Value.absent() : Value(error), + dismissed: Value(dismissed), + ); + } + + factory LocalChangesData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalChangesData( + entityType: serializer.fromJson(json['entityType']), + entityId: serializer.fromJson(json['entityId']), + entityRev: serializer.fromJson(json['entityRev']), + deleted: serializer.fromJson(json['deleted']), + data: serializer.fromJson(json['data']), + createAt: serializer.fromJson(json['createAt']), + concluded: serializer.fromJson(json['concluded']), + concludedMoment: serializer.fromJson(json['concludedMoment']), + error: serializer.fromJson(json['error']), + dismissed: serializer.fromJson(json['dismissed']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'entityType': serializer.toJson(entityType), + 'entityId': serializer.toJson(entityId), + 'entityRev': serializer.toJson(entityRev), + 'deleted': serializer.toJson(deleted), + 'data': serializer.toJson(data), + 'createAt': serializer.toJson(createAt), + 'concluded': serializer.toJson(concluded), + 'concludedMoment': serializer.toJson(concludedMoment), + 'error': serializer.toJson(error), + 'dismissed': serializer.toJson(dismissed), + }; + } + + LocalChangesData copyWith( + {String? entityType, + String? entityId, + String? entityRev, + bool? deleted, + String? data, + DateTime? createAt, + bool? concluded, + Value concludedMoment = const Value.absent(), + Value error = const Value.absent(), + bool? dismissed}) => + LocalChangesData( + entityType: entityType ?? this.entityType, + entityId: entityId ?? this.entityId, + entityRev: entityRev ?? this.entityRev, + deleted: deleted ?? this.deleted, + data: data ?? this.data, + createAt: createAt ?? this.createAt, + concluded: concluded ?? this.concluded, + concludedMoment: concludedMoment.present + ? concludedMoment.value + : this.concludedMoment, + error: error.present ? error.value : this.error, + dismissed: dismissed ?? this.dismissed, + ); + LocalChangesData copyWithCompanion(LocalChangesCompanion data) { + return LocalChangesData( + entityType: + data.entityType.present ? data.entityType.value : this.entityType, + entityId: data.entityId.present ? data.entityId.value : this.entityId, + entityRev: data.entityRev.present ? data.entityRev.value : this.entityRev, + deleted: data.deleted.present ? data.deleted.value : this.deleted, + data: data.data.present ? data.data.value : this.data, + createAt: data.createAt.present ? data.createAt.value : this.createAt, + concluded: data.concluded.present ? data.concluded.value : this.concluded, + concludedMoment: data.concludedMoment.present + ? data.concludedMoment.value + : this.concludedMoment, + error: data.error.present ? data.error.value : this.error, + dismissed: data.dismissed.present ? data.dismissed.value : this.dismissed, + ); + } + + @override + String toString() { + return (StringBuffer('LocalChangesData(') + ..write('entityType: $entityType, ') + ..write('entityId: $entityId, ') + ..write('entityRev: $entityRev, ') + ..write('deleted: $deleted, ') + ..write('data: $data, ') + ..write('createAt: $createAt, ') + ..write('concluded: $concluded, ') + ..write('concludedMoment: $concludedMoment, ') + ..write('error: $error, ') + ..write('dismissed: $dismissed') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(entityType, entityId, entityRev, deleted, + data, createAt, concluded, concludedMoment, error, dismissed); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalChangesData && + other.entityType == this.entityType && + other.entityId == this.entityId && + other.entityRev == this.entityRev && + other.deleted == this.deleted && + other.data == this.data && + other.createAt == this.createAt && + other.concluded == this.concluded && + other.concludedMoment == this.concludedMoment && + other.error == this.error && + other.dismissed == this.dismissed); +} + +class LocalChangesCompanion extends UpdateCompanion { + final Value entityType; + final Value entityId; + final Value entityRev; + final Value deleted; + final Value data; + final Value createAt; + final Value concluded; + final Value concludedMoment; + final Value error; + final Value dismissed; + final Value rowid; + const LocalChangesCompanion({ + this.entityType = const Value.absent(), + this.entityId = const Value.absent(), + this.entityRev = const Value.absent(), + this.deleted = const Value.absent(), + this.data = const Value.absent(), + this.createAt = const Value.absent(), + this.concluded = const Value.absent(), + this.concludedMoment = const Value.absent(), + this.error = const Value.absent(), + this.dismissed = const Value.absent(), + this.rowid = const Value.absent(), + }); + LocalChangesCompanion.insert({ + required String entityType, + required String entityId, + required String entityRev, + required bool deleted, + required String data, + required DateTime createAt, + required bool concluded, + this.concludedMoment = const Value.absent(), + this.error = const Value.absent(), + required bool dismissed, + this.rowid = const Value.absent(), + }) : entityType = Value(entityType), + entityId = Value(entityId), + entityRev = Value(entityRev), + deleted = Value(deleted), + data = Value(data), + createAt = Value(createAt), + concluded = Value(concluded), + dismissed = Value(dismissed); + static Insertable custom({ + Expression? entityType, + Expression? entityId, + Expression? entityRev, + Expression? deleted, + Expression? data, + Expression? createAt, + Expression? concluded, + Expression? concludedMoment, + Expression? error, + Expression? dismissed, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (entityType != null) 'entity_type': entityType, + if (entityId != null) 'entity_id': entityId, + if (entityRev != null) 'entity_rev': entityRev, + if (deleted != null) 'deleted': deleted, + if (data != null) 'data': data, + if (createAt != null) 'create_at': createAt, + if (concluded != null) 'concluded': concluded, + if (concludedMoment != null) 'concluded_moment': concludedMoment, + if (error != null) 'error': error, + if (dismissed != null) 'dismissed': dismissed, + if (rowid != null) 'rowid': rowid, + }); + } + + LocalChangesCompanion copyWith( + {Value? entityType, + Value? entityId, + Value? entityRev, + Value? deleted, + Value? data, + Value? createAt, + Value? concluded, + Value? concludedMoment, + Value? error, + Value? dismissed, + Value? rowid}) { + return LocalChangesCompanion( + entityType: entityType ?? this.entityType, + entityId: entityId ?? this.entityId, + entityRev: entityRev ?? this.entityRev, + deleted: deleted ?? this.deleted, + data: data ?? this.data, + createAt: createAt ?? this.createAt, + concluded: concluded ?? this.concluded, + concludedMoment: concludedMoment ?? this.concludedMoment, + error: error ?? this.error, + dismissed: dismissed ?? this.dismissed, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (entityType.present) { + map['entity_type'] = Variable(entityType.value); + } + if (entityId.present) { + map['entity_id'] = Variable(entityId.value); + } + if (entityRev.present) { + map['entity_rev'] = Variable(entityRev.value); + } + if (deleted.present) { + map['deleted'] = Variable(deleted.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (createAt.present) { + map['create_at'] = Variable(createAt.value); + } + if (concluded.present) { + map['concluded'] = Variable(concluded.value); + } + if (concludedMoment.present) { + map['concluded_moment'] = Variable(concludedMoment.value); + } + if (error.present) { + map['error'] = Variable(error.value); + } + if (dismissed.present) { + map['dismissed'] = Variable(dismissed.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalChangesCompanion(') + ..write('entityType: $entityType, ') + ..write('entityId: $entityId, ') + ..write('entityRev: $entityRev, ') + ..write('deleted: $deleted, ') + ..write('data: $data, ') + ..write('createAt: $createAt, ') + ..write('concluded: $concluded, ') + ..write('concludedMoment: $concludedMoment, ') + ..write('error: $error, ') + ..write('dismissed: $dismissed, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SyncMetadata extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SyncMetadata(this.attachedDatabase, [this._alias]); + late final GeneratedColumn entityType = GeneratedColumn( + 'entity_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [entityType, lastSyncedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'sync_metadata'; + @override + Set get $primaryKey => {entityType}; + @override + SyncMetadataData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SyncMetadataData( + entityType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entity_type'])!, + lastSyncedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}last_synced_at']), + ); + } + + @override + SyncMetadata createAlias(String alias) { + return SyncMetadata(attachedDatabase, alias); + } +} + +class SyncMetadataData extends DataClass + implements Insertable { + final String entityType; + final DateTime? lastSyncedAt; + const SyncMetadataData({required this.entityType, this.lastSyncedAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['entity_type'] = Variable(entityType); + if (!nullToAbsent || lastSyncedAt != null) { + map['last_synced_at'] = Variable(lastSyncedAt); + } + return map; + } + + SyncMetadataCompanion toCompanion(bool nullToAbsent) { + return SyncMetadataCompanion( + entityType: Value(entityType), + lastSyncedAt: lastSyncedAt == null && nullToAbsent + ? const Value.absent() + : Value(lastSyncedAt), + ); + } + + factory SyncMetadataData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SyncMetadataData( + entityType: serializer.fromJson(json['entityType']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'entityType': serializer.toJson(entityType), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + }; + } + + SyncMetadataData copyWith( + {String? entityType, + Value lastSyncedAt = const Value.absent()}) => + SyncMetadataData( + entityType: entityType ?? this.entityType, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + ); + SyncMetadataData copyWithCompanion(SyncMetadataCompanion data) { + return SyncMetadataData( + entityType: + data.entityType.present ? data.entityType.value : this.entityType, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + ); + } + + @override + String toString() { + return (StringBuffer('SyncMetadataData(') + ..write('entityType: $entityType, ') + ..write('lastSyncedAt: $lastSyncedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(entityType, lastSyncedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SyncMetadataData && + other.entityType == this.entityType && + other.lastSyncedAt == this.lastSyncedAt); +} + +class SyncMetadataCompanion extends UpdateCompanion { + final Value entityType; + final Value lastSyncedAt; + final Value rowid; + const SyncMetadataCompanion({ + this.entityType = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SyncMetadataCompanion.insert({ + required String entityType, + this.lastSyncedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : entityType = Value(entityType); + static Insertable custom({ + Expression? entityType, + Expression? lastSyncedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (entityType != null) 'entity_type': entityType, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SyncMetadataCompanion copyWith( + {Value? entityType, + Value? lastSyncedAt, + Value? rowid}) { + return SyncMetadataCompanion( + entityType: entityType ?? this.entityType, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (entityType.present) { + map['entity_type'] = Variable(entityType.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SyncMetadataCompanion(') + ..write('entityType: $entityType, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Categorizables extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Categorizables(this.attachedDatabase, [this._alias]); + late final GeneratedColumn categorizableId = GeneratedColumn( + 'categorizable_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn categorizableType = + GeneratedColumn('categorizable_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn categoryClientId = GeneratedColumn( + 'category_client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES categories (client_id)')); + @override + List get $columns => + [categorizableId, categorizableType, categoryClientId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'categorizables'; + @override + Set get $primaryKey => + {categorizableId, categorizableType, categoryClientId}; + @override + CategorizablesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CategorizablesData( + categorizableId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}categorizable_id'])!, + categorizableType: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}categorizable_type'])!, + categoryClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}category_client_id'])!, + ); + } + + @override + Categorizables createAlias(String alias) { + return Categorizables(attachedDatabase, alias); + } +} + +class CategorizablesData extends DataClass + implements Insertable { + final String categorizableId; + final String categorizableType; + final String categoryClientId; + const CategorizablesData( + {required this.categorizableId, + required this.categorizableType, + required this.categoryClientId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['categorizable_id'] = Variable(categorizableId); + map['categorizable_type'] = Variable(categorizableType); + map['category_client_id'] = Variable(categoryClientId); + return map; + } + + CategorizablesCompanion toCompanion(bool nullToAbsent) { + return CategorizablesCompanion( + categorizableId: Value(categorizableId), + categorizableType: Value(categorizableType), + categoryClientId: Value(categoryClientId), + ); + } + + factory CategorizablesData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CategorizablesData( + categorizableId: serializer.fromJson(json['categorizableId']), + categorizableType: serializer.fromJson(json['categorizableType']), + categoryClientId: serializer.fromJson(json['categoryClientId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'categorizableId': serializer.toJson(categorizableId), + 'categorizableType': serializer.toJson(categorizableType), + 'categoryClientId': serializer.toJson(categoryClientId), + }; + } + + CategorizablesData copyWith( + {String? categorizableId, + String? categorizableType, + String? categoryClientId}) => + CategorizablesData( + categorizableId: categorizableId ?? this.categorizableId, + categorizableType: categorizableType ?? this.categorizableType, + categoryClientId: categoryClientId ?? this.categoryClientId, + ); + CategorizablesData copyWithCompanion(CategorizablesCompanion data) { + return CategorizablesData( + categorizableId: data.categorizableId.present + ? data.categorizableId.value + : this.categorizableId, + categorizableType: data.categorizableType.present + ? data.categorizableType.value + : this.categorizableType, + categoryClientId: data.categoryClientId.present + ? data.categoryClientId.value + : this.categoryClientId, + ); + } + + @override + String toString() { + return (StringBuffer('CategorizablesData(') + ..write('categorizableId: $categorizableId, ') + ..write('categorizableType: $categorizableType, ') + ..write('categoryClientId: $categoryClientId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(categorizableId, categorizableType, categoryClientId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CategorizablesData && + other.categorizableId == this.categorizableId && + other.categorizableType == this.categorizableType && + other.categoryClientId == this.categoryClientId); +} + +class CategorizablesCompanion extends UpdateCompanion { + final Value categorizableId; + final Value categorizableType; + final Value categoryClientId; + final Value rowid; + const CategorizablesCompanion({ + this.categorizableId = const Value.absent(), + this.categorizableType = const Value.absent(), + this.categoryClientId = const Value.absent(), + this.rowid = const Value.absent(), + }); + CategorizablesCompanion.insert({ + required String categorizableId, + required String categorizableType, + required String categoryClientId, + this.rowid = const Value.absent(), + }) : categorizableId = Value(categorizableId), + categorizableType = Value(categorizableType), + categoryClientId = Value(categoryClientId); + static Insertable custom({ + Expression? categorizableId, + Expression? categorizableType, + Expression? categoryClientId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (categorizableId != null) 'categorizable_id': categorizableId, + if (categorizableType != null) 'categorizable_type': categorizableType, + if (categoryClientId != null) 'category_client_id': categoryClientId, + if (rowid != null) 'rowid': rowid, + }); + } + + CategorizablesCompanion copyWith( + {Value? categorizableId, + Value? categorizableType, + Value? categoryClientId, + Value? rowid}) { + return CategorizablesCompanion( + categorizableId: categorizableId ?? this.categorizableId, + categorizableType: categorizableType ?? this.categorizableType, + categoryClientId: categoryClientId ?? this.categoryClientId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (categorizableId.present) { + map['categorizable_id'] = Variable(categorizableId.value); + } + if (categorizableType.present) { + map['categorizable_type'] = Variable(categorizableType.value); + } + if (categoryClientId.present) { + map['category_client_id'] = Variable(categoryClientId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CategorizablesCompanion(') + ..write('categorizableId: $categorizableId, ') + ..write('categorizableType: $categorizableType, ') + ..write('categoryClientId: $categoryClientId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Notifications extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Notifications(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn title = GeneratedColumn( + 'title', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn body = GeneratedColumn( + 'body', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn readAt = GeneratedColumn( + 'read_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + type, + title, + body, + data, + readAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'notifications'; + @override + Set get $primaryKey => {clientId}; + @override + NotificationsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return NotificationsData( + 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']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + title: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}title'])!, + body: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}body'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + readAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}read_at']), + ); + } + + @override + Notifications createAlias(String alias) { + return Notifications(attachedDatabase, alias); + } +} + +class NotificationsData 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 type; + final String title; + final String body; + final String data; + final DateTime? readAt; + const NotificationsData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.type, + required this.title, + required this.body, + required this.data, + this.readAt}); + @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['type'] = Variable(type); + map['title'] = Variable(title); + map['body'] = Variable(body); + map['data'] = Variable(data); + if (!nullToAbsent || readAt != null) { + map['read_at'] = Variable(readAt); + } + return map; + } + + NotificationsCompanion toCompanion(bool nullToAbsent) { + return NotificationsCompanion( + 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), + type: Value(type), + title: Value(title), + body: Value(body), + data: Value(data), + readAt: + readAt == null && nullToAbsent ? const Value.absent() : Value(readAt), + ); + } + + factory NotificationsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return NotificationsData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + type: serializer.fromJson(json['type']), + title: serializer.fromJson(json['title']), + body: serializer.fromJson(json['body']), + data: serializer.fromJson(json['data']), + readAt: serializer.fromJson(json['readAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'type': serializer.toJson(type), + 'title': serializer.toJson(title), + 'body': serializer.toJson(body), + 'data': serializer.toJson(data), + 'readAt': serializer.toJson(readAt), + }; + } + + NotificationsData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? type, + String? title, + String? body, + String? data, + Value readAt = const Value.absent()}) => + NotificationsData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + type: type ?? this.type, + title: title ?? this.title, + body: body ?? this.body, + data: data ?? this.data, + readAt: readAt.present ? readAt.value : this.readAt, + ); + NotificationsData copyWithCompanion(NotificationsCompanion data) { + return NotificationsData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + type: data.type.present ? data.type.value : this.type, + title: data.title.present ? data.title.value : this.title, + body: data.body.present ? data.body.value : this.body, + data: data.data.present ? data.data.value : this.data, + readAt: data.readAt.present ? data.readAt.value : this.readAt, + ); + } + + @override + String toString() { + return (StringBuffer('NotificationsData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('type: $type, ') + ..write('title: $title, ') + ..write('body: $body, ') + ..write('data: $data, ') + ..write('readAt: $readAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, userId, clientId, rev, createdAt, + updatedAt, deletedAt, lastSyncedAt, type, title, body, data, readAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is NotificationsData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.type == this.type && + other.title == this.title && + other.body == this.body && + other.data == this.data && + other.readAt == this.readAt); +} + +class NotificationsCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value type; + final Value title; + final Value body; + final Value data; + final Value readAt; + final Value rowid; + const NotificationsCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.type = const Value.absent(), + this.title = const Value.absent(), + this.body = const Value.absent(), + this.data = const Value.absent(), + this.readAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + NotificationsCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String type, + required String title, + required String body, + required String data, + this.readAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : type = Value(type), + title = Value(title), + body = Value(body), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? type, + Expression? title, + Expression? body, + Expression? data, + Expression? readAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (type != null) 'type': type, + if (title != null) 'title': title, + if (body != null) 'body': body, + if (data != null) 'data': data, + if (readAt != null) 'read_at': readAt, + if (rowid != null) 'rowid': rowid, + }); + } + + NotificationsCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? type, + Value? title, + Value? body, + Value? data, + Value? readAt, + Value? rowid}) { + return NotificationsCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + type: type ?? this.type, + title: title ?? this.title, + body: body ?? this.body, + data: data ?? this.data, + readAt: readAt ?? this.readAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (body.present) { + map['body'] = Variable(body.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (readAt.present) { + map['read_at'] = Variable(readAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('NotificationsCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('type: $type, ') + ..write('title: $title, ') + ..write('body: $body, ') + ..write('data: $data, ') + ..write('readAt: $readAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class MediaFiles extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MediaFiles(this.attachedDatabase, [this._alias]); + late final GeneratedColumn path = GeneratedColumn( + 'path', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fileableType = GeneratedColumn( + 'fileable_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn fileableId = GeneratedColumn( + 'fileable_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn localFileableType = + GeneratedColumn('local_fileable_type', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn localFileableId = GeneratedColumn( + 'local_fileable_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + path, + id, + type, + fileableType, + fileableId, + localFileableType, + localFileableId, + createdAt, + updatedAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'media_files'; + @override + Set get $primaryKey => {path}; + @override + MediaFilesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MediaFilesData( + path: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}path'])!, + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type']), + fileableType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}fileable_type']), + fileableId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}fileable_id']), + localFileableType: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}local_fileable_type']), + localFileableId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}local_fileable_id']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']), + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at']), + ); + } + + @override + MediaFiles createAlias(String alias) { + return MediaFiles(attachedDatabase, alias); + } +} + +class MediaFilesData extends DataClass implements Insertable { + final String path; + final int? id; + final String? type; + final String? fileableType; + final int? fileableId; + final String? localFileableType; + final String? localFileableId; + final DateTime? createdAt; + final DateTime? updatedAt; + const MediaFilesData( + {required this.path, + this.id, + this.type, + this.fileableType, + this.fileableId, + this.localFileableType, + this.localFileableId, + this.createdAt, + this.updatedAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['path'] = Variable(path); + if (!nullToAbsent || id != null) { + map['id'] = Variable(id); + } + if (!nullToAbsent || type != null) { + map['type'] = Variable(type); + } + if (!nullToAbsent || fileableType != null) { + map['fileable_type'] = Variable(fileableType); + } + if (!nullToAbsent || fileableId != null) { + map['fileable_id'] = Variable(fileableId); + } + if (!nullToAbsent || localFileableType != null) { + map['local_fileable_type'] = Variable(localFileableType); + } + if (!nullToAbsent || localFileableId != null) { + map['local_fileable_id'] = Variable(localFileableId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || updatedAt != null) { + map['updated_at'] = Variable(updatedAt); + } + return map; + } + + MediaFilesCompanion toCompanion(bool nullToAbsent) { + return MediaFilesCompanion( + path: Value(path), + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + type: type == null && nullToAbsent ? const Value.absent() : Value(type), + fileableType: fileableType == null && nullToAbsent + ? const Value.absent() + : Value(fileableType), + fileableId: fileableId == null && nullToAbsent + ? const Value.absent() + : Value(fileableId), + localFileableType: localFileableType == null && nullToAbsent + ? const Value.absent() + : Value(localFileableType), + localFileableId: localFileableId == null && nullToAbsent + ? const Value.absent() + : Value(localFileableId), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + updatedAt: updatedAt == null && nullToAbsent + ? const Value.absent() + : Value(updatedAt), + ); + } + + factory MediaFilesData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MediaFilesData( + path: serializer.fromJson(json['path']), + id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), + fileableType: serializer.fromJson(json['fileableType']), + fileableId: serializer.fromJson(json['fileableId']), + localFileableType: + serializer.fromJson(json['localFileableType']), + localFileableId: serializer.fromJson(json['localFileableId']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'path': serializer.toJson(path), + 'id': serializer.toJson(id), + 'type': serializer.toJson(type), + 'fileableType': serializer.toJson(fileableType), + 'fileableId': serializer.toJson(fileableId), + 'localFileableType': serializer.toJson(localFileableType), + 'localFileableId': serializer.toJson(localFileableId), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + MediaFilesData copyWith( + {String? path, + Value id = const Value.absent(), + Value type = const Value.absent(), + Value fileableType = const Value.absent(), + Value fileableId = const Value.absent(), + Value localFileableType = const Value.absent(), + Value localFileableId = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent()}) => + MediaFilesData( + path: path ?? this.path, + id: id.present ? id.value : this.id, + type: type.present ? type.value : this.type, + fileableType: + fileableType.present ? fileableType.value : this.fileableType, + fileableId: fileableId.present ? fileableId.value : this.fileableId, + localFileableType: localFileableType.present + ? localFileableType.value + : this.localFileableType, + localFileableId: localFileableId.present + ? localFileableId.value + : this.localFileableId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + updatedAt: updatedAt.present ? updatedAt.value : this.updatedAt, + ); + MediaFilesData copyWithCompanion(MediaFilesCompanion data) { + return MediaFilesData( + path: data.path.present ? data.path.value : this.path, + id: data.id.present ? data.id.value : this.id, + type: data.type.present ? data.type.value : this.type, + fileableType: data.fileableType.present + ? data.fileableType.value + : this.fileableType, + fileableId: + data.fileableId.present ? data.fileableId.value : this.fileableId, + localFileableType: data.localFileableType.present + ? data.localFileableType.value + : this.localFileableType, + localFileableId: data.localFileableId.present + ? data.localFileableId.value + : this.localFileableId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } + + @override + String toString() { + return (StringBuffer('MediaFilesData(') + ..write('path: $path, ') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('fileableType: $fileableType, ') + ..write('fileableId: $fileableId, ') + ..write('localFileableType: $localFileableType, ') + ..write('localFileableId: $localFileableId, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(path, id, type, fileableType, fileableId, + localFileableType, localFileableId, createdAt, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MediaFilesData && + other.path == this.path && + other.id == this.id && + other.type == this.type && + other.fileableType == this.fileableType && + other.fileableId == this.fileableId && + other.localFileableType == this.localFileableType && + other.localFileableId == this.localFileableId && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class MediaFilesCompanion extends UpdateCompanion { + final Value path; + final Value id; + final Value type; + final Value fileableType; + final Value fileableId; + final Value localFileableType; + final Value localFileableId; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const MediaFilesCompanion({ + this.path = const Value.absent(), + this.id = const Value.absent(), + this.type = const Value.absent(), + this.fileableType = const Value.absent(), + this.fileableId = const Value.absent(), + this.localFileableType = const Value.absent(), + this.localFileableId = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MediaFilesCompanion.insert({ + required String path, + this.id = const Value.absent(), + this.type = const Value.absent(), + this.fileableType = const Value.absent(), + this.fileableId = const Value.absent(), + this.localFileableType = const Value.absent(), + this.localFileableId = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : path = Value(path); + static Insertable custom({ + Expression? path, + Expression? id, + Expression? type, + Expression? fileableType, + Expression? fileableId, + Expression? localFileableType, + Expression? localFileableId, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (path != null) 'path': path, + if (id != null) 'id': id, + if (type != null) 'type': type, + if (fileableType != null) 'fileable_type': fileableType, + if (fileableId != null) 'fileable_id': fileableId, + if (localFileableType != null) 'local_fileable_type': localFileableType, + if (localFileableId != null) 'local_fileable_id': localFileableId, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MediaFilesCompanion copyWith( + {Value? path, + Value? id, + Value? type, + Value? fileableType, + Value? fileableId, + Value? localFileableType, + Value? localFileableId, + Value? createdAt, + Value? updatedAt, + Value? rowid}) { + return MediaFilesCompanion( + path: path ?? this.path, + id: id ?? this.id, + type: type ?? this.type, + fileableType: fileableType ?? this.fileableType, + fileableId: fileableId ?? this.fileableId, + localFileableType: localFileableType ?? this.localFileableType, + localFileableId: localFileableId ?? this.localFileableId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (path.present) { + map['path'] = Variable(path.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (fileableType.present) { + map['fileable_type'] = Variable(fileableType.value); + } + if (fileableId.present) { + map['fileable_id'] = Variable(fileableId.value); + } + if (localFileableType.present) { + map['local_fileable_type'] = Variable(localFileableType.value); + } + if (localFileableId.present) { + map['local_fileable_id'] = Variable(localFileableId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MediaFilesCompanion(') + ..write('path: $path, ') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('fileableType: $fileableType, ') + ..write('fileableId: $fileableId, ') + ..write('localFileableType: $localFileableType, ') + ..write('localFileableId: $localFileableId, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Transfers extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Transfers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn amount = GeneratedColumn( + 'amount', aliasedName, false, + type: DriftSqlType.double, requiredDuringInsert: true); + late final GeneratedColumn fromWalletId = GeneratedColumn( + 'from_wallet_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn toWalletId = GeneratedColumn( + 'to_wallet_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn fromWalletClientId = + GeneratedColumn('from_wallet_client_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES wallets (client_id)')); + late final GeneratedColumn toWalletClientId = GeneratedColumn( + 'to_wallet_client_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES wallets (client_id)')); + late final GeneratedColumn exchangeRate = GeneratedColumn( + 'exchange_rate', aliasedName, true, + type: DriftSqlType.double, requiredDuringInsert: false); + late final GeneratedColumn datetime = GeneratedColumn( + 'datetime', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn expenseTransactionClientId = + GeneratedColumn( + 'expense_transaction_client_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES transactions (client_id)')); + late final GeneratedColumn incomeTransactionClientId = + GeneratedColumn('income_transaction_client_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES transactions (client_id)')); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + amount, + fromWalletId, + toWalletId, + fromWalletClientId, + toWalletClientId, + exchangeRate, + datetime, + expenseTransactionClientId, + incomeTransactionClientId + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'transfers'; + @override + Set get $primaryKey => {clientId}; + @override + TransfersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TransfersData( + 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']), + amount: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}amount'])!, + fromWalletId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}from_wallet_id']), + toWalletId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}to_wallet_id']), + fromWalletClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}from_wallet_client_id']), + toWalletClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}to_wallet_client_id']), + exchangeRate: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}exchange_rate']), + datetime: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}datetime'])!, + expenseTransactionClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}expense_transaction_client_id']), + incomeTransactionClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}income_transaction_client_id']), + ); + } + + @override + Transfers createAlias(String alias) { + return Transfers(attachedDatabase, alias); + } +} + +class TransfersData 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 double amount; + final int? fromWalletId; + final int? toWalletId; + final String? fromWalletClientId; + final String? toWalletClientId; + final double? exchangeRate; + final DateTime datetime; + final String? expenseTransactionClientId; + final String? incomeTransactionClientId; + const TransfersData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.amount, + this.fromWalletId, + this.toWalletId, + this.fromWalletClientId, + this.toWalletClientId, + this.exchangeRate, + required this.datetime, + this.expenseTransactionClientId, + this.incomeTransactionClientId}); + @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['amount'] = Variable(amount); + if (!nullToAbsent || fromWalletId != null) { + map['from_wallet_id'] = Variable(fromWalletId); + } + if (!nullToAbsent || toWalletId != null) { + map['to_wallet_id'] = Variable(toWalletId); + } + if (!nullToAbsent || fromWalletClientId != null) { + map['from_wallet_client_id'] = Variable(fromWalletClientId); + } + if (!nullToAbsent || toWalletClientId != null) { + map['to_wallet_client_id'] = Variable(toWalletClientId); + } + if (!nullToAbsent || exchangeRate != null) { + map['exchange_rate'] = Variable(exchangeRate); + } + map['datetime'] = Variable(datetime); + if (!nullToAbsent || expenseTransactionClientId != null) { + map['expense_transaction_client_id'] = + Variable(expenseTransactionClientId); + } + if (!nullToAbsent || incomeTransactionClientId != null) { + map['income_transaction_client_id'] = + Variable(incomeTransactionClientId); + } + return map; + } + + TransfersCompanion toCompanion(bool nullToAbsent) { + return TransfersCompanion( + 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), + amount: Value(amount), + fromWalletId: fromWalletId == null && nullToAbsent + ? const Value.absent() + : Value(fromWalletId), + toWalletId: toWalletId == null && nullToAbsent + ? const Value.absent() + : Value(toWalletId), + fromWalletClientId: fromWalletClientId == null && nullToAbsent + ? const Value.absent() + : Value(fromWalletClientId), + toWalletClientId: toWalletClientId == null && nullToAbsent + ? const Value.absent() + : Value(toWalletClientId), + exchangeRate: exchangeRate == null && nullToAbsent + ? const Value.absent() + : Value(exchangeRate), + datetime: Value(datetime), + expenseTransactionClientId: + expenseTransactionClientId == null && nullToAbsent + ? const Value.absent() + : Value(expenseTransactionClientId), + incomeTransactionClientId: + incomeTransactionClientId == null && nullToAbsent + ? const Value.absent() + : Value(incomeTransactionClientId), + ); + } + + factory TransfersData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TransfersData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + amount: serializer.fromJson(json['amount']), + fromWalletId: serializer.fromJson(json['fromWalletId']), + toWalletId: serializer.fromJson(json['toWalletId']), + fromWalletClientId: + serializer.fromJson(json['fromWalletClientId']), + toWalletClientId: serializer.fromJson(json['toWalletClientId']), + exchangeRate: serializer.fromJson(json['exchangeRate']), + datetime: serializer.fromJson(json['datetime']), + expenseTransactionClientId: + serializer.fromJson(json['expenseTransactionClientId']), + incomeTransactionClientId: + serializer.fromJson(json['incomeTransactionClientId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'amount': serializer.toJson(amount), + 'fromWalletId': serializer.toJson(fromWalletId), + 'toWalletId': serializer.toJson(toWalletId), + 'fromWalletClientId': serializer.toJson(fromWalletClientId), + 'toWalletClientId': serializer.toJson(toWalletClientId), + 'exchangeRate': serializer.toJson(exchangeRate), + 'datetime': serializer.toJson(datetime), + 'expenseTransactionClientId': + serializer.toJson(expenseTransactionClientId), + 'incomeTransactionClientId': + serializer.toJson(incomeTransactionClientId), + }; + } + + TransfersData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + double? amount, + Value fromWalletId = const Value.absent(), + Value toWalletId = const Value.absent(), + Value fromWalletClientId = const Value.absent(), + Value toWalletClientId = const Value.absent(), + Value exchangeRate = const Value.absent(), + DateTime? datetime, + Value expenseTransactionClientId = const Value.absent(), + Value incomeTransactionClientId = const Value.absent()}) => + TransfersData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + amount: amount ?? this.amount, + fromWalletId: + fromWalletId.present ? fromWalletId.value : this.fromWalletId, + toWalletId: toWalletId.present ? toWalletId.value : this.toWalletId, + fromWalletClientId: fromWalletClientId.present + ? fromWalletClientId.value + : this.fromWalletClientId, + toWalletClientId: toWalletClientId.present + ? toWalletClientId.value + : this.toWalletClientId, + exchangeRate: + exchangeRate.present ? exchangeRate.value : this.exchangeRate, + datetime: datetime ?? this.datetime, + expenseTransactionClientId: expenseTransactionClientId.present + ? expenseTransactionClientId.value + : this.expenseTransactionClientId, + incomeTransactionClientId: incomeTransactionClientId.present + ? incomeTransactionClientId.value + : this.incomeTransactionClientId, + ); + TransfersData copyWithCompanion(TransfersCompanion data) { + return TransfersData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + amount: data.amount.present ? data.amount.value : this.amount, + fromWalletId: data.fromWalletId.present + ? data.fromWalletId.value + : this.fromWalletId, + toWalletId: + data.toWalletId.present ? data.toWalletId.value : this.toWalletId, + fromWalletClientId: data.fromWalletClientId.present + ? data.fromWalletClientId.value + : this.fromWalletClientId, + toWalletClientId: data.toWalletClientId.present + ? data.toWalletClientId.value + : this.toWalletClientId, + exchangeRate: data.exchangeRate.present + ? data.exchangeRate.value + : this.exchangeRate, + datetime: data.datetime.present ? data.datetime.value : this.datetime, + expenseTransactionClientId: data.expenseTransactionClientId.present + ? data.expenseTransactionClientId.value + : this.expenseTransactionClientId, + incomeTransactionClientId: data.incomeTransactionClientId.present + ? data.incomeTransactionClientId.value + : this.incomeTransactionClientId, + ); + } + + @override + String toString() { + return (StringBuffer('TransfersData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('amount: $amount, ') + ..write('fromWalletId: $fromWalletId, ') + ..write('toWalletId: $toWalletId, ') + ..write('fromWalletClientId: $fromWalletClientId, ') + ..write('toWalletClientId: $toWalletClientId, ') + ..write('exchangeRate: $exchangeRate, ') + ..write('datetime: $datetime, ') + ..write('expenseTransactionClientId: $expenseTransactionClientId, ') + ..write('incomeTransactionClientId: $incomeTransactionClientId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + amount, + fromWalletId, + toWalletId, + fromWalletClientId, + toWalletClientId, + exchangeRate, + datetime, + expenseTransactionClientId, + incomeTransactionClientId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TransfersData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.amount == this.amount && + other.fromWalletId == this.fromWalletId && + other.toWalletId == this.toWalletId && + other.fromWalletClientId == this.fromWalletClientId && + other.toWalletClientId == this.toWalletClientId && + other.exchangeRate == this.exchangeRate && + other.datetime == this.datetime && + other.expenseTransactionClientId == this.expenseTransactionClientId && + other.incomeTransactionClientId == this.incomeTransactionClientId); +} + +class TransfersCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value amount; + final Value fromWalletId; + final Value toWalletId; + final Value fromWalletClientId; + final Value toWalletClientId; + final Value exchangeRate; + final Value datetime; + final Value expenseTransactionClientId; + final Value incomeTransactionClientId; + final Value rowid; + const TransfersCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.amount = const Value.absent(), + this.fromWalletId = const Value.absent(), + this.toWalletId = const Value.absent(), + this.fromWalletClientId = const Value.absent(), + this.toWalletClientId = const Value.absent(), + this.exchangeRate = const Value.absent(), + this.datetime = const Value.absent(), + this.expenseTransactionClientId = const Value.absent(), + this.incomeTransactionClientId = const Value.absent(), + this.rowid = const Value.absent(), + }); + TransfersCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required double amount, + this.fromWalletId = const Value.absent(), + this.toWalletId = const Value.absent(), + this.fromWalletClientId = const Value.absent(), + this.toWalletClientId = const Value.absent(), + this.exchangeRate = const Value.absent(), + required DateTime datetime, + this.expenseTransactionClientId = const Value.absent(), + this.incomeTransactionClientId = const Value.absent(), + this.rowid = const Value.absent(), + }) : amount = Value(amount), + datetime = Value(datetime); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? amount, + Expression? fromWalletId, + Expression? toWalletId, + Expression? fromWalletClientId, + Expression? toWalletClientId, + Expression? exchangeRate, + Expression? datetime, + Expression? expenseTransactionClientId, + Expression? incomeTransactionClientId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (amount != null) 'amount': amount, + if (fromWalletId != null) 'from_wallet_id': fromWalletId, + if (toWalletId != null) 'to_wallet_id': toWalletId, + if (fromWalletClientId != null) + 'from_wallet_client_id': fromWalletClientId, + if (toWalletClientId != null) 'to_wallet_client_id': toWalletClientId, + if (exchangeRate != null) 'exchange_rate': exchangeRate, + if (datetime != null) 'datetime': datetime, + if (expenseTransactionClientId != null) + 'expense_transaction_client_id': expenseTransactionClientId, + if (incomeTransactionClientId != null) + 'income_transaction_client_id': incomeTransactionClientId, + if (rowid != null) 'rowid': rowid, + }); + } + + TransfersCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? amount, + Value? fromWalletId, + Value? toWalletId, + Value? fromWalletClientId, + Value? toWalletClientId, + Value? exchangeRate, + Value? datetime, + Value? expenseTransactionClientId, + Value? incomeTransactionClientId, + Value? rowid}) { + return TransfersCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + amount: amount ?? this.amount, + fromWalletId: fromWalletId ?? this.fromWalletId, + toWalletId: toWalletId ?? this.toWalletId, + fromWalletClientId: fromWalletClientId ?? this.fromWalletClientId, + toWalletClientId: toWalletClientId ?? this.toWalletClientId, + exchangeRate: exchangeRate ?? this.exchangeRate, + datetime: datetime ?? this.datetime, + expenseTransactionClientId: + expenseTransactionClientId ?? this.expenseTransactionClientId, + incomeTransactionClientId: + incomeTransactionClientId ?? this.incomeTransactionClientId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (amount.present) { + map['amount'] = Variable(amount.value); + } + if (fromWalletId.present) { + map['from_wallet_id'] = Variable(fromWalletId.value); + } + if (toWalletId.present) { + map['to_wallet_id'] = Variable(toWalletId.value); + } + if (fromWalletClientId.present) { + map['from_wallet_client_id'] = Variable(fromWalletClientId.value); + } + if (toWalletClientId.present) { + map['to_wallet_client_id'] = Variable(toWalletClientId.value); + } + if (exchangeRate.present) { + map['exchange_rate'] = Variable(exchangeRate.value); + } + if (datetime.present) { + map['datetime'] = Variable(datetime.value); + } + if (expenseTransactionClientId.present) { + map['expense_transaction_client_id'] = + Variable(expenseTransactionClientId.value); + } + if (incomeTransactionClientId.present) { + map['income_transaction_client_id'] = + Variable(incomeTransactionClientId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TransfersCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('amount: $amount, ') + ..write('fromWalletId: $fromWalletId, ') + ..write('toWalletId: $toWalletId, ') + ..write('fromWalletClientId: $fromWalletClientId, ') + ..write('toWalletClientId: $toWalletClientId, ') + ..write('exchangeRate: $exchangeRate, ') + ..write('datetime: $datetime, ') + ..write('expenseTransactionClientId: $expenseTransactionClientId, ') + ..write('incomeTransactionClientId: $incomeTransactionClientId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Budgets extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Budgets(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn slug = GeneratedColumn( + 'slug', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn amount = GeneratedColumn( + 'amount', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn currency = GeneratedColumn( + 'currency', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn periodType = GeneratedColumn( + 'period_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn startDate = GeneratedColumn( + 'start_date', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn endDate = GeneratedColumn( + 'end_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + 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 CustomExpression('0')); + late final GeneratedColumn thresholdPercent = GeneratedColumn( + 'threshold_percent', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('80')); + 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 CustomExpression('0')); + 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 CustomExpression('1')); + late final GeneratedColumn ownerType = GeneratedColumn( + 'owner_type', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'user\'')); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn progress = GeneratedColumn( + 'progress', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @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 + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'budgets'; + @override + Set get $primaryKey => {clientId}; + @override + BudgetsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BudgetsData( + 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: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}amount'])!, + currency: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}currency'])!, + periodType: 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: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}progress']), + ); + } + + @override + Budgets createAlias(String alias) { + return Budgets(attachedDatabase, alias); + } +} + +class BudgetsData 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 String amount; + final String currency; + final String 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 String? progress; + const BudgetsData( + {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(amount); + map['currency'] = Variable(currency); + map['period_type'] = Variable(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(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 BudgetsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BudgetsData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + name: serializer.fromJson(json['name']), + slug: serializer.fromJson(json['slug']), + description: serializer.fromJson(json['description']), + amount: serializer.fromJson(json['amount']), + currency: serializer.fromJson(json['currency']), + periodType: serializer.fromJson(json['periodType']), + startDate: serializer.fromJson(json['startDate']), + endDate: serializer.fromJson(json['endDate']), + rolloverEnabled: serializer.fromJson(json['rolloverEnabled']), + thresholdPercent: serializer.fromJson(json['thresholdPercent']), + forecastAlertsEnabled: + serializer.fromJson(json['forecastAlertsEnabled']), + isActive: serializer.fromJson(json['isActive']), + ownerType: serializer.fromJson(json['ownerType']), + ownerId: serializer.fromJson(json['ownerId']), + progress: serializer.fromJson(json['progress']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'name': serializer.toJson(name), + 'slug': serializer.toJson(slug), + 'description': serializer.toJson(description), + 'amount': serializer.toJson(amount), + 'currency': serializer.toJson(currency), + 'periodType': serializer.toJson(periodType), + 'startDate': serializer.toJson(startDate), + 'endDate': serializer.toJson(endDate), + 'rolloverEnabled': serializer.toJson(rolloverEnabled), + 'thresholdPercent': serializer.toJson(thresholdPercent), + 'forecastAlertsEnabled': serializer.toJson(forecastAlertsEnabled), + 'isActive': serializer.toJson(isActive), + 'ownerType': serializer.toJson(ownerType), + 'ownerId': serializer.toJson(ownerId), + 'progress': serializer.toJson(progress), + }; + } + + BudgetsData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? name, + String? slug, + Value description = const Value.absent(), + String? amount, + String? currency, + String? periodType, + DateTime? startDate, + Value endDate = const Value.absent(), + bool? rolloverEnabled, + int? thresholdPercent, + bool? forecastAlertsEnabled, + bool? isActive, + String? ownerType, + Value ownerId = const Value.absent(), + Value progress = const Value.absent()}) => + BudgetsData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + name: name ?? this.name, + slug: slug ?? this.slug, + description: description.present ? description.value : this.description, + amount: amount ?? this.amount, + currency: currency ?? this.currency, + periodType: periodType ?? this.periodType, + startDate: startDate ?? this.startDate, + endDate: endDate.present ? endDate.value : this.endDate, + rolloverEnabled: rolloverEnabled ?? this.rolloverEnabled, + thresholdPercent: thresholdPercent ?? this.thresholdPercent, + forecastAlertsEnabled: + forecastAlertsEnabled ?? this.forecastAlertsEnabled, + isActive: isActive ?? this.isActive, + ownerType: ownerType ?? this.ownerType, + ownerId: ownerId.present ? ownerId.value : this.ownerId, + progress: progress.present ? progress.value : this.progress, + ); + BudgetsData copyWithCompanion(BudgetsCompanion data) { + return BudgetsData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + name: data.name.present ? data.name.value : this.name, + slug: data.slug.present ? data.slug.value : this.slug, + description: + data.description.present ? data.description.value : this.description, + amount: data.amount.present ? data.amount.value : this.amount, + currency: data.currency.present ? data.currency.value : this.currency, + periodType: + data.periodType.present ? data.periodType.value : this.periodType, + startDate: data.startDate.present ? data.startDate.value : this.startDate, + endDate: data.endDate.present ? data.endDate.value : this.endDate, + rolloverEnabled: data.rolloverEnabled.present + ? data.rolloverEnabled.value + : this.rolloverEnabled, + thresholdPercent: data.thresholdPercent.present + ? data.thresholdPercent.value + : this.thresholdPercent, + forecastAlertsEnabled: data.forecastAlertsEnabled.present + ? data.forecastAlertsEnabled.value + : this.forecastAlertsEnabled, + isActive: data.isActive.present ? data.isActive.value : this.isActive, + ownerType: data.ownerType.present ? data.ownerType.value : this.ownerType, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + progress: data.progress.present ? data.progress.value : this.progress, + ); + } + + @override + String toString() { + return (StringBuffer('BudgetsData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('slug: $slug, ') + ..write('description: $description, ') + ..write('amount: $amount, ') + ..write('currency: $currency, ') + ..write('periodType: $periodType, ') + ..write('startDate: $startDate, ') + ..write('endDate: $endDate, ') + ..write('rolloverEnabled: $rolloverEnabled, ') + ..write('thresholdPercent: $thresholdPercent, ') + ..write('forecastAlertsEnabled: $forecastAlertsEnabled, ') + ..write('isActive: $isActive, ') + ..write('ownerType: $ownerType, ') + ..write('ownerId: $ownerId, ') + ..write('progress: $progress') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + name, + slug, + description, + amount, + currency, + periodType, + startDate, + endDate, + rolloverEnabled, + thresholdPercent, + forecastAlertsEnabled, + isActive, + ownerType, + ownerId, + progress + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BudgetsData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.name == this.name && + other.slug == this.slug && + other.description == this.description && + other.amount == this.amount && + other.currency == this.currency && + other.periodType == this.periodType && + other.startDate == this.startDate && + other.endDate == this.endDate && + other.rolloverEnabled == this.rolloverEnabled && + other.thresholdPercent == this.thresholdPercent && + other.forecastAlertsEnabled == this.forecastAlertsEnabled && + other.isActive == this.isActive && + other.ownerType == this.ownerType && + other.ownerId == this.ownerId && + other.progress == this.progress); +} + +class BudgetsCompanion extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value name; + final Value slug; + final Value description; + final Value amount; + final Value currency; + final Value periodType; + final Value startDate; + final Value endDate; + final Value rolloverEnabled; + final Value thresholdPercent; + final Value forecastAlertsEnabled; + final Value isActive; + final Value ownerType; + final Value ownerId; + final Value progress; + final Value rowid; + const BudgetsCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.name = const Value.absent(), + this.slug = const Value.absent(), + this.description = const Value.absent(), + this.amount = const Value.absent(), + this.currency = const Value.absent(), + this.periodType = const Value.absent(), + this.startDate = const Value.absent(), + this.endDate = const Value.absent(), + this.rolloverEnabled = const Value.absent(), + this.thresholdPercent = const Value.absent(), + this.forecastAlertsEnabled = const Value.absent(), + this.isActive = const Value.absent(), + this.ownerType = const Value.absent(), + this.ownerId = const Value.absent(), + this.progress = const Value.absent(), + this.rowid = const Value.absent(), + }); + BudgetsCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String name, + required String slug, + this.description = const Value.absent(), + required String amount, + required String currency, + required String periodType, + required DateTime startDate, + this.endDate = const Value.absent(), + this.rolloverEnabled = const Value.absent(), + this.thresholdPercent = const Value.absent(), + this.forecastAlertsEnabled = const Value.absent(), + this.isActive = const Value.absent(), + this.ownerType = const Value.absent(), + this.ownerId = const Value.absent(), + this.progress = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name), + slug = Value(slug), + amount = Value(amount), + currency = Value(currency), + periodType = Value(periodType), + startDate = Value(startDate); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? name, + Expression? slug, + Expression? description, + Expression? amount, + Expression? currency, + Expression? periodType, + Expression? startDate, + Expression? endDate, + Expression? rolloverEnabled, + Expression? thresholdPercent, + Expression? forecastAlertsEnabled, + Expression? isActive, + Expression? ownerType, + Expression? ownerId, + Expression? progress, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (name != null) 'name': name, + if (slug != null) 'slug': slug, + if (description != null) 'description': description, + if (amount != null) 'amount': amount, + if (currency != null) 'currency': currency, + if (periodType != null) 'period_type': periodType, + if (startDate != null) 'start_date': startDate, + if (endDate != null) 'end_date': endDate, + if (rolloverEnabled != null) 'rollover_enabled': rolloverEnabled, + if (thresholdPercent != null) 'threshold_percent': thresholdPercent, + if (forecastAlertsEnabled != null) + 'forecast_alerts_enabled': forecastAlertsEnabled, + if (isActive != null) 'is_active': isActive, + if (ownerType != null) 'owner_type': ownerType, + if (ownerId != null) 'owner_id': ownerId, + if (progress != null) 'progress': progress, + if (rowid != null) 'rowid': rowid, + }); + } + + BudgetsCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? name, + Value? slug, + Value? description, + Value? amount, + Value? currency, + Value? periodType, + Value? startDate, + Value? endDate, + Value? rolloverEnabled, + Value? thresholdPercent, + Value? forecastAlertsEnabled, + Value? isActive, + Value? ownerType, + Value? ownerId, + Value? progress, + Value? rowid}) { + return BudgetsCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + name: name ?? this.name, + slug: slug ?? this.slug, + description: description ?? this.description, + amount: amount ?? this.amount, + currency: currency ?? this.currency, + periodType: periodType ?? this.periodType, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + rolloverEnabled: rolloverEnabled ?? this.rolloverEnabled, + thresholdPercent: thresholdPercent ?? this.thresholdPercent, + forecastAlertsEnabled: + forecastAlertsEnabled ?? this.forecastAlertsEnabled, + isActive: isActive ?? this.isActive, + ownerType: ownerType ?? this.ownerType, + ownerId: ownerId ?? this.ownerId, + progress: progress ?? this.progress, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (slug.present) { + map['slug'] = Variable(slug.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (amount.present) { + map['amount'] = Variable(amount.value); + } + if (currency.present) { + map['currency'] = Variable(currency.value); + } + if (periodType.present) { + map['period_type'] = Variable(periodType.value); + } + if (startDate.present) { + map['start_date'] = Variable(startDate.value); + } + if (endDate.present) { + map['end_date'] = Variable(endDate.value); + } + if (rolloverEnabled.present) { + map['rollover_enabled'] = Variable(rolloverEnabled.value); + } + if (thresholdPercent.present) { + map['threshold_percent'] = Variable(thresholdPercent.value); + } + if (forecastAlertsEnabled.present) { + map['forecast_alerts_enabled'] = + Variable(forecastAlertsEnabled.value); + } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } + if (ownerType.present) { + map['owner_type'] = Variable(ownerType.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (progress.present) { + map['progress'] = Variable(progress.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BudgetsCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('name: $name, ') + ..write('slug: $slug, ') + ..write('description: $description, ') + ..write('amount: $amount, ') + ..write('currency: $currency, ') + ..write('periodType: $periodType, ') + ..write('startDate: $startDate, ') + ..write('endDate: $endDate, ') + ..write('rolloverEnabled: $rolloverEnabled, ') + ..write('thresholdPercent: $thresholdPercent, ') + ..write('forecastAlertsEnabled: $forecastAlertsEnabled, ') + ..write('isActive: $isActive, ') + ..write('ownerType: $ownerType, ') + ..write('ownerId: $ownerId, ') + ..write('progress: $progress, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class BudgetTargets extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BudgetTargets(this.attachedDatabase, [this._alias]); + late final GeneratedColumn budgetClientId = GeneratedColumn( + 'budget_client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES budgets (client_id)')); + late final GeneratedColumn targetType = GeneratedColumn( + 'target_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn targetClientId = GeneratedColumn( + 'target_client_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => + [budgetClientId, targetType, targetClientId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'budget_targets'; + @override + Set get $primaryKey => + {budgetClientId, targetType, targetClientId}; + @override + BudgetTargetsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BudgetTargetsData( + budgetClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}budget_client_id'])!, + targetType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}target_type'])!, + targetClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}target_client_id'])!, + ); + } + + @override + BudgetTargets createAlias(String alias) { + return BudgetTargets(attachedDatabase, alias); + } +} + +class BudgetTargetsData extends DataClass + implements Insertable { + final String budgetClientId; + final String targetType; + final String targetClientId; + const BudgetTargetsData( + {required this.budgetClientId, + required this.targetType, + required this.targetClientId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['budget_client_id'] = Variable(budgetClientId); + map['target_type'] = Variable(targetType); + map['target_client_id'] = Variable(targetClientId); + return map; + } + + BudgetTargetsCompanion toCompanion(bool nullToAbsent) { + return BudgetTargetsCompanion( + budgetClientId: Value(budgetClientId), + targetType: Value(targetType), + targetClientId: Value(targetClientId), + ); + } + + factory BudgetTargetsData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BudgetTargetsData( + budgetClientId: serializer.fromJson(json['budgetClientId']), + targetType: serializer.fromJson(json['targetType']), + targetClientId: serializer.fromJson(json['targetClientId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'budgetClientId': serializer.toJson(budgetClientId), + 'targetType': serializer.toJson(targetType), + 'targetClientId': serializer.toJson(targetClientId), + }; + } + + BudgetTargetsData copyWith( + {String? budgetClientId, + String? targetType, + String? targetClientId}) => + BudgetTargetsData( + budgetClientId: budgetClientId ?? this.budgetClientId, + targetType: targetType ?? this.targetType, + targetClientId: targetClientId ?? this.targetClientId, + ); + BudgetTargetsData copyWithCompanion(BudgetTargetsCompanion data) { + return BudgetTargetsData( + budgetClientId: data.budgetClientId.present + ? data.budgetClientId.value + : this.budgetClientId, + targetType: + data.targetType.present ? data.targetType.value : this.targetType, + targetClientId: data.targetClientId.present + ? data.targetClientId.value + : this.targetClientId, + ); + } + + @override + String toString() { + return (StringBuffer('BudgetTargetsData(') + ..write('budgetClientId: $budgetClientId, ') + ..write('targetType: $targetType, ') + ..write('targetClientId: $targetClientId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(budgetClientId, targetType, targetClientId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BudgetTargetsData && + other.budgetClientId == this.budgetClientId && + other.targetType == this.targetType && + other.targetClientId == this.targetClientId); +} + +class BudgetTargetsCompanion extends UpdateCompanion { + final Value budgetClientId; + final Value targetType; + final Value targetClientId; + final Value rowid; + const BudgetTargetsCompanion({ + this.budgetClientId = const Value.absent(), + this.targetType = const Value.absent(), + this.targetClientId = const Value.absent(), + this.rowid = const Value.absent(), + }); + BudgetTargetsCompanion.insert({ + required String budgetClientId, + required String targetType, + required String targetClientId, + this.rowid = const Value.absent(), + }) : budgetClientId = Value(budgetClientId), + targetType = Value(targetType), + targetClientId = Value(targetClientId); + static Insertable custom({ + Expression? budgetClientId, + Expression? targetType, + Expression? targetClientId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (budgetClientId != null) 'budget_client_id': budgetClientId, + if (targetType != null) 'target_type': targetType, + if (targetClientId != null) 'target_client_id': targetClientId, + if (rowid != null) 'rowid': rowid, + }); + } + + BudgetTargetsCompanion copyWith( + {Value? budgetClientId, + Value? targetType, + Value? targetClientId, + Value? rowid}) { + return BudgetTargetsCompanion( + budgetClientId: budgetClientId ?? this.budgetClientId, + targetType: targetType ?? this.targetType, + targetClientId: targetClientId ?? this.targetClientId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (budgetClientId.present) { + map['budget_client_id'] = Variable(budgetClientId.value); + } + if (targetType.present) { + map['target_type'] = Variable(targetType.value); + } + if (targetClientId.present) { + map['target_client_id'] = Variable(targetClientId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BudgetTargetsCompanion(') + ..write('budgetClientId: $budgetClientId, ') + ..write('targetType: $targetType, ') + ..write('targetClientId: $targetClientId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class BudgetPeriodStates extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BudgetPeriodStates(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE')); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn clientId = GeneratedColumn( + 'client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\'')); + late final GeneratedColumn rev = GeneratedColumn( + 'rev', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'1\'')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP')); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn lastSyncedAt = GeneratedColumn( + 'last_synced_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn budgetClientId = GeneratedColumn( + 'budget_client_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES budgets (client_id)')); + late final GeneratedColumn periodStart = GeneratedColumn( + 'period_start', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn periodEnd = GeneratedColumn( + 'period_end', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn netSpent = GeneratedColumn( + 'net_spent', aliasedName, false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0.0')); + late final GeneratedColumn rolloverIn = GeneratedColumn( + 'rollover_in', aliasedName, false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0.0')); + late final GeneratedColumn rolloverOut = GeneratedColumn( + 'rollover_out', aliasedName, false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0.0')); + late final GeneratedColumn closedAt = GeneratedColumn( + 'closed_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + budgetClientId, + periodStart, + periodEnd, + netSpent, + rolloverIn, + rolloverOut, + closedAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'budget_period_states'; + @override + Set get $primaryKey => {clientId}; + @override + BudgetPeriodStatesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BudgetPeriodStatesData( + 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']), + budgetClientId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}budget_client_id'])!, + periodStart: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}period_start'])!, + periodEnd: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}period_end'])!, + netSpent: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}net_spent'])!, + rolloverIn: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}rollover_in'])!, + rolloverOut: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}rollover_out'])!, + closedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}closed_at']), + ); + } + + @override + BudgetPeriodStates createAlias(String alias) { + return BudgetPeriodStates(attachedDatabase, alias); + } +} + +class BudgetPeriodStatesData 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 budgetClientId; + final DateTime periodStart; + final DateTime periodEnd; + final double netSpent; + final double rolloverIn; + final double rolloverOut; + final DateTime? closedAt; + const BudgetPeriodStatesData( + {this.id, + this.userId, + required this.clientId, + this.rev, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + this.lastSyncedAt, + required this.budgetClientId, + required this.periodStart, + required this.periodEnd, + required this.netSpent, + required this.rolloverIn, + required this.rolloverOut, + this.closedAt}); + @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['budget_client_id'] = Variable(budgetClientId); + map['period_start'] = Variable(periodStart); + map['period_end'] = Variable(periodEnd); + map['net_spent'] = Variable(netSpent); + map['rollover_in'] = Variable(rolloverIn); + map['rollover_out'] = Variable(rolloverOut); + if (!nullToAbsent || closedAt != null) { + map['closed_at'] = Variable(closedAt); + } + return map; + } + + BudgetPeriodStatesCompanion toCompanion(bool nullToAbsent) { + return BudgetPeriodStatesCompanion( + 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), + budgetClientId: Value(budgetClientId), + periodStart: Value(periodStart), + periodEnd: Value(periodEnd), + netSpent: Value(netSpent), + rolloverIn: Value(rolloverIn), + rolloverOut: Value(rolloverOut), + closedAt: closedAt == null && nullToAbsent + ? const Value.absent() + : Value(closedAt), + ); + } + + factory BudgetPeriodStatesData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BudgetPeriodStatesData( + id: serializer.fromJson(json['id']), + userId: serializer.fromJson(json['userId']), + clientId: serializer.fromJson(json['clientId']), + rev: serializer.fromJson(json['rev']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + lastSyncedAt: serializer.fromJson(json['lastSyncedAt']), + budgetClientId: serializer.fromJson(json['budgetClientId']), + periodStart: serializer.fromJson(json['periodStart']), + periodEnd: serializer.fromJson(json['periodEnd']), + netSpent: serializer.fromJson(json['netSpent']), + rolloverIn: serializer.fromJson(json['rolloverIn']), + rolloverOut: serializer.fromJson(json['rolloverOut']), + closedAt: serializer.fromJson(json['closedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'userId': serializer.toJson(userId), + 'clientId': serializer.toJson(clientId), + 'rev': serializer.toJson(rev), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'lastSyncedAt': serializer.toJson(lastSyncedAt), + 'budgetClientId': serializer.toJson(budgetClientId), + 'periodStart': serializer.toJson(periodStart), + 'periodEnd': serializer.toJson(periodEnd), + 'netSpent': serializer.toJson(netSpent), + 'rolloverIn': serializer.toJson(rolloverIn), + 'rolloverOut': serializer.toJson(rolloverOut), + 'closedAt': serializer.toJson(closedAt), + }; + } + + BudgetPeriodStatesData copyWith( + {Value id = const Value.absent(), + Value userId = const Value.absent(), + String? clientId, + Value rev = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + Value lastSyncedAt = const Value.absent(), + String? budgetClientId, + DateTime? periodStart, + DateTime? periodEnd, + double? netSpent, + double? rolloverIn, + double? rolloverOut, + Value closedAt = const Value.absent()}) => + BudgetPeriodStatesData( + id: id.present ? id.value : this.id, + userId: userId.present ? userId.value : this.userId, + clientId: clientId ?? this.clientId, + rev: rev.present ? rev.value : this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + lastSyncedAt: + lastSyncedAt.present ? lastSyncedAt.value : this.lastSyncedAt, + budgetClientId: budgetClientId ?? this.budgetClientId, + periodStart: periodStart ?? this.periodStart, + periodEnd: periodEnd ?? this.periodEnd, + netSpent: netSpent ?? this.netSpent, + rolloverIn: rolloverIn ?? this.rolloverIn, + rolloverOut: rolloverOut ?? this.rolloverOut, + closedAt: closedAt.present ? closedAt.value : this.closedAt, + ); + BudgetPeriodStatesData copyWithCompanion(BudgetPeriodStatesCompanion data) { + return BudgetPeriodStatesData( + id: data.id.present ? data.id.value : this.id, + userId: data.userId.present ? data.userId.value : this.userId, + clientId: data.clientId.present ? data.clientId.value : this.clientId, + rev: data.rev.present ? data.rev.value : this.rev, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + lastSyncedAt: data.lastSyncedAt.present + ? data.lastSyncedAt.value + : this.lastSyncedAt, + budgetClientId: data.budgetClientId.present + ? data.budgetClientId.value + : this.budgetClientId, + periodStart: + data.periodStart.present ? data.periodStart.value : this.periodStart, + periodEnd: data.periodEnd.present ? data.periodEnd.value : this.periodEnd, + netSpent: data.netSpent.present ? data.netSpent.value : this.netSpent, + rolloverIn: + data.rolloverIn.present ? data.rolloverIn.value : this.rolloverIn, + rolloverOut: + data.rolloverOut.present ? data.rolloverOut.value : this.rolloverOut, + closedAt: data.closedAt.present ? data.closedAt.value : this.closedAt, + ); + } + + @override + String toString() { + return (StringBuffer('BudgetPeriodStatesData(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('budgetClientId: $budgetClientId, ') + ..write('periodStart: $periodStart, ') + ..write('periodEnd: $periodEnd, ') + ..write('netSpent: $netSpent, ') + ..write('rolloverIn: $rolloverIn, ') + ..write('rolloverOut: $rolloverOut, ') + ..write('closedAt: $closedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + userId, + clientId, + rev, + createdAt, + updatedAt, + deletedAt, + lastSyncedAt, + budgetClientId, + periodStart, + periodEnd, + netSpent, + rolloverIn, + rolloverOut, + closedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BudgetPeriodStatesData && + other.id == this.id && + other.userId == this.userId && + other.clientId == this.clientId && + other.rev == this.rev && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.lastSyncedAt == this.lastSyncedAt && + other.budgetClientId == this.budgetClientId && + other.periodStart == this.periodStart && + other.periodEnd == this.periodEnd && + other.netSpent == this.netSpent && + other.rolloverIn == this.rolloverIn && + other.rolloverOut == this.rolloverOut && + other.closedAt == this.closedAt); +} + +class BudgetPeriodStatesCompanion + extends UpdateCompanion { + final Value id; + final Value userId; + final Value clientId; + final Value rev; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value lastSyncedAt; + final Value budgetClientId; + final Value periodStart; + final Value periodEnd; + final Value netSpent; + final Value rolloverIn; + final Value rolloverOut; + final Value closedAt; + final Value rowid; + const BudgetPeriodStatesCompanion({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + this.budgetClientId = const Value.absent(), + this.periodStart = const Value.absent(), + this.periodEnd = const Value.absent(), + this.netSpent = const Value.absent(), + this.rolloverIn = const Value.absent(), + this.rolloverOut = const Value.absent(), + this.closedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + BudgetPeriodStatesCompanion.insert({ + this.id = const Value.absent(), + this.userId = const Value.absent(), + this.clientId = const Value.absent(), + this.rev = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.lastSyncedAt = const Value.absent(), + required String budgetClientId, + required DateTime periodStart, + required DateTime periodEnd, + this.netSpent = const Value.absent(), + this.rolloverIn = const Value.absent(), + this.rolloverOut = const Value.absent(), + this.closedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : budgetClientId = Value(budgetClientId), + periodStart = Value(periodStart), + periodEnd = Value(periodEnd); + static Insertable custom({ + Expression? id, + Expression? userId, + Expression? clientId, + Expression? rev, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? lastSyncedAt, + Expression? budgetClientId, + Expression? periodStart, + Expression? periodEnd, + Expression? netSpent, + Expression? rolloverIn, + Expression? rolloverOut, + Expression? closedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (userId != null) 'user_id': userId, + if (clientId != null) 'client_id': clientId, + if (rev != null) 'rev': rev, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (lastSyncedAt != null) 'last_synced_at': lastSyncedAt, + if (budgetClientId != null) 'budget_client_id': budgetClientId, + if (periodStart != null) 'period_start': periodStart, + if (periodEnd != null) 'period_end': periodEnd, + if (netSpent != null) 'net_spent': netSpent, + if (rolloverIn != null) 'rollover_in': rolloverIn, + if (rolloverOut != null) 'rollover_out': rolloverOut, + if (closedAt != null) 'closed_at': closedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + BudgetPeriodStatesCompanion copyWith( + {Value? id, + Value? userId, + Value? clientId, + Value? rev, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? lastSyncedAt, + Value? budgetClientId, + Value? periodStart, + Value? periodEnd, + Value? netSpent, + Value? rolloverIn, + Value? rolloverOut, + Value? closedAt, + Value? rowid}) { + return BudgetPeriodStatesCompanion( + id: id ?? this.id, + userId: userId ?? this.userId, + clientId: clientId ?? this.clientId, + rev: rev ?? this.rev, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt, + budgetClientId: budgetClientId ?? this.budgetClientId, + periodStart: periodStart ?? this.periodStart, + periodEnd: periodEnd ?? this.periodEnd, + netSpent: netSpent ?? this.netSpent, + rolloverIn: rolloverIn ?? this.rolloverIn, + rolloverOut: rolloverOut ?? this.rolloverOut, + closedAt: closedAt ?? this.closedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (clientId.present) { + map['client_id'] = Variable(clientId.value); + } + if (rev.present) { + map['rev'] = Variable(rev.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (lastSyncedAt.present) { + map['last_synced_at'] = Variable(lastSyncedAt.value); + } + if (budgetClientId.present) { + map['budget_client_id'] = Variable(budgetClientId.value); + } + if (periodStart.present) { + map['period_start'] = Variable(periodStart.value); + } + if (periodEnd.present) { + map['period_end'] = Variable(periodEnd.value); + } + if (netSpent.present) { + map['net_spent'] = Variable(netSpent.value); + } + if (rolloverIn.present) { + map['rollover_in'] = Variable(rolloverIn.value); + } + if (rolloverOut.present) { + map['rollover_out'] = Variable(rolloverOut.value); + } + if (closedAt.present) { + map['closed_at'] = Variable(closedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BudgetPeriodStatesCompanion(') + ..write('id: $id, ') + ..write('userId: $userId, ') + ..write('clientId: $clientId, ') + ..write('rev: $rev, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('lastSyncedAt: $lastSyncedAt, ') + ..write('budgetClientId: $budgetClientId, ') + ..write('periodStart: $periodStart, ') + ..write('periodEnd: $periodEnd, ') + ..write('netSpent: $netSpent, ') + ..write('rolloverIn: $rolloverIn, ') + ..write('rolloverOut: $rolloverOut, ') + ..write('closedAt: $closedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV5 extends GeneratedDatabase { + DatabaseAtV5(QueryExecutor e) : super(e); + late final Wallets wallets = Wallets(this); + late final Parties parties = Parties(this); + late final Groups groups = Groups(this); + late final Transactions transactions = Transactions(this); + late final Categories categories = Categories(this); + late final Configs configs = Configs(this); + late final Users users = Users(this); + late final LocalChanges localChanges = LocalChanges(this); + late final SyncMetadata syncMetadata = SyncMetadata(this); + late final Categorizables categorizables = Categorizables(this); + late final Notifications notifications = Notifications(this); + late final MediaFiles mediaFiles = MediaFiles(this); + late final Transfers transfers = Transfers(this); + late final Budgets budgets = Budgets(this); + late final BudgetTargets budgetTargets = BudgetTargets(this); + late final BudgetPeriodStates budgetPeriodStates = BudgetPeriodStates(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + wallets, + parties, + groups, + transactions, + categories, + configs, + users, + localChanges, + syncMetadata, + categorizables, + notifications, + mediaFiles, + transfers, + budgets, + budgetTargets, + budgetPeriodStates + ]; + @override + int get schemaVersion => 5; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/test/unit/compute_local_progress_test.dart b/test/unit/compute_local_progress_test.dart new file mode 100644 index 00000000..8230ac78 --- /dev/null +++ b/test/unit/compute_local_progress_test.dart @@ -0,0 +1,380 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:trakli/data/services/budget/compute_local_progress.dart'; +import 'package:trakli/data/database/app_database.dart'; +import 'package:trakli/domain/entities/exchange_rate_entity.dart'; +import 'package:trakli/presentation/utils/enums.dart'; + +void main() { + // Reference moment: mid-May 2026. Sits inside the May monthly window so the + // pure function under test naturally picks May 1–May 31 as the period. + final now = DateTime.utc(2026, 5, 15, 12); + + Budget makeBudget({ + double amount = 500.0, + String currency = 'USD', + BudgetPeriodType periodType = BudgetPeriodType.monthly, + int thresholdPercent = 80, + DateTime? startDate, + DateTime? endDate, + bool rolloverEnabled = false, + }) { + return Budget( + clientId: 'budget-1', + name: 'Groceries', + slug: 'groceries', + amount: amount, + currency: currency, + periodType: periodType, + startDate: startDate ?? DateTime.utc(2026, 5, 1), + endDate: endDate, + rolloverEnabled: rolloverEnabled, + thresholdPercent: thresholdPercent, + forecastAlertsEnabled: false, + isActive: true, + ownerType: 'user', + createdAt: DateTime.utc(2026, 1, 1), + updatedAt: DateTime.utc(2026, 1, 1), + ); + } + + BudgetTarget categoryTarget(String categoryClientId) => BudgetTarget( + budgetClientId: 'budget-1', + targetType: BudgetTargetType.category, + targetClientId: categoryClientId, + ); + + BudgetTxnInput txn({ + required double amount, + DateTime? at, + TransactionType type = TransactionType.expense, + String walletClientId = 'w1', + String? walletCurrency = 'USD', + String? groupClientId, + Set categoryClientIds = const {}, + int? transferId, + }) { + return BudgetTxnInput( + type: type, + amount: amount, + datetime: at ?? DateTime.utc(2026, 5, 10), + transferId: transferId, + walletClientId: walletClientId, + walletCurrency: walletCurrency, + groupClientId: groupClientId, + categoryClientIds: categoryClientIds, + ); + } + + group('computeLocalProgress', () { + test('sums matching expense transactions in the current monthly window', + () { + final progress = computeLocalProgress( + budget: makeBudget(), + targets: [categoryTarget('cat-food')], + txns: [ + txn(amount: 45, categoryClientIds: {'cat-food'}), + txn(amount: 30, categoryClientIds: {'cat-food'}), + txn(amount: 12, categoryClientIds: {'cat-other'}), // skipped + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.grossSpent, 75.0); + expect(progress.netSpent, 75.0); + expect(progress.refunds, 0.0); + expect(progress.limit, 500.0); + expect(progress.effectiveLimit, 500.0); + expect(progress.remaining, 425.0); + expect(progress.percentUsed, closeTo(15.0, 0.001)); + expect(progress.status, BudgetStatus.onTrack); + expect(progress.isThresholdCrossed, false); + expect(progress.isForecastBreach, false); + expect(progress.periodStart, DateTime.utc(2026, 5, 1)); + }); + + test('empty targets is a catch-all', () { + final progress = computeLocalProgress( + budget: makeBudget(), + targets: const [], + txns: [ + txn(amount: 20), + txn(amount: 30, categoryClientIds: {'anything'}), + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 50.0); + }); + + test('skips transfers, income, transactions outside the period', () { + final progress = computeLocalProgress( + budget: makeBudget(), + targets: const [], + txns: [ + txn(amount: 100, transferId: 7), // transfer + txn(amount: 50, type: TransactionType.income), // income + txn(amount: 10, at: DateTime.utc(2026, 4, 30)), // before window + txn(amount: 10, at: DateTime.utc(2026, 6, 1)), // after window + txn(amount: 5), // counted + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 5.0); + }); + + test('matches by wallet target', () { + final progress = computeLocalProgress( + budget: makeBudget(), + targets: [ + const BudgetTarget( + budgetClientId: 'budget-1', + targetType: BudgetTargetType.wallet, + targetClientId: 'w-checking', + ), + ], + txns: [ + txn(amount: 25, walletClientId: 'w-checking'), + txn(amount: 99, walletClientId: 'w-other'), // skipped + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 25.0); + }); + + test('crosses threshold but stays on track when under 100%', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 100, thresholdPercent: 80), + targets: const [], + txns: [txn(amount: 85)], + lastKnownProgress: null, + now: now, + ); + + expect(progress.percentUsed, closeTo(85.0, 0.001)); + expect(progress.isThresholdCrossed, true); + expect(progress.status, BudgetStatus.nearLimit); + }); + + test('flips to over_budget when netSpent exceeds effective limit', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 100), + targets: const [], + txns: [txn(amount: 150)], + lastKnownProgress: null, + now: now, + ); + + expect(progress.remaining, -50.0); + expect(progress.status, BudgetStatus.overBudget); + expect(progress.percentUsed, closeTo(150.0, 0.001)); + }); + + test('preserves rolloverIn from the last known server progress', () { + final last = computeLocalProgress( + budget: makeBudget(amount: 100), + targets: const [], + txns: const [], + lastKnownProgress: null, + now: now, + ).copyWith(rolloverIn: 50.0); + + final progress = computeLocalProgress( + budget: makeBudget(amount: 100), + targets: const [], + txns: [txn(amount: 60)], + lastKnownProgress: last, + now: now, + ); + + expect(progress.rolloverIn, 50.0); + expect(progress.effectiveLimit, 150.0); + expect(progress.remaining, 90.0); + expect(progress.status, BudgetStatus.onTrack); // 60/150 = 40% + }); + + test('weekly window covers Mon–Sun of reference week', () { + // 2026-05-15 is a Friday → week is Mon May 11 – Sun May 17. + final progress = computeLocalProgress( + budget: makeBudget(periodType: BudgetPeriodType.weekly), + targets: const [], + txns: [ + txn(amount: 10, at: DateTime.utc(2026, 5, 11)), // Monday, in + txn(amount: 5, at: DateTime.utc(2026, 5, 17)), // Sunday, in + txn(amount: 99, at: DateTime.utc(2026, 5, 10)), // prior Sunday, out + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 15.0); + expect(progress.periodStart, DateTime.utc(2026, 5, 11)); + }); + + test('custom budget uses literal start/end dates', () { + final progress = computeLocalProgress( + budget: makeBudget( + periodType: BudgetPeriodType.custom, + startDate: DateTime.utc(2026, 5, 10), + endDate: DateTime.utc(2026, 5, 20), + ), + targets: const [], + txns: [ + txn(amount: 30, at: DateTime.utc(2026, 5, 15)), + txn(amount: 99, at: DateTime.utc(2026, 5, 21)), // out + txn(amount: 99, at: DateTime.utc(2026, 5, 9)), // out + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 30.0); + expect(progress.periodStart, DateTime.utc(2026, 5, 10)); + expect(progress.periodEnd, DateTime.utc(2026, 5, 20)); + }); + + test('zero amount budget over-budgets on first dollar spent', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 0), + targets: const [], + txns: [txn(amount: 1)], + lastKnownProgress: null, + now: now, + ); + + expect(progress.percentUsed, 100.0); + expect(progress.status, BudgetStatus.overBudget); + }); + + test( + 'foreign-currency transactions are excluded from netSpent when no ' + 'rate is available', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 500, currency: 'USD'), + targets: [categoryTarget('cat-food')], + txns: [ + // matches budget currency — counted + txn(amount: 30, walletCurrency: 'USD', categoryClientIds: {'cat-food'}), + // foreign wallet, no FX snapshot — excluded + txn(amount: 25, walletCurrency: 'EUR', categoryClientIds: {'cat-food'}), + txn(amount: 12, walletCurrency: 'EUR', categoryClientIds: {'cat-food'}), + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 30); + }); + + test('empty-targets (catch-all) still applies the currency filter', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 500, currency: 'USD'), + targets: const [], + txns: [ + txn(amount: 40, walletCurrency: 'USD'), + txn(amount: 60, walletCurrency: 'EUR'), + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 40); + }); + + test('transactions with unknown wallet currency are skipped', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 500, currency: 'USD'), + targets: [categoryTarget('cat-food')], + txns: [ + txn(amount: 30, walletCurrency: 'USD', categoryClientIds: {'cat-food'}), + txn(amount: 99, walletCurrency: null, categoryClientIds: {'cat-food'}), + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 30); + }); + + test('same-currency transactions are all counted', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 500, currency: 'USD'), + targets: [categoryTarget('cat-food')], + txns: [ + txn(amount: 30, walletCurrency: 'USD', categoryClientIds: {'cat-food'}), + txn(amount: 25, walletCurrency: 'USD', categoryClientIds: {'cat-food'}), + ], + lastKnownProgress: null, + now: now, + ); + + expect(progress.netSpent, 55); + }); + + group('currency conversion (current-rate snapshot)', () { + // rates are base-relative: units of per 1 unit of baseCode (USD). + // So 1 USD = 0.5 EUR, 1 USD = 0.8 GBP. + final fx = ExchangeRateEntity( + provider: 'test', + baseCode: 'USD', + rates: const {'USD': 1.0, 'EUR': 0.5, 'GBP': 0.8}, + timeLastUpdated: DateTime.utc(2026, 5, 1), + timeNextUpdated: DateTime.utc(2026, 5, 2), + ); + + test('converts a foreign-currency match into the budget currency', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 500, currency: 'USD'), + targets: [categoryTarget('cat-food')], + txns: [ + txn(amount: 30, walletCurrency: 'USD', categoryClientIds: {'cat-food'}), + // 50 EUR ÷ 0.5 = 100 USD + txn(amount: 50, walletCurrency: 'EUR', categoryClientIds: {'cat-food'}), + ], + lastKnownProgress: null, + now: now, + exchangeRate: fx, + ); + + expect(progress.netSpent, closeTo(130.0, 0.001)); + }); + + test('cross-rate via base when the budget currency is not the base', () { + // Budget in EUR; spend in GBP. 80 GBP → base: 80/0.8 = 100 USD → + // EUR: 100 * 0.5 = 50 EUR. + final progress = computeLocalProgress( + budget: makeBudget(amount: 500, currency: 'EUR'), + targets: const [], + txns: [txn(amount: 80, walletCurrency: 'GBP')], + lastKnownProgress: null, + now: now, + exchangeRate: fx, + ); + + expect(progress.netSpent, closeTo(50.0, 0.001)); + }); + + test('excludes the transaction when the pair has no rate', () { + final progress = computeLocalProgress( + budget: makeBudget(amount: 500, currency: 'USD'), + targets: const [], + txns: [ + txn(amount: 30, walletCurrency: 'USD'), + // JPY is absent from the snapshot → cannot convert. + txn(amount: 90, walletCurrency: 'JPY'), + ], + lastKnownProgress: null, + now: now, + exchangeRate: fx, + ); + + expect(progress.netSpent, 30); + }); + }); + }); +}