Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functional Expense Manager app #144

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions expense_manager/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
libisar.dylib
tmp

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
11 changes: 11 additions & 0 deletions expense_manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Introduction

`expense_manager` is a simple yet functional personal expense manager app built with Flutter. It allows you to:

- Track your daily expense
- Track your income
- View your expenses and incomes
- Group your expenses into categories
- Tag your expenses easily!
- A dashboard that shows the overall earnings
- Beautiful report pages with charts
32 changes: 32 additions & 0 deletions expense_manager/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
21 changes: 21 additions & 0 deletions expense_manager/lib/components/action_delete_icon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';

class ActionDeleteIcon extends StatelessWidget {
final VoidCallback onTap;

const ActionDeleteIcon({
super.key,
required this.onTap,
});

@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(12),
child: Icon(Icons.delete, color: Colors.white),
),
);
}
}
122 changes: 122 additions & 0 deletions expense_manager/lib/components/amount_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import 'package:expense_manager/misc/extensions.dart';
import 'package:expense_manager/components/dialog/custom_dialog.dart';
import 'package:expense_manager/misc/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class AmountDialog extends StatelessWidget {
final int amount;
final ValueChanged<int> onAmountChanged;

const AmountDialog({
super.key,
this.amount = 0,
required this.onAmountChanged,
});

@override
Widget build(BuildContext context) {
return CustomDialog(
maxWidth: 600,
child: GetBuilder<_AmountDialogController>(
init: _AmountDialogController(amount: amount),
builder: (controller) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
EdgeInsets.symmetric(horizontal: 16).copyWith(top: 16),
child: Text(
controller.amount.toMoney(),
style: Theme.of(context).textTheme.displaySmall,
),
),

// number pads
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
childAspectRatio: 2.3,
children: controller.keys.map((numKey) {
return InkWell(
onTap: () {
controller.calculateAmount(numKey);
},
child: Center(
child: numKey == 'back'
? Icon(Icons.backspace)
: Text(numKey, style: TextStyle(fontSize: 21)),
),
);
}).toList(),
),

Padding(
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
child: Text(
'CANCEL',
style: TextStyle(color: Colors.grey),
),
onPressed: () => goBack(context),
),
TextButton(
child: Text('OK'),
onPressed: () {
onAmountChanged.call(controller.amount);
goBack(context);
},
),
],
),
),
],
);
}),
);
}
}

class _AmountDialogController extends GetxController {
final List<String> keys = [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'',
'0',
'back'
];
int amount;

_AmountDialogController({required this.amount});

void calculateAmount(String numKey) {
if (numKey.isEmpty) return;

switch (numKey) {
case 'back':
String amountText = amount.toString();
amountText = amountText.length <= 1
? '0'
: amountText.substring(0, amountText.length - 1);
amount = int.parse(amountText);
break;
default:
final amountText = amount.toString() + numKey;
amount = int.parse(amountText);
}

update();
}
}
44 changes: 44 additions & 0 deletions expense_manager/lib/components/amount_input_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:expense_manager/misc/extensions.dart';
import 'package:flutter/material.dart';
import 'amount_dialog.dart';

class AmountInputView extends StatelessWidget {
final int initialAmount;
final ValueChanged<int> onChange;

const AmountInputView({
super.key,
this.initialAmount = 0,
required this.onChange,
});

@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => _showAmountDialog(context),
child: Padding(
padding: EdgeInsets.all(8),
child: Align(
alignment: Alignment.centerRight,
child: Text(
initialAmount.toMoney(),
textAlign: TextAlign.right,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
);
}

void _showAmountDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AmountDialog(
amount: initialAmount,
onAmountChanged: (int amount) {
onChange.call(amount);
},
),
);
}
}
104 changes: 104 additions & 0 deletions expense_manager/lib/components/button/custom_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import '../../misc/colors.dart';

class CustomButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final BorderRadius borderRadius;
final Color color;
final Color textColor;
final Color borderColor;
final Color? shadowColor;
final Icon? leftIcon;
final Icon? rightIcon;
final EdgeInsetsGeometry? padding;
final double elevation;
final bool disabled;
final bool loading;
final double? height;
final bool useCustomShape;

const CustomButton(
this.text, {
super.key,
this.onPressed,
this.disabled = false,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
this.color = CustomColors.primary,
this.textColor = Colors.white,
this.borderColor = Colors.transparent,
this.leftIcon,
this.rightIcon,
this.padding,
this.shadowColor,
this.elevation = 0,
this.loading = false,
this.height,
this.useCustomShape = true,
});

@override
Widget build(BuildContext context) {
return SizedBox(
height: height ?? 45,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: textColor,
backgroundColor: color,
elevation: 2,
shadowColor: shadowColor,
shape: useCustomShape
? RoundedRectangleBorder(
side: BorderSide(
color: borderColor,
),
borderRadius: borderRadius,
)
: null,
),
onPressed: disabled
? null
: () {
if (loading) return;
onPressed?.call();
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (leftIcon != null)
Align(
alignment: Alignment.centerLeft,
child: leftIcon,
),
Align(
alignment: Alignment.center,
child: loading
? _renderProgressBar()
: Text(
text,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16.0),
),
),
if (rightIcon != null)
Align(
alignment: Alignment.centerRight,
child: rightIcon,
)
],
),
),
);
}

Widget _renderProgressBar() {
return const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Colors.white),
strokeWidth: 2,
),
);
}
}
Loading