diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..8957b74 --- /dev/null +++ b/.ignore @@ -0,0 +1,8 @@ +android +ios +web +linux +.metadata +analysis_options.yaml +pubspec.lock +// lib/**/*.g.dart \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..ce01264 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + channel: unknown + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: android + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: ios + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: linux + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: macos + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: web + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: windows + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md deleted file mode 100644 index e5970b1..0000000 --- a/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# rluv - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/android/build.gradle b/android/build.gradle index 58a8c74..713d7f6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..8705c4a --- /dev/null +++ b/build.yaml @@ -0,0 +1,10 @@ +targets: + $default: + sources: + exclude: + - 'example/**' + builders: + json_serializable: + options: + explicit_to_json: true + field_rename: snake \ No newline at end of file diff --git a/lib/features/budget/screens/budget_overview.dart b/lib/features/budget/screens/budget_overview.dart index e69de29..7749807 100644 --- a/lib/features/budget/screens/budget_overview.dart +++ b/lib/features/budget/screens/budget_overview.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers.dart'; +import 'package:rluv/features/budget/widgets/add_transaction_dialog.dart'; +import 'package:rluv/features/budget/widgets/budget_net_bar.dart'; +import 'package:rluv/global/styles.dart'; +import 'package:rluv/global/utils.dart'; + +import '../../../global/store.dart'; + +class BudgetOverviewScreen extends ConsumerStatefulWidget { + const BudgetOverviewScreen({super.key}); + + @override + ConsumerState createState() => + _BudgetOverviewScreenState(); +} + +class _BudgetOverviewScreenState extends ConsumerState { + final budgetListScrollController = ScrollController(); + @override + Widget build(BuildContext context) { + final budgetCategoriesRef = ref.watch(Store().budgetCategoriesProvider); + final screen = BuildMedia(context).size; + return budgetCategoriesRef.when( + data: ((budgetCategories) => RefreshIndicator( + onRefresh: () async { + final _ = await ref.refresh(Store().dashboardProvider.future); + }, + child: Column( + children: [ + /// TOP HALF, TITLE & OVERVIEW + Container( + height: screen.height * 0.33, + width: screen.width, + color: Styles.purpleNurple, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(flex: 2), + Text( + formatDate(DateTime.now()), + style: const TextStyle( + fontSize: 16, color: Styles.electricBlue), + ), + const Spacer(), + const Text('MONTHLY', + style: TextStyle( + fontSize: 42, color: Styles.electricBlue)), + const Spacer(flex: 2), + const BudgetNetBar(isPositive: true, net: 5024.64), + const Spacer(), + const BudgetNetBar(isPositive: false, net: 2004.37), + const Spacer(), + ], + ), + ), + + /// BOTTOM HALF, BUDGET BREAKDOWN + Expanded( + child: SizedBox( + width: screen.width, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(14.0), + child: Container( + decoration: BoxDecoration( + color: Styles.blushingPink, + borderRadius: BorderRadius.circular(16.0), + ), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'BUDGET', + style: TextStyle( + fontSize: 28, + color: Styles.electricBlue), + ), + ), + Scrollbar( + controller: budgetListScrollController, + thumbVisibility: true, + child: ListView( + controller: budgetListScrollController, + shrinkWrap: true, + children: [ + for (final budget in budgetCategories) + Row( + children: [ + Padding( + padding: + const EdgeInsets.all(8.0), + child: SizedBox( + width: 85, + child: Text( + budget.name, + style: const TextStyle( + fontSize: 16.0), + ), + ), + ), + AnimatedContainer( + duration: const Duration( + milliseconds: 500), + child: Expanded( + child: Padding( + padding: const EdgeInsets.only( + right: 18.0), + child: Stack( + children: [ + Container( + height: 30, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: + BorderRadius + .circular(10.0), + ), + ), + Padding( + padding: + const EdgeInsets.all( + 2.0), + child: Container( + height: 26, + decoration: + BoxDecoration( + color: Styles + .emptyBarGrey, + borderRadius: + BorderRadius + .circular( + 10.0), + ), + ), + ), + Padding( + padding: + const EdgeInsets.all( + 2.0), + child: SizedBox( + height: 26, + child: + FractionallySizedBox( + heightFactor: 1.0, + widthFactor: 0.5, + child: Container( + decoration: + BoxDecoration( + color: + budget.color, + borderRadius: + BorderRadius + .circular( + 10.0), + ), + ), + ), + ), + ), + ], + ), + )), + ) + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ], + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 15.0), + child: Container( + height: 70, + decoration: BoxDecoration( + color: Styles.seaweedGreen, + borderRadius: BorderRadius.circular(15.0), + ), + child: InkWell( + child: const Center( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 20.0, vertical: 14.0), + child: Text( + 'Transaction History', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 25), + ), + ), + ), + onTap: () { + printRed('FIXME'); + }, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: Container( + decoration: BoxDecoration( + color: Styles.purpleNurple, + borderRadius: BorderRadius.circular(40.0), + ), + height: 80, + width: 80, + child: IconButton( + icon: const Icon( + Icons.add, + size: 48, + color: Styles.lavender, + ), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const AddTransactionDialog(); + }, + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ) + ], + ), + )), + loading: () => const CircularProgressIndicator(), + error: (error, stackTrace) => Text('Error: $error, \n\n$stackTrace'), + ); + } +} diff --git a/lib/features/budget/widgets/add_transaction_dialog.dart b/lib/features/budget/widgets/add_transaction_dialog.dart new file mode 100644 index 0000000..47d70f6 --- /dev/null +++ b/lib/features/budget/widgets/add_transaction_dialog.dart @@ -0,0 +1,162 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../global/api.dart'; +import '../../../global/store.dart'; +import '../../../global/styles.dart'; +import '../../../models/budget_category_model.dart'; +import '../../../models/transaction_model.dart'; + +class AddTransactionDialog extends ConsumerStatefulWidget { + const AddTransactionDialog({super.key}); + + @override + ConsumerState createState() => + _AddTransactionDialogState(); +} + +class _AddTransactionDialogState extends ConsumerState { + final amountController = TextEditingController(); + + @override + Widget build(BuildContext context) { + final List budgetCategories = + ref.read(Store().budgetCategoriesProvider).requireValue; + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + side: const BorderSide(color: Styles.purpleNurple, width: 5.0), + ), + child: StatefulBuilder(builder: (context, setState) { + TransactionType transactionType = TransactionType.expense; + BudgetCategory selectedBudgetCategory = budgetCategories.first; + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: Styles.purpleNurple, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row(children: [ + InkWell( + onTap: transactionType == TransactionType.expense + ? null + : () { + setState(() => + transactionType = TransactionType.expense); + }, + child: AnimatedContainer( + height: 30, + width: 70, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0)), + color: transactionType == TransactionType.expense + ? Styles.lavender + : Styles.sand), + duration: const Duration(milliseconds: 300), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Text('Expense'), + ), + ), + ), + InkWell( + onTap: transactionType == TransactionType.income + ? null + : () { + setState( + () => transactionType = TransactionType.income); + }, + child: AnimatedContainer( + height: 30, + width: 70, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0)), + color: transactionType == TransactionType.income + ? Styles.lavender + : Styles.sand), + duration: const Duration(milliseconds: 300), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Text('Income'), + ), + ), + ), + ]), + Row( + children: [ + const Text('Amount:'), + SizedBox( + width: 80, + child: TextField( + controller: amountController, + decoration: InputDecoration( + fillColor: Styles.lavender, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: + const BorderSide(color: Colors.transparent), + ), + ), + ), + ), + ], + ), + Row( + children: [ + const Padding( + padding: EdgeInsets.only(right: 28.0), + child: Text('Category:'), + ), + DropdownButton( + value: selectedBudgetCategory, + items: budgetCategories + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e.name), + ), + ) + .toList(), + onChanged: (BudgetCategory? value) { + if (value != null) { + if (kDebugMode) { + print('${value.name} selected'); + } + setState(() => selectedBudgetCategory = value); + } + }, + ), + ], + ), + ElevatedButton( + child: const Text('Add'), + onPressed: () { + Api().put( + path: 'transactions', + data: Transaction( + amount: double.parse(amountController.text), + addedByUserId: 1, + budgetCategoryId: 1, + budgetId: 1, + date: DateTime.now(), + transactionType: transactionType)); + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }), + ); + } +} diff --git a/lib/features/budget/widgets/budget_category_bar.dart b/lib/features/budget/widgets/budget_category_bar.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/budget/widgets/budget_net_bar.dart b/lib/features/budget/widgets/budget_net_bar.dart new file mode 100644 index 0000000..13d64f9 --- /dev/null +++ b/lib/features/budget/widgets/budget_net_bar.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:helpers/helpers.dart'; +import 'package:rluv/global/styles.dart'; + +class BudgetNetBar extends StatelessWidget { + const BudgetNetBar({super.key, required this.isPositive, required this.net}); + + final bool isPositive; + final double net; + + @override + Widget build(BuildContext context) { + final screenWidth = BuildMedia(context).width; + return Container( + width: screenWidth * 0.85, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: isPositive ? Styles.incomeBlue : Styles.expensesOrange, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + child: + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text( + isPositive ? 'Income' : 'Expenses', + style: const TextStyle( + fontSize: 20, + ), + ), + Text( + '\$$net', + style: TextStyle( + fontSize: 20, + color: isPositive ? Styles.incomeGreen : Styles.expensesRed), + ), + ]), + ), + ); + } +} diff --git a/lib/global/api.dart b/lib/global/api.dart index e69de29..04795e8 100644 --- a/lib/global/api.dart +++ b/lib/global/api.dart @@ -0,0 +1,96 @@ +import 'package:dio/dio.dart'; +import 'package:helpers/helpers/print.dart'; + +class Api { + static final Api _instance = Api._internal(); + + factory Api() { + if (_instance.initDone) return _instance; + + _instance.initDone = true; + _instance.dio = Dio(); + _instance.dio.options.baseUrl = "http://localhost:8081/"; + _instance.dio.interceptors.add( + InterceptorsWrapper( + onRequest: (RequestOptions options, RequestInterceptorHandler handler) { + // Do something before request is sent. + // If you want to resolve the request with custom data, + // you can resolve a `Response` using `handler.resolve(response)`. + // If you want to reject the request with a error message, + // you can reject with a `DioException` using `handler.reject(dioError)`. + return handler.next(options); + }, + onResponse: (Response response, ResponseInterceptorHandler handler) { + if (response.statusCode != null && + response.statusCode! < 500 && + response.statusCode! >= 400) { + return handler.reject(DioException.badResponse( + requestOptions: RequestOptions(), + response: response, + statusCode: response.statusCode!)); + } + // Do something with response data. + // If you want to reject the request with a error message, + // you can reject a `DioException` object using `handler.reject(dioError)`. + return handler.next(response); + }, + onError: (DioException e, ErrorInterceptorHandler handler) { + printPink(e); + // Do something with response error. + // If you want to resolve the request with some custom data, + // you can resolve a `Response` object using `handler.resolve(response)`. + return handler.next(e); + }, + ), + ); + return _instance; + } + Api._internal(); + + bool initDone = false; + late final Dio dio; + + Future?> get(String path) async { + try { + final res = await dio.get(path); + + if (res.data != null) { + return res.data as Map; + } + return null; + } catch (err) { + printRed('Error in get: $err'); + return null; + } + } + + Future?> put( + {required String path, Object? data}) async { + try { + final res = await dio.put(path, data: data); + + if (res.data != null) { + return res.data as Map; + } + return null; + } catch (err) { + printRed('Error in put: $err'); + return null; + } + } + + Future?> delete( + {required String path, Object? data}) async { + try { + final res = await dio.delete(path, data: data); + + if (res.data != null) { + return res.data as Map; + } + return null; + } catch (err) { + printRed('Error in delete: $err'); + return null; + } + } +} diff --git a/lib/global/store.dart b/lib/global/store.dart index e69de29..9a8fbc3 100644 --- a/lib/global/store.dart +++ b/lib/global/store.dart @@ -0,0 +1,81 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers/print.dart'; +import 'package:rluv/global/api.dart'; +import 'package:rluv/models/budget.dart'; +import 'package:rluv/models/budget_category_model.dart'; + +import '../models/family_model.dart'; +import '../models/transaction_model.dart'; +import '../models/user.dart'; + +class Store { + static final Store _instance = Store._internal(); + bool _initDone = false; + + factory Store() { + if (_instance._initDone) { + return _instance; + } + _instance._initDone = true; + _instance.dashboardProvider = FutureProvider?>( + (ref) async { + final family = await ref.watch(_instance.familyProvider.future); + return Api().get("dashboard/${family.id}"); + }, + ); + _instance.budgetCategoriesProvider = + FutureProvider>((ref) async { + final dash = await ref.watch(_instance.dashboardProvider.future); + printAmber(dash); + if (dash == null) return []; + final categories = dash['budget_categories'] as List; + return categories + .map( + (e) => BudgetCategory.fromJson(e as Map), + ) + .toList(); + }); + _instance.transactionsProvider = + FutureProvider>((ref) async { + final dash = await ref.watch(_instance.dashboardProvider.future); + if (dash == null) return []; + final transactions = dash['transactions'] as List; + return transactions + .map( + (e) => Transaction.fromJson(e as Map), + ) + .toList(); + }); + return _instance; + } + + Store._internal(); + + final FutureProvider userProvider = FutureProvider( + (ref) { + return User( + id: 0, + budgetId: 1, + createdAt: DateTime.now(), + familyId: 1, + lastActivityAt: DateTime.now(), + name: 'TEMP', + updatedAt: DateTime.now(), + ); + }, + ); + + final FutureProvider familyProvider = + FutureProvider((ref) => FamilyModel( + id: 1, + budgetId: 1, + createdAt: DateTime.now(), + updatedAt: DateTime.now())); + + late final FutureProvider> budgetCategoriesProvider; + late final FutureProvider budgetProvider; + late final FutureProvider> transactionsProvider; + late final FutureProvider?> dashboardProvider; + + void fetchDashboard() {} +} diff --git a/lib/global/styles.dart b/lib/global/styles.dart new file mode 100644 index 0000000..4b71883 --- /dev/null +++ b/lib/global/styles.dart @@ -0,0 +1,21 @@ +import 'dart:ui'; + +class Styles { + // Theme Colors + static const Color purpleNurple = Color(0xffA188A6); + static const Color sunflower = Color(0xffFFEE88); + static const Color electricBlue = Color(0xFF19647E); + static const Color blushingPink = Color(0xFFE78F8E); + static const Color seaweedGreen = Color(0xFF86BA90); + static const Color emptyBarGrey = Color(0xFFC8C8C8); + static const Color lavender = Color(0xFFB8B8FF); + static const Color sand = Color(0xFFD9D9D9); + + // Income Colors + static const Color incomeBlue = Color(0xFFB8B8FF); + static const Color incomeGreen = Color(0xFF0FA102); + + // Expenses Colors + static const Color expensesOrange = Color(0xFFFA824C); + static const Color expensesRed = Color(0xFF9E0000); +} diff --git a/lib/global/utils.dart b/lib/global/utils.dart new file mode 100644 index 0000000..1a30b0c --- /dev/null +++ b/lib/global/utils.dart @@ -0,0 +1,17 @@ +import 'dart:ui'; + +import 'package:intl/intl.dart'; + +String formatDate(DateTime time) { + return DateFormat('EEEE, dd MMMM yyyy').format(time); +} + +DateTime dateFromJson(int value) => DateTime.fromMillisecondsSinceEpoch(value); + +int dateToJson(DateTime? value) => + value == null ? 0 : value.millisecondsSinceEpoch; + +String colorToJson(Color color) => + color.toString().split('(0x')[1].split(')')[0]; + +Color colorFromJson(String hex) => Color(int.parse(hex, radix: 16)); diff --git a/lib/main.dart b/lib/main.dart index 008fa38..f7a4a4b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rluv/features/budget/screens/budget_overview.dart'; +import 'package:rluv/global/styles.dart'; void main() { runApp(const MyApp()); @@ -10,106 +13,57 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.blue, + return ProviderScope( + child: MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const Home(), ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; +class Home extends ConsumerStatefulWidget { + const Home({super.key}); @override - State createState() => _MyHomePageState(); + ConsumerState createState() => _HomeState(); } -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - +class _HomeState extends ConsumerState { @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), + resizeToAvoidBottomInset: false, + drawer: const Drawer( + backgroundColor: Styles.purpleNurple, ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + appBar: AppBar( + backgroundColor: Styles.purpleNurple, + title: Text( + ref.watch(appBarTitleProvider), ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + body: ref.watch(currentHomePageProvider), ); } } + +final currentHomePageProvider = StateProvider( + (ref) => const BudgetOverviewScreen(), +); + +final appBarTitleProvider = Provider( + (ref) { + final currentPageName = ref.watch(currentHomePageProvider).toString(); + switch (currentPageName) { + case 'BudgetOverviewScreen': + return 'Budget'; + default: + return ''; + } + }, +); diff --git a/lib/models/budget.dart b/lib/models/budget.dart new file mode 100644 index 0000000..b28db81 --- /dev/null +++ b/lib/models/budget.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'budget.g.dart'; + +@JsonSerializable() +class Budget { + const Budget({ + this.id, + required this.name, + required this.createdAt, + required this.updatedAt, + }); + + final int? id; + final String name; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime createdAt; + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime updatedAt; + + factory Budget.fromJson(Map json) => _$BudgetFromJson(json); + + Map toJson() => _$BudgetToJson(this); +} diff --git a/lib/models/budget.g.dart b/lib/models/budget.g.dart new file mode 100644 index 0000000..1fba5c4 --- /dev/null +++ b/lib/models/budget.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'budget.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Budget _$BudgetFromJson(Map json) => Budget( + id: json['id'] as int?, + name: json['name'] as String, + createdAt: dateFromJson(json['created_at'] as int), + updatedAt: dateFromJson(json['updated_at'] as int), + ); + +Map _$BudgetToJson(Budget instance) => { + 'id': instance.id, + 'name': instance.name, + 'created_at': dateToJson(instance.createdAt), + 'updated_at': dateToJson(instance.updatedAt), + }; diff --git a/lib/models/budget_category_model.dart b/lib/models/budget_category_model.dart new file mode 100644 index 0000000..90a6e34 --- /dev/null +++ b/lib/models/budget_category_model.dart @@ -0,0 +1,35 @@ +import 'dart:ui'; + +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'budget_category_model.g.dart'; + +@JsonSerializable() +class BudgetCategory { + const BudgetCategory({ + this.id, + required this.budgetId, + required this.name, + required this.color, + required this.createdAt, + required this.amount, + }); + + final int budgetId; + final int? id; + final String name; + final double amount; + + @JsonKey(fromJson: colorFromJson, toJson: colorToJson) + final Color color; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime createdAt; + + factory BudgetCategory.fromJson(Map json) => + _$BudgetCategoryFromJson(json); + + Map toJson() => _$BudgetCategoryToJson(this); +} diff --git a/lib/models/budget_category_model.g.dart b/lib/models/budget_category_model.g.dart new file mode 100644 index 0000000..a47efe8 --- /dev/null +++ b/lib/models/budget_category_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'budget_category_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BudgetCategory _$BudgetCategoryFromJson(Map json) => + BudgetCategory( + id: json['id'] as int?, + budgetId: json['budget_id'] as int, + name: json['name'] as String, + color: colorFromJson(json['color'] as String), + createdAt: dateFromJson(json['created_at'] as int), + amount: (json['amount'] as num).toDouble(), + ); + +Map _$BudgetCategoryToJson(BudgetCategory instance) => + { + 'budget_id': instance.budgetId, + 'id': instance.id, + 'name': instance.name, + 'amount': instance.amount, + 'color': colorToJson(instance.color), + 'created_at': dateToJson(instance.createdAt), + }; diff --git a/lib/models/family_model.dart b/lib/models/family_model.dart new file mode 100644 index 0000000..8c9be80 --- /dev/null +++ b/lib/models/family_model.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'family_model.g.dart'; + +@JsonSerializable() +class FamilyModel { + const FamilyModel({ + required this.id, + required this.budgetId, + required this.createdAt, + required this.updatedAt, + }); + + final int id, budgetId; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime createdAt; + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime updatedAt; + + factory FamilyModel.fromJson(Map json) => + _$FamilyModelFromJson(json); + + Map toJson() => _$FamilyModelToJson(this); +} diff --git a/lib/models/family_model.g.dart b/lib/models/family_model.g.dart new file mode 100644 index 0000000..e411dbf --- /dev/null +++ b/lib/models/family_model.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'family_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FamilyModel _$FamilyModelFromJson(Map json) => FamilyModel( + id: json['id'] as int, + budgetId: json['budget_id'] as int, + createdAt: dateFromJson(json['created_at'] as int), + updatedAt: dateFromJson(json['updated_at'] as int), + ); + +Map _$FamilyModelToJson(FamilyModel instance) => + { + 'id': instance.id, + 'budget_id': instance.budgetId, + 'created_at': dateToJson(instance.createdAt), + 'updated_at': dateToJson(instance.updatedAt), + }; diff --git a/lib/models/transaction_model.dart b/lib/models/transaction_model.dart new file mode 100644 index 0000000..b2fc6a9 --- /dev/null +++ b/lib/models/transaction_model.dart @@ -0,0 +1,40 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'transaction_model.g.dart'; + +enum TransactionType { + income, + expense, +} + +@JsonSerializable() +class Transaction { + const Transaction({ + this.id, + required this.amount, + required this.transactionType, + required this.budgetId, + required this.budgetCategoryId, + required this.addedByUserId, + required this.date, + this.createdAt, + }); + + final int? id; + final int budgetId, budgetCategoryId, addedByUserId; + final double amount; + final TransactionType transactionType; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime date; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? createdAt; + + factory Transaction.fromJson(Map json) => + _$TransactionFromJson(json); + + Map toJson() => _$TransactionToJson(this); +} diff --git a/lib/models/transaction_model.g.dart b/lib/models/transaction_model.g.dart new file mode 100644 index 0000000..573be33 --- /dev/null +++ b/lib/models/transaction_model.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'transaction_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Transaction _$TransactionFromJson(Map json) => Transaction( + id: json['id'] as int?, + amount: (json['amount'] as num).toDouble(), + transactionType: + $enumDecode(_$TransactionTypeEnumMap, json['transaction_type']), + budgetId: json['budget_id'] as int, + budgetCategoryId: json['budget_category_id'] as int, + addedByUserId: json['added_by_user_id'] as int, + date: dateFromJson(json['date'] as int), + createdAt: dateFromJson(json['created_at'] as int), + ); + +Map _$TransactionToJson(Transaction instance) => + { + 'id': instance.id, + 'budget_id': instance.budgetId, + 'budget_category_id': instance.budgetCategoryId, + 'added_by_user_id': instance.addedByUserId, + 'amount': instance.amount, + 'transaction_type': _$TransactionTypeEnumMap[instance.transactionType]!, + 'date': dateToJson(instance.date), + 'created_at': dateToJson(instance.createdAt), + }; + +const _$TransactionTypeEnumMap = { + TransactionType.income: 'income', + TransactionType.expense: 'expense', +}; diff --git a/lib/models/user.dart b/lib/models/user.dart new file mode 100644 index 0000000..dd2ca16 --- /dev/null +++ b/lib/models/user.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'user.g.dart'; + +@JsonSerializable() +class User { + const User({ + this.id, + required this.name, + required this.familyId, + required this.budgetId, + this.createdAt, + this.updatedAt, + this.lastActivityAt, + }); + + final int? id; + final int familyId, budgetId; + final String name; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? createdAt; + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? updatedAt; + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? lastActivityAt; + + factory User.fromJson(Map json) => _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart new file mode 100644 index 0000000..acd57a0 --- /dev/null +++ b/lib/models/user.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) => User( + id: json['id'] as int?, + name: json['name'] as String, + familyId: json['family_id'] as int, + budgetId: json['budget_id'] as int, + createdAt: dateFromJson(json['created_at'] as int), + updatedAt: dateFromJson(json['updated_at'] as int), + lastActivityAt: dateFromJson(json['last_activity_at'] as int), + ); + +Map _$UserToJson(User instance) => { + 'id': instance.id, + 'family_id': instance.familyId, + 'budget_id': instance.budgetId, + 'name': instance.name, + 'created_at': dateToJson(instance.createdAt), + 'updated_at': dateToJson(instance.updatedAt), + 'last_activity_at': dateToJson(instance.lastActivityAt), + }; diff --git a/pubspec.lock b/pubspec.lock index 677f451..cb577c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -296,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" io: dependency: transitive description: @@ -308,10 +316,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -348,10 +356,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -364,10 +372,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -388,10 +396,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider_linux: dependency: transitive description: @@ -617,10 +625,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timing: dependency: transitive description: @@ -694,5 +702,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0-0 <4.0.0" flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8f11290..d65378c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: uuid: ^3.0.7 json_annotation: ^4.8.0 shared_preferences: ^2.1.2 + intl: ^0.18.1 dev_dependencies: flutter_test: